diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java index 140713130..95204811f 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java @@ -113,7 +113,7 @@ protected void addZrtpAdvertisedEncryptions( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) && accountID.isEncryptionProtocolEnabled( - ZrtpControl.PROTO_NAME) + SrtpControlType.ZRTP) && call.isSipZrtpAttribute()) { // ZRTP @@ -169,7 +169,7 @@ protected void addSDesAdvertisedEncryptions( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) && accountID.isEncryptionProtocolEnabled( - SDesControl.PROTO_NAME)) + SrtpControlType.SDES)) { SrtpControls srtpControls = getSrtpControls(); SDesControl sdesControl @@ -320,7 +320,7 @@ protected boolean setZrtpEncryptionOnDescription( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) && accountID.isEncryptionProtocolEnabled( - ZrtpControl.PROTO_NAME) + SrtpControlType.ZRTP) && call.isSipZrtpAttribute()) { ZrtpControl zrtpControl @@ -398,7 +398,7 @@ protected boolean setSDesEncryptionOnDescription( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) && accountID.isEncryptionProtocolEnabled( - SDesControl.PROTO_NAME)) + SrtpControlType.SDES)) { // get or create the control SrtpControls srtpControls = getSrtpControls(); diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java index 7aa09bafa..316d4c401 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java @@ -1,1407 +1,1407 @@ -/* - * Jitsi, the OpenSource Java VoIP and Instant Messaging client. - * - * Distributable under LGPL license. - * See terms of license at gnu.org. - */ -package net.java.sip.communicator.impl.protocol.jabber; - -import java.lang.ref.*; -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.neomedia.*; -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.filter.*; -import org.jivesoftware.smack.packet.*; -import org.jivesoftware.smackx.packet.*; - -/** - * A Jabber implementation of the Call abstract class encapsulating - * Jabber jingle sessions. - * - * @author Emil Ivov - * @author Lyubomir Marinov - * @author Boris Grozev - */ -public class CallJabberImpl - extends AbstractCallJabberGTalkImpl -{ - /** - * The Logger used by the CallJabberImpl class and its - * instances for logging output. - */ - private static final Logger logger = Logger.getLogger(CallJabberImpl.class); - - /** - * The Jitsi Videobridge conference which the local peer represented by this - * instance is a focus of. - */ - private ColibriConferenceIQ colibri; - - /** - * The shared CallPeerMediaHandler state which is to be used by the - * CallPeers of this Call which use {@link #colibri}. - */ - private MediaHandler colibriMediaHandler; - - /** - * Contains one ColibriStreamConnector for each MediaType - */ - private final List> - colibriStreamConnectors; - - /** - * The entity ID of the Jitsi Videobridge to be utilized by this - * Call for the purposes of establishing a server-assisted - * telephony conference. - */ - private String jitsiVideobridge; - - /** - * Initializes a new CallJabberImpl instance. - * - * @param parentOpSet the {@link OperationSetBasicTelephonyJabberImpl} - * instance in the context of which this call has been created. - */ - protected CallJabberImpl( - OperationSetBasicTelephonyJabberImpl parentOpSet) - { - super(parentOpSet); - - int mediaTypeValueCount = MediaType.values().length; - - colibriStreamConnectors - = new ArrayList>( - mediaTypeValueCount); - for (int i = 0; i < mediaTypeValueCount; i++) - colibriStreamConnectors.add(null); - - //let's add ourselves to the calls repo. we are doing it ourselves just - //to make sure that no one ever forgets. - parentOpSet.getActiveCallsRepository().addCall(this); - } - - /** - * Closes a specific ColibriStreamConnector which is associated with - * a MediaStream of a specific MediaType upon request from - * a specific CallPeer. - * - * @param peer the CallPeer which requests the closing of the - * specified colibriStreamConnector - * @param mediaType the MediaType of the MediaStream with - * which the specified colibriStreamConnector is associated - * @param colibriStreamConnector the ColibriStreamConnector to close on - * behalf of the specified peer - */ - public void closeColibriStreamConnector( - CallPeerJabberImpl peer, - MediaType mediaType, - ColibriStreamConnector colibriStreamConnector) - { - colibriStreamConnector.close(); - synchronized (colibriStreamConnectors) - { - int index = mediaType.ordinal(); - WeakReference weakReference - = colibriStreamConnectors.get(index); - if (weakReference != null && colibriStreamConnector - .equals(weakReference.get())) - { - colibriStreamConnectors.set(index, null); - } - } - } - - /** - * {@inheritDoc} - * - * Sends a content message to each of the CallPeers - * associated with this CallJabberImpl in order to include/exclude - * the "isfocus" attribute. - */ - @Override - protected void conferenceFocusChanged(boolean oldValue, boolean newValue) - { - try - { - Iterator peers = getCallPeers(); - - while (peers.hasNext()) - { - CallPeerJabberImpl callPeer = peers.next(); - - if (callPeer.getState() == CallPeerState.CONNECTED) - callPeer.sendCoinSessionInfo(); - } - } - finally - { - super.conferenceFocusChanged(oldValue, newValue); - } - } - - /** - * Allocates colibri (conference) channels for a specific MediaType - * to be used by a specific CallPeer. - * - * @param peer the CallPeer which is to use the allocated colibri - * (conference) channels - * @param contentMap the local and remote ContentPacketExtensions - * which specify the MediaTypes for which colibri (conference) - * channels are to be allocated - * @return a ColibriConferenceIQ which describes the allocated - * colibri (conference) channels for the specified mediaTypes which - * are to be used by the specified peer; otherwise, null - */ - public ColibriConferenceIQ createColibriChannels( - CallPeerJabberImpl peer, - Map contentMap) - throws OperationFailedException - { - if (!getConference().isJitsiVideobridge()) - return null; - - /* - * For a colibri conference to work properly, all CallPeers in the - * conference must share one and the same CallPeerMediaHandler state - * i.e. they must use a single set of MediaStreams as if there was a - * single CallPeerMediaHandler. - */ - CallPeerMediaHandlerJabberImpl peerMediaHandler - = peer.getMediaHandler(); - - if (peerMediaHandler.getMediaHandler() != colibriMediaHandler) - { - for (MediaType mediaType : MediaType.values()) - { - if (peerMediaHandler.getStream(mediaType) != null) - return null; - } - } - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - String jitsiVideobridge - = (colibri == null) ? getJitsiVideobridge() : colibri.getFrom(); - - if ((jitsiVideobridge == null) || (jitsiVideobridge.length() == 0)) - { - logger.error( - "Failed to allocate colibri channels: no videobridge" - + " found."); - return null; - } - - /* - * The specified CallPeer will participate in the colibri conference - * organized by this Call so it must use the shared CallPeerMediaHandler - * state of all CallPeers in the same colibri conference. - */ - if (colibriMediaHandler == null) - colibriMediaHandler = new MediaHandler(); - peerMediaHandler.setMediaHandler(colibriMediaHandler); - - ColibriConferenceIQ conferenceRequest = new ColibriConferenceIQ(); - - if (colibri != null) - conferenceRequest.setID(colibri.getID()); - - for (Map.Entry e - : contentMap.entrySet()) - { - ContentPacketExtension localContent = e.getKey(); - ContentPacketExtension remoteContent = e.getValue(); - ContentPacketExtension cpe - = (remoteContent == null) ? localContent : remoteContent; - RtpDescriptionPacketExtension rdpe - = cpe.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - String media = rdpe.getMedia(); - MediaType mediaType = MediaType.parseString(media); - String contentName = mediaType.toString(); - ColibriConferenceIQ.Content contentRequest - = new ColibriConferenceIQ.Content(contentName); - - conferenceRequest.addContent(contentRequest); - - boolean requestLocalChannel = true; - - if (colibri != null) - { - ColibriConferenceIQ.Content content - = colibri.getContent(contentName); - - if ((content != null) && (content.getChannelCount() > 0)) - requestLocalChannel = false; - } - - boolean peerIsInitiator = peer.isInitiator(); - - if (requestLocalChannel) - { - ColibriConferenceIQ.Channel localChannelRequest - = new ColibriConferenceIQ.Channel(); - - localChannelRequest.setEndpoint(protocolProvider.getOurJID()); - localChannelRequest.setInitiator(peerIsInitiator); - - for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) - localChannelRequest.addPayloadType(ptpe); - setTransportOnChannel(peer, media, localChannelRequest); - // DTLS-SRTP - setDtlsEncryptionOnChannel( - jitsiVideobridge, - peer, - mediaType, - localChannelRequest); - /* - * Since Jitsi Videobridge supports multiple Jingle transports, - * it is a good idea to indicate which one is expected on a - * channel. - */ - ensureTransportOnChannel(localChannelRequest, peer); - contentRequest.addChannel(localChannelRequest); - } - - ColibriConferenceIQ.Channel remoteChannelRequest - = new ColibriConferenceIQ.Channel(); - - remoteChannelRequest.setEndpoint(peer.getAddress()); - remoteChannelRequest.setInitiator(!peerIsInitiator); - - for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) - remoteChannelRequest.addPayloadType(ptpe); - setTransportOnChannel( - media, - localContent, - remoteContent, - peer, - remoteChannelRequest); - // DTLS-SRTP - setDtlsEncryptionOnChannel( - mediaType, - localContent, - remoteContent, - peer, - remoteChannelRequest); - /* - * Since Jitsi Videobridge supports multiple Jingle transports, it - * is a good idea to indicate which one is expected on a channel. - */ - ensureTransportOnChannel(remoteChannelRequest, peer); - contentRequest.addChannel(remoteChannelRequest); - } - - XMPPConnection connection = protocolProvider.getConnection(); - PacketCollector packetCollector - = connection.createPacketCollector( - new PacketIDFilter(conferenceRequest.getPacketID())); - - conferenceRequest.setTo(jitsiVideobridge); - conferenceRequest.setType(IQ.Type.GET); - connection.sendPacket(conferenceRequest); - - Packet response - = packetCollector.nextResult( - SmackConfiguration.getPacketReplyTimeout()); - - packetCollector.cancel(); - - if (response == null) - { - logger.error( - "Failed to allocate colibri channels: response is null." - + " Maybe the response timed out."); - return null; - } - else if (response.getError() != null) - { - logger.error( - "Failed to allocate colibri channels: " - + response.getError()); - return null; - } - else if (!(response instanceof ColibriConferenceIQ)) - { - logger.error( - "Failed to allocate colibri channels: response is not a" - + " colibri conference"); - return null; - } - - ColibriConferenceIQ conferenceResponse = (ColibriConferenceIQ) response; - String conferenceResponseID = conferenceResponse.getID(); - - /* - * Update the complete ColibriConferenceIQ representation maintained by - * this instance with the information given by the (current) response. - */ - { - if (colibri == null) - { - colibri = new ColibriConferenceIQ(); - /* - * XXX We must remember the JID of the Jitsi Videobridge because - * (1) we do not want to re-discover it in every method - * invocation on this Call instance and (2) we want to use one - * and the same for all CallPeers within this Call instance. - */ - colibri.setFrom(conferenceResponse.getFrom()); - } - - String colibriID = colibri.getID(); - - if (colibriID == null) - colibri.setID(conferenceResponseID); - else if (!colibriID.equals(conferenceResponseID)) - throw new IllegalStateException("conference.id"); - - for (ColibriConferenceIQ.Content contentResponse - : conferenceResponse.getContents()) - { - String contentName = contentResponse.getName(); - ColibriConferenceIQ.Content content - = colibri.getOrCreateContent(contentName); - - for (ColibriConferenceIQ.Channel channelResponse - : contentResponse.getChannels()) - { - int channelIndex = content.getChannelCount(); - - content.addChannel(channelResponse); - if (channelIndex == 0) - { - TransportManagerJabberImpl transportManager - = peerMediaHandler.getTransportManager(); - - transportManager - .isEstablishingConnectivityWithJitsiVideobridge - = true; - transportManager - .startConnectivityEstablishmentWithJitsiVideobridge - = true; - - MediaType mediaType - = MediaType.parseString(contentName); - - // DTLS-SRTP - addDtlsAdvertisedEncryptions( - peer, - channelResponse, - mediaType); - } - } - } - } - - /* - * Formulate the result to be returned to the caller which is a subset - * of the whole conference information kept by this CallJabberImpl and - * includes the remote channels explicitly requested by the method - * caller and their respective local channels. - */ - ColibriConferenceIQ conferenceResult = new ColibriConferenceIQ(); - - conferenceResult.setFrom(colibri.getFrom()); - conferenceResult.setID(conferenceResponseID); - - for (Map.Entry e - : contentMap.entrySet()) - { - ContentPacketExtension localContent = e.getKey(); - ContentPacketExtension remoteContent = e.getValue(); - ContentPacketExtension cpe - = (remoteContent == null) ? localContent : remoteContent; - RtpDescriptionPacketExtension rdpe - = cpe.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - MediaType mediaType = MediaType.parseString(rdpe.getMedia()); - ColibriConferenceIQ.Content contentResponse - = conferenceResponse.getContent(mediaType.toString()); - - if (contentResponse != null) - { - String contentName = contentResponse.getName(); - ColibriConferenceIQ.Content contentResult - = new ColibriConferenceIQ.Content(contentName); - - conferenceResult.addContent(contentResult); - - /* - * The local channel may have been allocated in a previous - * method call as part of the allocation of the first remote - * channel in the respective content. Anyway, the current method - * caller still needs to know about it. - */ - ColibriConferenceIQ.Content content - = colibri.getContent(contentName); - ColibriConferenceIQ.Channel localChannel = null; - - if ((content != null) && (content.getChannelCount() > 0)) - { - localChannel = content.getChannel(0); - contentResult.addChannel(localChannel); - } - - String localChannelID - = (localChannel == null) ? null : localChannel.getID(); - - for (ColibriConferenceIQ.Channel channelResponse - : contentResponse.getChannels()) - { - if ((localChannelID == null) - || !localChannelID.equals(channelResponse.getID())) - contentResult.addChannel(channelResponse); - } - } - } - - return conferenceResult; - } - - /** - * Initializes a ColibriStreamConnector on behalf of a specific - * CallPeer to be used in association with a specific - * ColibriConferenceIQ.Channel of a specific MediaType. - * - * @param peer the CallPeer which requests the initialization of a - * ColibriStreamConnector - * @param mediaType the MediaType of the stream which is to use the - * initialized ColibriStreamConnector for RTP and RTCP traffic - * @param channel the ColibriConferenceIQ.Channel to which RTP and - * RTCP traffic is to be sent and from which such traffic is to be received - * via the initialized ColibriStreamConnector - * @param factory a StreamConnectorFactory implementation which is - * to allocate the sockets to be used for RTP and RTCP traffic - * @return a ColibriStreamConnector to be used for RTP and RTCP - * traffic associated with the specified channel - */ - public ColibriStreamConnector createColibriStreamConnector( - CallPeerJabberImpl peer, - MediaType mediaType, - ColibriConferenceIQ.Channel channel, - StreamConnectorFactory factory) - { - String channelID = channel.getID(); - - if (channelID == null) - throw new IllegalArgumentException("channel"); - - if (colibri == null) - throw new IllegalStateException("colibri"); - - ColibriConferenceIQ.Content content - = colibri.getContent(mediaType.toString()); - - if (content == null) - throw new IllegalArgumentException("mediaType"); - if ((content.getChannelCount() < 1) - || !channelID.equals((channel = content.getChannel(0)).getID())) - throw new IllegalArgumentException("channel"); - - ColibriStreamConnector colibriStreamConnector; - - synchronized (colibriStreamConnectors) - { - int index = mediaType.ordinal(); - WeakReference weakReference - = colibriStreamConnectors.get(index); - - colibriStreamConnector - = (weakReference == null) ? null : weakReference.get(); - if (colibriStreamConnector == null) - { - StreamConnector streamConnector - = factory.createStreamConnector(); - - if (streamConnector != null) - { - colibriStreamConnector - = new ColibriStreamConnector(streamConnector); - colibriStreamConnectors.set( - index, - new WeakReference( - colibriStreamConnector)); - } - } - } - - return colibriStreamConnector; - } - - /** - * Expires specific (colibri) conference channels used by a specific - * CallPeer. - * - * @param peer the CallPeer which uses the specified (colibri) - * conference channels to be expired - * @param conference a ColibriConferenceIQ which specifies the - * (colibri) conference channels to be expired - */ - public void expireColibriChannels( - CallPeerJabberImpl peer, - ColibriConferenceIQ conference) - { - // Formulate the ColibriConferenceIQ request which is to be sent. - if (colibri != null) - { - String conferenceID = colibri.getID(); - - if (conferenceID.equals(conference.getID())) - { - ColibriConferenceIQ conferenceRequest - = new ColibriConferenceIQ(); - - conferenceRequest.setID(conferenceID); - - for (ColibriConferenceIQ.Content content - : conference.getContents()) - { - ColibriConferenceIQ.Content colibriContent - = colibri.getContent(content.getName()); - - if (colibriContent != null) - { - ColibriConferenceIQ.Content contentRequest - = conferenceRequest.getOrCreateContent( - colibriContent.getName()); - - for (ColibriConferenceIQ.Channel channel - : content.getChannels()) - { - ColibriConferenceIQ.Channel colibriChannel - = colibriContent.getChannel(channel.getID()); - - if (colibriChannel != null) - { - ColibriConferenceIQ.Channel channelRequest - = new ColibriConferenceIQ.Channel(); - - channelRequest.setExpire(0); - channelRequest.setID(colibriChannel.getID()); - contentRequest.addChannel(channelRequest); - } - } - } - } - - /* - * Remove the channels which are to be expired from the internal - * state of the conference managed by this CallJabberImpl. - */ - for (ColibriConferenceIQ.Content contentRequest - : conferenceRequest.getContents()) - { - ColibriConferenceIQ.Content colibriContent - = colibri.getContent(contentRequest.getName()); - - for (ColibriConferenceIQ.Channel channelRequest - : contentRequest.getChannels()) - { - ColibriConferenceIQ.Channel colibriChannel - = colibriContent.getChannel(channelRequest.getID()); - - colibriContent.removeChannel(colibriChannel); - - /* - * If the last remote channel is to be expired, expire - * the local channel as well. - */ - if (colibriContent.getChannelCount() == 1) - { - colibriChannel = colibriContent.getChannel(0); - - channelRequest = new ColibriConferenceIQ.Channel(); - channelRequest.setExpire(0); - channelRequest.setID(colibriChannel.getID()); - contentRequest.addChannel(channelRequest); - - colibriContent.removeChannel(colibriChannel); - - break; - } - } - } - - /* - * At long last, send the ColibriConferenceIQ request to expire - * the channels. - */ - conferenceRequest.setTo(colibri.getFrom()); - conferenceRequest.setType(IQ.Type.SET); - getProtocolProvider().getConnection().sendPacket( - conferenceRequest); - } - } - } - - /** - * Sends a ColibriConferenceIQ to the videobridge used by this - * CallJabberImpl, in order to request the the direction of - * the channel with ID channelID be set to - * direction - * @param channelID the ID of the channel for which to set the - * direction. - * @param mediaType the MediaType of the channel (we can deduce this - * by searching the ColibriConferenceIQ, but it's more convenient - * to have it) - * @param direction the MediaDirection to set. - */ - public void setChannelDirection(String channelID, - MediaType mediaType, - MediaDirection direction) - { - if ((colibri != null) && (channelID != null)) - { - ColibriConferenceIQ.Content content - = colibri.getContent(mediaType.toString()); - - if (content != null) - { - ColibriConferenceIQ.Channel channel - = content.getChannel(channelID); - - /* - * Note that we send requests even when the local Channel's - * direction and the direction we are setting are the same. We - * can easily avoid this, but we risk not sending necessary - * packets if local Channel and the actual channel on the - * videobridge are out of sync. - */ - if (channel != null) - { - ColibriConferenceIQ.Channel requestChannel - = new ColibriConferenceIQ.Channel(); - - requestChannel.setID(channelID); - requestChannel.setDirection(direction); - - ColibriConferenceIQ.Content requestContent - = new ColibriConferenceIQ.Content(); - - requestContent.setName(mediaType.toString()); - requestContent.addChannel(requestChannel); - - ColibriConferenceIQ conferenceRequest - = new ColibriConferenceIQ(); - - conferenceRequest.setID(colibri.getID()); - conferenceRequest.setTo(colibri.getFrom()); - conferenceRequest.setType(IQ.Type.SET); - conferenceRequest.addContent(requestContent); - - getProtocolProvider().getConnection().sendPacket( - conferenceRequest); - } - } - } - } - - /** - * Creates a CallPeerJabberImpl from calleeJID and sends - * them session-initiate IQ request. - * - * @param calleeJID the party that we would like to invite to this call. - * @param discoverInfo any discovery information that we have for the jid - * we are trying to reach and that we are passing in order to avoid having - * to ask for it again. - * @param sessionInitiateExtensions a collection of additional and optional - * PacketExtensions to be added to the session-initiate - * {@link JingleIQ} which is to init this CallJabberImpl - * @param supportedTransports the XML namespaces of the jingle transports - * to use. - * - * @return the newly created CallPeerJabberImpl corresponding to - * calleeJID. All following state change events will be - * delivered through this call peer. - * - * @throws OperationFailedException with the corresponding code if we fail - * to create the call. - */ - public CallPeerJabberImpl initiateSession( - String calleeJID, - DiscoverInfo discoverInfo, - Iterable sessionInitiateExtensions, - Collection supportedTransports) - throws OperationFailedException - { - // create the session-initiate IQ - CallPeerJabberImpl callPeer = new CallPeerJabberImpl(calleeJID, this); - - callPeer.setDiscoveryInfo(discoverInfo); - - addCallPeer(callPeer); - - callPeer.setState(CallPeerState.INITIATING_CALL); - - // If this was the first peer we added in this call, then the call is - // new and we need to notify everyone of its creation. - if (getCallPeerCount() == 1) - parentOpSet.fireCallEvent(CallEvent.CALL_INITIATED, this); - - CallPeerMediaHandlerJabberImpl mediaHandler - = callPeer.getMediaHandler(); - - //set the supported transports before the transport manager is created - mediaHandler.setSupportedTransports(supportedTransports); - - /* enable video if it is a video call */ - mediaHandler.setLocalVideoTransmissionEnabled(localVideoAllowed); - /* enable remote-control if it is a desktop sharing session */ - mediaHandler.setLocalInputEvtAware(getLocalInputEvtAware()); - - /* - * Set call state to connecting so that the user interface would start - * playing the tones. We do that here because we may be harvesting - * STUN/TURN addresses in initiateSession() which would take a while. - */ - callPeer.setState(CallPeerState.CONNECTING); - - // if initializing session fails, set peer to failed - boolean sessionInitiated = false; - - try - { - callPeer.initiateSession(sessionInitiateExtensions); - sessionInitiated = true; - } - finally - { - // if initialization throws an exception - if (!sessionInitiated) - callPeer.setState(CallPeerState.FAILED); - } - return callPeer; - } - - /** - * Updates the Jingle sessions for the CallPeers of this - * Call, to reflect the current state of the the video contents of - * this Call. Sends a content-modify, content-add - * or content-remove message to each of the current - * CallPeers. - * - * @throws OperationFailedException if a problem occurred during message - * generation or there was a network problem - */ - @Override - public void modifyVideoContent() - throws OperationFailedException - { - if (logger.isDebugEnabled()) - logger.debug("Updating video content for " + this); - - boolean change = false; - for (CallPeerJabberImpl peer : getCallPeerList()) - change |= peer.sendModifyVideoContent(); - - if (change) - fireCallChangeEvent( - CallChangeEvent.CALL_PARTICIPANTS_CHANGE, null, null); - } - - /** - * Notifies this instance that a specific ColibriConferenceIQ has - * been received. - * - * @param conferenceIQ the ColibriConferenceIQ which has been - * received - * @return true if the specified conferenceIQ was - * processed by this instance and no further processing is to be performed - * by other possible processors of ColibriConferenceIQs; otherwise, - * false. Because a ColibriConferenceIQ request sent from - * the Jitsi Videobridge server to the application as its client concerns a - * specific CallJabberImpl implementation, no further processing by - * other CallJabberImpl instances is necessary once the - * ColibriConferenceIQ is processed by the associated - * CallJabberImpl instance. - */ - boolean processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) - { - if (colibri == null) - { - /* - * This instance has not set up any conference using the Jitsi - * Videobridge server-side technology yet so it cannot be bothered - * with related requests. - */ - return false; - } - else if (conferenceIQ.getID().equals(colibri.getID())) - { - /* - * Remove the local Channels (from the specified conferenceIQ) i.e. - * the Channels on which the local peer/user is sending to the Jitsi - * Videobridge server because they concern this Call only and not - * its CallPeers. - */ - for (MediaType mediaType : MediaType.values()) - { - String contentName = mediaType.toString(); - ColibriConferenceIQ.Content content - = conferenceIQ.getContent(contentName); - - if (content != null) - { - ColibriConferenceIQ.Content thisContent - = colibri.getContent(contentName); - - if ((thisContent != null) - && (thisContent.getChannelCount() > 0)) - { - ColibriConferenceIQ.Channel thisChannel - = thisContent.getChannel(0); - ColibriConferenceIQ.Channel channel - = content.getChannel(thisChannel.getID()); - - if (channel != null) - content.removeChannel(channel); - } - } - } - - for (CallPeerJabberImpl callPeer : getCallPeerList()) - callPeer.processColibriConferenceIQ(conferenceIQ); - - /* - * We have removed the local Channels from the specified - * conferenceIQ. Consequently, it is no longer the same and fit for - * processing by other CallJabberImpl instances. - */ - return true; - } - else - { - /* - * This instance has set up a conference using the Jitsi Videobridge - * server-side technology but it is not the one referred to by the - * specified conferenceIQ i.e. the specified conferenceIQ does not - * concern this instance. - */ - return false; - } - } - - /** - * Creates a new call peer and sends a RINGING response. - * - * @param jingleIQ the {@link JingleIQ} that created the session. - * - * @return the newly created {@link CallPeerJabberImpl} (the one that sent - * the INVITE). - */ - public CallPeerJabberImpl processSessionInitiate(JingleIQ jingleIQ) - { - // Use the IQs 'from', instead of the jingle 'initiator' field, - // because we want to make sure that following IQs are sent with the - // correct 'to'. - String remoteParty = jingleIQ.getFrom(); - - boolean autoAnswer = false; - CallPeerJabberImpl attendant = null; - OperationSetBasicTelephonyJabberImpl basicTelephony = null; - - CallPeerJabberImpl callPeer - = new CallPeerJabberImpl(remoteParty, this, jingleIQ); - - addCallPeer(callPeer); - - /* - * We've already sent ack to the specified session-initiate so if it has - * been sent as part of an attended transfer, we have to hang up on the - * attendant. - */ - try - { - TransferPacketExtension transfer - = (TransferPacketExtension) - jingleIQ.getExtension( - TransferPacketExtension.ELEMENT_NAME, - TransferPacketExtension.NAMESPACE); - - if (transfer != null) - { - String sid = transfer.getSID(); - - if (sid != null) - { - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - basicTelephony - = (OperationSetBasicTelephonyJabberImpl) - protocolProvider.getOperationSet( - OperationSetBasicTelephony.class); - CallJabberImpl attendantCall - = basicTelephony - .getActiveCallsRepository() - .findSID(sid); - - if (attendantCall != null) - { - attendant = attendantCall.getPeer(sid); - if ((attendant != null) - && basicTelephony - .getFullCalleeURI(attendant.getAddress()) - .equals(transfer.getFrom()) - && protocolProvider.getOurJID().equals( - transfer.getTo())) - { - //basicTelephony.hangupCallPeer(attendant); - autoAnswer = true; - } - } - } - } - } - catch (Throwable t) - { - logger.error( - "Failed to hang up on attendant" - + " as part of session transfer", - t); - - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - } - - CoinPacketExtension coin - = (CoinPacketExtension) - jingleIQ.getExtension( - CoinPacketExtension.ELEMENT_NAME, - CoinPacketExtension.NAMESPACE); - - if (coin != null) - { - boolean b - = Boolean.parseBoolean( - (String) - coin.getAttribute( - CoinPacketExtension.ISFOCUS_ATTR_NAME)); - - callPeer.setConferenceFocus(b); - } - - //before notifying about this call, make sure that it looks alright - callPeer.processSessionInitiate(jingleIQ); - - // if paranoia is set, to accept the call we need to know that - // the other party has support for media encryption - if (getProtocolProvider().getAccountID().getAccountPropertyBoolean( - ProtocolProviderFactory.MODE_PARANOIA, false) - && callPeer.getMediaHandler().getAdvertisedEncryptionMethods() - .length - == 0) - { - //send an error response; - String reasonText - = JabberActivator.getResources().getI18NString( - "service.gui.security.encryption.required"); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - jingleIQ.getTo(), - jingleIQ.getFrom(), - jingleIQ.getSID(), - Reason.SECURITY_ERROR, - reasonText); - - callPeer.setState(CallPeerState.FAILED, reasonText); - getProtocolProvider().getConnection().sendPacket(errResp); - - return null; - } - - if (callPeer.getState() == CallPeerState.FAILED) - return null; - - callPeer.setState( CallPeerState.INCOMING_CALL ); - - // in case of attended transfer, auto answer the call - if (autoAnswer) - { - /* answer directly */ - try - { - callPeer.answer(); - } - catch(Exception e) - { - logger.info( - "Exception occurred while answer transferred call", - e); - callPeer = null; - } - - // hang up now - try - { - basicTelephony.hangupCallPeer(attendant); - } - catch(OperationFailedException e) - { - logger.error( - "Failed to hang up on attendant as part of session" - + " transfer", - e); - } - - return callPeer; - } - - /* see if offer contains audio and video so that we can propose - * option to the user (i.e. answer with video if it is a video call...) - */ - List offer - = callPeer.getSessionIQ().getContentList(); - Map directions - = new HashMap(); - - directions.put(MediaType.AUDIO, MediaDirection.INACTIVE); - directions.put(MediaType.VIDEO, MediaDirection.INACTIVE); - - for (ContentPacketExtension c : offer) - { - String contentName = c.getName(); - MediaDirection remoteDirection - = JingleUtils.getDirection(c, callPeer.isInitiator()); - - if (MediaType.AUDIO.toString().equals(contentName)) - directions.put(MediaType.AUDIO, remoteDirection); - else if (MediaType.VIDEO.toString().equals(contentName)) - directions.put(MediaType.VIDEO, remoteDirection); - } - - // If this was the first peer we added in this call, then the call is - // new and we need to notify everyone of its creation. - if (getCallPeerCount() == 1) - { - parentOpSet.fireCallEvent( - CallEvent.CALL_RECEIVED, - this, - directions); - } - - // Manages auto answer with "audio only", or "audio/video" answer. - OperationSetAutoAnswerJabberImpl autoAnswerOpSet - = (OperationSetAutoAnswerJabberImpl) - getProtocolProvider().getOperationSet( - OperationSetBasicAutoAnswer.class); - - if (autoAnswerOpSet != null) - autoAnswerOpSet.autoAnswer(this, directions); - - return callPeer; - } - - /** - * Updates the state of the local DTLS-SRTP endpoint (i.e. the local - * DtlsControl instance) from the state of the remote DTLS-SRTP - * endpoint represented by a specific ColibriConferenceIQ.Channel. - * - * @param peer the CallPeer associated with the method invocation - * @param channel the ColibriConferenceIQ.Channel which represents - * the state of the remote DTLS-SRTP endpoint - * @param mediaType the MediaType of the media to be transmitted - * over the DTLS-SRTP session - */ - private boolean addDtlsAdvertisedEncryptions( - CallPeerJabberImpl peer, - ColibriConferenceIQ.Channel channel, - MediaType mediaType) - { - CallPeerMediaHandlerJabberImpl peerMediaHandler - = peer.getMediaHandler(); - DtlsControl dtlsControl - = (DtlsControl) - peerMediaHandler.getSrtpControls().get( - mediaType, - SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - dtlsControl.setSetup( - peer.isInitiator() - ? DtlsControl.Setup.ACTIVE - : DtlsControl.Setup.PASSIVE); - } - - IceUdpTransportPacketExtension remoteTransport = channel.getTransport(); - - return - peerMediaHandler.addDtlsAdvertisedEncryptions( - true, - remoteTransport, - mediaType); - } - - /** - * Updates the state of the remote DTLS-SRTP endpoint represented by a - * specific ColibriConferenceIQ.Channel from the state of the local - * DTLS-SRTP endpoint. The specified channel is to be used by the - * conference focus for the purposes of transmitting media between a remote - * peer and the Jitsi Videobridge server. - * - * @param mediaType the MediaType of the media to be transmitted - * over the DTLS-SRTP session - * @param localContent the ContentPacketExtension of the local peer - * in the negotiation between the local and the remote peers. If - * remoteContent is null, represents an offer from the - * local peer to the remote peer; otherwise, represents an answer from the - * local peer to an offer from the remote peer. - * @param remoteContent the ContentPacketExtension, if any, of the - * remote peer in the negotiation between the local and the remote peers. If - * null, localContent represents an offer from the local - * peer to the remote peer; otherwise, localContent represents an - * answer from the local peer to an offer from the remote peer - * @param peer the CallPeer which represents the remote peer and - * which is associated with the specified channel - * @param channel the ColibriConferenceIQ.Channel which represents - * the state of the remote DTLS-SRTP endpoint. - */ - private void setDtlsEncryptionOnChannel( - MediaType mediaType, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent, - CallPeerJabberImpl peer, - ColibriConferenceIQ.Channel channel) - { - AccountID accountID = getProtocolProvider().getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME) - && (remoteContent != null)) - { - IceUdpTransportPacketExtension remoteTransport - = remoteContent.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - if (remoteTransport != null) - { - List remoteFingerprints - = remoteTransport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - - if (!remoteFingerprints.isEmpty()) - { - IceUdpTransportPacketExtension localTransport - = ensureTransportOnChannel(channel, peer); - - if (localTransport != null) - { - List localFingerprints - = localTransport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - - if (localFingerprints.isEmpty()) - { - for (DtlsFingerprintPacketExtension remoteFingerprint - : remoteFingerprints) - { - DtlsFingerprintPacketExtension localFingerprint - = new DtlsFingerprintPacketExtension(); - - localFingerprint.setFingerprint( - remoteFingerprint.getFingerprint()); - localFingerprint.setHash( - remoteFingerprint.getHash()); - localTransport.addChildExtension( - localFingerprint); - } - } - } - } - } - } - } - - /** - * Updates the state of the remote DTLS-SRTP endpoint represented by a - * specific ColibriConferenceIQ.Channel from the state of the local - * DTLS-SRTP endpoint (i.e. the local DtlsControl instance). The - * specified channel is to be used by the conference focus for the - * purposes of transmitting media between the local peer and the Jitsi - * Videobridge server. - * - * @param jitsiVideobridge the address/JID of the Jitsi Videobridge - * @param peer the CallPeer associated with the method invocation - * @param mediaType the MediaType of the media to be transmitted - * over the DTLS-SRTP session - * @param channel the ColibriConferenceIQ.Channel which represents - * the state of the remote DTLS-SRTP endpoint. - */ - private void setDtlsEncryptionOnChannel( - String jitsiVideobridge, - CallPeerJabberImpl peer, - MediaType mediaType, - ColibriConferenceIQ.Channel channel) - { - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - AccountID accountID = protocolProvider.getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME) - && protocolProvider.isFeatureSupported( - jitsiVideobridge, - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_DTLS_SRTP)) - { - CallPeerMediaHandlerJabberImpl mediaHandler - = peer.getMediaHandler(); - DtlsControl dtlsControl - = (DtlsControl) - mediaHandler.getSrtpControls().getOrCreate( - mediaType, - SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - IceUdpTransportPacketExtension transport - = ensureTransportOnChannel(channel, peer); - - if (transport != null) - setDtlsEncryptionOnTransport(dtlsControl, transport); - } - } - } - - /** - * Sets the properties (i.e. fingerprint and hash function) of a specific - * DtlsControl on the specific - * IceUdpTransportPacketExtension. - * - * @param dtlsControl the DtlsControl the properties of which are - * to be set on the specified localTransport - * @param localTransport the IceUdpTransportPacketExtension on - * which the properties of the specified dtlsControl are to be set - */ - static void setDtlsEncryptionOnTransport( - DtlsControl dtlsControl, - IceUdpTransportPacketExtension localTransport) - { - String fingerprint = dtlsControl.getLocalFingerprint(); - String hash = dtlsControl.getLocalFingerprintHashFunction(); - - DtlsFingerprintPacketExtension fingerprintPE - = localTransport.getFirstChildOfType( - DtlsFingerprintPacketExtension.class); - - if (fingerprintPE == null) - { - fingerprintPE = new DtlsFingerprintPacketExtension(); - localTransport.addChildExtension(fingerprintPE); - } - fingerprintPE.setFingerprint(fingerprint); - fingerprintPE.setHash(hash); - } - - private void setTransportOnChannel( - CallPeerJabberImpl peer, - String media, - ColibriConferenceIQ.Channel channel) - throws OperationFailedException - { - PacketExtension transport - = peer.getMediaHandler().getTransportManager().createTransport( - media); - - if (transport instanceof IceUdpTransportPacketExtension) - channel.setTransport((IceUdpTransportPacketExtension) transport); - } - - private void setTransportOnChannel( - String media, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent, - CallPeerJabberImpl peer, - ColibriConferenceIQ.Channel channel) - throws OperationFailedException - { - if (remoteContent != null) - { - IceUdpTransportPacketExtension transport - = remoteContent.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - channel.setTransport( - TransportManagerJabberImpl.cloneTransportAndCandidates( - transport)); - } - } - - /** - * Makes an attempt to ensure that a specific - * ColibriConferenceIQ.Channel has a non-null - * transport set. If the specified channel does not have - * a transport, the method invokes the TransportManager of - * the specified CallPeerJabberImpl to initialize a new - * PacketExtension. - * - * @param channel the ColibriConferenceIQ.Channel to ensure the - * transport on - * @param peer the CallPeerJabberImpl which is associated with the - * specified channel and which specifies the - * TransportManager to be described in the specified - * channel - * @return the transport of the specified channel - */ - private IceUdpTransportPacketExtension ensureTransportOnChannel( - ColibriConferenceIQ.Channel channel, - CallPeerJabberImpl peer) - { - IceUdpTransportPacketExtension transport - = channel.getTransport(); - - if (transport == null) - { - PacketExtension pe - = peer - .getMediaHandler() - .getTransportManager() - .createTransportPacketExtension(); - - if (pe instanceof IceUdpTransportPacketExtension) - { - transport = (IceUdpTransportPacketExtension) pe; - channel.setTransport(transport); - } - } - return transport; - } - - /** - * Gets the entity ID of the Jitsi Videobridge to be utilized by this - * Call for the purposes of establishing a server-assisted - * telephony conference. - * - * @return the entity ID of the Jitsi Videobridge to be utilized by this - * Call for the purposes of establishing a server-assisted - * telephony conference. - */ - public String getJitsiVideobridge() - { - if ((this.jitsiVideobridge == null) - && getConference().isJitsiVideobridge()) - { - String jitsiVideobridge - = getProtocolProvider().getJitsiVideobridge(); - - if (jitsiVideobridge != null) - this.jitsiVideobridge = jitsiVideobridge; - } - return this.jitsiVideobridge; - } -} +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.jabber; + +import java.lang.ref.*; +import java.util.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.neomedia.*; +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.*; + +/** + * A Jabber implementation of the Call abstract class encapsulating + * Jabber jingle sessions. + * + * @author Emil Ivov + * @author Lyubomir Marinov + * @author Boris Grozev + */ +public class CallJabberImpl + extends AbstractCallJabberGTalkImpl +{ + /** + * The Logger used by the CallJabberImpl class and its + * instances for logging output. + */ + private static final Logger logger = Logger.getLogger(CallJabberImpl.class); + + /** + * The Jitsi Videobridge conference which the local peer represented by this + * instance is a focus of. + */ + private ColibriConferenceIQ colibri; + + /** + * The shared CallPeerMediaHandler state which is to be used by the + * CallPeers of this Call which use {@link #colibri}. + */ + private MediaHandler colibriMediaHandler; + + /** + * Contains one ColibriStreamConnector for each MediaType + */ + private final List> + colibriStreamConnectors; + + /** + * The entity ID of the Jitsi Videobridge to be utilized by this + * Call for the purposes of establishing a server-assisted + * telephony conference. + */ + private String jitsiVideobridge; + + /** + * Initializes a new CallJabberImpl instance. + * + * @param parentOpSet the {@link OperationSetBasicTelephonyJabberImpl} + * instance in the context of which this call has been created. + */ + protected CallJabberImpl( + OperationSetBasicTelephonyJabberImpl parentOpSet) + { + super(parentOpSet); + + int mediaTypeValueCount = MediaType.values().length; + + colibriStreamConnectors + = new ArrayList>( + mediaTypeValueCount); + for (int i = 0; i < mediaTypeValueCount; i++) + colibriStreamConnectors.add(null); + + //let's add ourselves to the calls repo. we are doing it ourselves just + //to make sure that no one ever forgets. + parentOpSet.getActiveCallsRepository().addCall(this); + } + + /** + * Closes a specific ColibriStreamConnector which is associated with + * a MediaStream of a specific MediaType upon request from + * a specific CallPeer. + * + * @param peer the CallPeer which requests the closing of the + * specified colibriStreamConnector + * @param mediaType the MediaType of the MediaStream with + * which the specified colibriStreamConnector is associated + * @param colibriStreamConnector the ColibriStreamConnector to close on + * behalf of the specified peer + */ + public void closeColibriStreamConnector( + CallPeerJabberImpl peer, + MediaType mediaType, + ColibriStreamConnector colibriStreamConnector) + { + colibriStreamConnector.close(); + synchronized (colibriStreamConnectors) + { + int index = mediaType.ordinal(); + WeakReference weakReference + = colibriStreamConnectors.get(index); + if (weakReference != null && colibriStreamConnector + .equals(weakReference.get())) + { + colibriStreamConnectors.set(index, null); + } + } + } + + /** + * {@inheritDoc} + * + * Sends a content message to each of the CallPeers + * associated with this CallJabberImpl in order to include/exclude + * the "isfocus" attribute. + */ + @Override + protected void conferenceFocusChanged(boolean oldValue, boolean newValue) + { + try + { + Iterator peers = getCallPeers(); + + while (peers.hasNext()) + { + CallPeerJabberImpl callPeer = peers.next(); + + if (callPeer.getState() == CallPeerState.CONNECTED) + callPeer.sendCoinSessionInfo(); + } + } + finally + { + super.conferenceFocusChanged(oldValue, newValue); + } + } + + /** + * Allocates colibri (conference) channels for a specific MediaType + * to be used by a specific CallPeer. + * + * @param peer the CallPeer which is to use the allocated colibri + * (conference) channels + * @param contentMap the local and remote ContentPacketExtensions + * which specify the MediaTypes for which colibri (conference) + * channels are to be allocated + * @return a ColibriConferenceIQ which describes the allocated + * colibri (conference) channels for the specified mediaTypes which + * are to be used by the specified peer; otherwise, null + */ + public ColibriConferenceIQ createColibriChannels( + CallPeerJabberImpl peer, + Map contentMap) + throws OperationFailedException + { + if (!getConference().isJitsiVideobridge()) + return null; + + /* + * For a colibri conference to work properly, all CallPeers in the + * conference must share one and the same CallPeerMediaHandler state + * i.e. they must use a single set of MediaStreams as if there was a + * single CallPeerMediaHandler. + */ + CallPeerMediaHandlerJabberImpl peerMediaHandler + = peer.getMediaHandler(); + + if (peerMediaHandler.getMediaHandler() != colibriMediaHandler) + { + for (MediaType mediaType : MediaType.values()) + { + if (peerMediaHandler.getStream(mediaType) != null) + return null; + } + } + + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + String jitsiVideobridge + = (colibri == null) ? getJitsiVideobridge() : colibri.getFrom(); + + if ((jitsiVideobridge == null) || (jitsiVideobridge.length() == 0)) + { + logger.error( + "Failed to allocate colibri channels: no videobridge" + + " found."); + return null; + } + + /* + * The specified CallPeer will participate in the colibri conference + * organized by this Call so it must use the shared CallPeerMediaHandler + * state of all CallPeers in the same colibri conference. + */ + if (colibriMediaHandler == null) + colibriMediaHandler = new MediaHandler(); + peerMediaHandler.setMediaHandler(colibriMediaHandler); + + ColibriConferenceIQ conferenceRequest = new ColibriConferenceIQ(); + + if (colibri != null) + conferenceRequest.setID(colibri.getID()); + + for (Map.Entry e + : contentMap.entrySet()) + { + ContentPacketExtension localContent = e.getKey(); + ContentPacketExtension remoteContent = e.getValue(); + ContentPacketExtension cpe + = (remoteContent == null) ? localContent : remoteContent; + RtpDescriptionPacketExtension rdpe + = cpe.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + String media = rdpe.getMedia(); + MediaType mediaType = MediaType.parseString(media); + String contentName = mediaType.toString(); + ColibriConferenceIQ.Content contentRequest + = new ColibriConferenceIQ.Content(contentName); + + conferenceRequest.addContent(contentRequest); + + boolean requestLocalChannel = true; + + if (colibri != null) + { + ColibriConferenceIQ.Content content + = colibri.getContent(contentName); + + if ((content != null) && (content.getChannelCount() > 0)) + requestLocalChannel = false; + } + + boolean peerIsInitiator = peer.isInitiator(); + + if (requestLocalChannel) + { + ColibriConferenceIQ.Channel localChannelRequest + = new ColibriConferenceIQ.Channel(); + + localChannelRequest.setEndpoint(protocolProvider.getOurJID()); + localChannelRequest.setInitiator(peerIsInitiator); + + for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) + localChannelRequest.addPayloadType(ptpe); + setTransportOnChannel(peer, media, localChannelRequest); + // DTLS-SRTP + setDtlsEncryptionOnChannel( + jitsiVideobridge, + peer, + mediaType, + localChannelRequest); + /* + * Since Jitsi Videobridge supports multiple Jingle transports, + * it is a good idea to indicate which one is expected on a + * channel. + */ + ensureTransportOnChannel(localChannelRequest, peer); + contentRequest.addChannel(localChannelRequest); + } + + ColibriConferenceIQ.Channel remoteChannelRequest + = new ColibriConferenceIQ.Channel(); + + remoteChannelRequest.setEndpoint(peer.getAddress()); + remoteChannelRequest.setInitiator(!peerIsInitiator); + + for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) + remoteChannelRequest.addPayloadType(ptpe); + setTransportOnChannel( + media, + localContent, + remoteContent, + peer, + remoteChannelRequest); + // DTLS-SRTP + setDtlsEncryptionOnChannel( + mediaType, + localContent, + remoteContent, + peer, + remoteChannelRequest); + /* + * Since Jitsi Videobridge supports multiple Jingle transports, it + * is a good idea to indicate which one is expected on a channel. + */ + ensureTransportOnChannel(remoteChannelRequest, peer); + contentRequest.addChannel(remoteChannelRequest); + } + + XMPPConnection connection = protocolProvider.getConnection(); + PacketCollector packetCollector + = connection.createPacketCollector( + new PacketIDFilter(conferenceRequest.getPacketID())); + + conferenceRequest.setTo(jitsiVideobridge); + conferenceRequest.setType(IQ.Type.GET); + connection.sendPacket(conferenceRequest); + + Packet response + = packetCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + + packetCollector.cancel(); + + if (response == null) + { + logger.error( + "Failed to allocate colibri channels: response is null." + + " Maybe the response timed out."); + return null; + } + else if (response.getError() != null) + { + logger.error( + "Failed to allocate colibri channels: " + + response.getError()); + return null; + } + else if (!(response instanceof ColibriConferenceIQ)) + { + logger.error( + "Failed to allocate colibri channels: response is not a" + + " colibri conference"); + return null; + } + + ColibriConferenceIQ conferenceResponse = (ColibriConferenceIQ) response; + String conferenceResponseID = conferenceResponse.getID(); + + /* + * Update the complete ColibriConferenceIQ representation maintained by + * this instance with the information given by the (current) response. + */ + { + if (colibri == null) + { + colibri = new ColibriConferenceIQ(); + /* + * XXX We must remember the JID of the Jitsi Videobridge because + * (1) we do not want to re-discover it in every method + * invocation on this Call instance and (2) we want to use one + * and the same for all CallPeers within this Call instance. + */ + colibri.setFrom(conferenceResponse.getFrom()); + } + + String colibriID = colibri.getID(); + + if (colibriID == null) + colibri.setID(conferenceResponseID); + else if (!colibriID.equals(conferenceResponseID)) + throw new IllegalStateException("conference.id"); + + for (ColibriConferenceIQ.Content contentResponse + : conferenceResponse.getContents()) + { + String contentName = contentResponse.getName(); + ColibriConferenceIQ.Content content + = colibri.getOrCreateContent(contentName); + + for (ColibriConferenceIQ.Channel channelResponse + : contentResponse.getChannels()) + { + int channelIndex = content.getChannelCount(); + + content.addChannel(channelResponse); + if (channelIndex == 0) + { + TransportManagerJabberImpl transportManager + = peerMediaHandler.getTransportManager(); + + transportManager + .isEstablishingConnectivityWithJitsiVideobridge + = true; + transportManager + .startConnectivityEstablishmentWithJitsiVideobridge + = true; + + MediaType mediaType + = MediaType.parseString(contentName); + + // DTLS-SRTP + addDtlsAdvertisedEncryptions( + peer, + channelResponse, + mediaType); + } + } + } + } + + /* + * Formulate the result to be returned to the caller which is a subset + * of the whole conference information kept by this CallJabberImpl and + * includes the remote channels explicitly requested by the method + * caller and their respective local channels. + */ + ColibriConferenceIQ conferenceResult = new ColibriConferenceIQ(); + + conferenceResult.setFrom(colibri.getFrom()); + conferenceResult.setID(conferenceResponseID); + + for (Map.Entry e + : contentMap.entrySet()) + { + ContentPacketExtension localContent = e.getKey(); + ContentPacketExtension remoteContent = e.getValue(); + ContentPacketExtension cpe + = (remoteContent == null) ? localContent : remoteContent; + RtpDescriptionPacketExtension rdpe + = cpe.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + MediaType mediaType = MediaType.parseString(rdpe.getMedia()); + ColibriConferenceIQ.Content contentResponse + = conferenceResponse.getContent(mediaType.toString()); + + if (contentResponse != null) + { + String contentName = contentResponse.getName(); + ColibriConferenceIQ.Content contentResult + = new ColibriConferenceIQ.Content(contentName); + + conferenceResult.addContent(contentResult); + + /* + * The local channel may have been allocated in a previous + * method call as part of the allocation of the first remote + * channel in the respective content. Anyway, the current method + * caller still needs to know about it. + */ + ColibriConferenceIQ.Content content + = colibri.getContent(contentName); + ColibriConferenceIQ.Channel localChannel = null; + + if ((content != null) && (content.getChannelCount() > 0)) + { + localChannel = content.getChannel(0); + contentResult.addChannel(localChannel); + } + + String localChannelID + = (localChannel == null) ? null : localChannel.getID(); + + for (ColibriConferenceIQ.Channel channelResponse + : contentResponse.getChannels()) + { + if ((localChannelID == null) + || !localChannelID.equals(channelResponse.getID())) + contentResult.addChannel(channelResponse); + } + } + } + + return conferenceResult; + } + + /** + * Initializes a ColibriStreamConnector on behalf of a specific + * CallPeer to be used in association with a specific + * ColibriConferenceIQ.Channel of a specific MediaType. + * + * @param peer the CallPeer which requests the initialization of a + * ColibriStreamConnector + * @param mediaType the MediaType of the stream which is to use the + * initialized ColibriStreamConnector for RTP and RTCP traffic + * @param channel the ColibriConferenceIQ.Channel to which RTP and + * RTCP traffic is to be sent and from which such traffic is to be received + * via the initialized ColibriStreamConnector + * @param factory a StreamConnectorFactory implementation which is + * to allocate the sockets to be used for RTP and RTCP traffic + * @return a ColibriStreamConnector to be used for RTP and RTCP + * traffic associated with the specified channel + */ + public ColibriStreamConnector createColibriStreamConnector( + CallPeerJabberImpl peer, + MediaType mediaType, + ColibriConferenceIQ.Channel channel, + StreamConnectorFactory factory) + { + String channelID = channel.getID(); + + if (channelID == null) + throw new IllegalArgumentException("channel"); + + if (colibri == null) + throw new IllegalStateException("colibri"); + + ColibriConferenceIQ.Content content + = colibri.getContent(mediaType.toString()); + + if (content == null) + throw new IllegalArgumentException("mediaType"); + if ((content.getChannelCount() < 1) + || !channelID.equals((channel = content.getChannel(0)).getID())) + throw new IllegalArgumentException("channel"); + + ColibriStreamConnector colibriStreamConnector; + + synchronized (colibriStreamConnectors) + { + int index = mediaType.ordinal(); + WeakReference weakReference + = colibriStreamConnectors.get(index); + + colibriStreamConnector + = (weakReference == null) ? null : weakReference.get(); + if (colibriStreamConnector == null) + { + StreamConnector streamConnector + = factory.createStreamConnector(); + + if (streamConnector != null) + { + colibriStreamConnector + = new ColibriStreamConnector(streamConnector); + colibriStreamConnectors.set( + index, + new WeakReference( + colibriStreamConnector)); + } + } + } + + return colibriStreamConnector; + } + + /** + * Expires specific (colibri) conference channels used by a specific + * CallPeer. + * + * @param peer the CallPeer which uses the specified (colibri) + * conference channels to be expired + * @param conference a ColibriConferenceIQ which specifies the + * (colibri) conference channels to be expired + */ + public void expireColibriChannels( + CallPeerJabberImpl peer, + ColibriConferenceIQ conference) + { + // Formulate the ColibriConferenceIQ request which is to be sent. + if (colibri != null) + { + String conferenceID = colibri.getID(); + + if (conferenceID.equals(conference.getID())) + { + ColibriConferenceIQ conferenceRequest + = new ColibriConferenceIQ(); + + conferenceRequest.setID(conferenceID); + + for (ColibriConferenceIQ.Content content + : conference.getContents()) + { + ColibriConferenceIQ.Content colibriContent + = colibri.getContent(content.getName()); + + if (colibriContent != null) + { + ColibriConferenceIQ.Content contentRequest + = conferenceRequest.getOrCreateContent( + colibriContent.getName()); + + for (ColibriConferenceIQ.Channel channel + : content.getChannels()) + { + ColibriConferenceIQ.Channel colibriChannel + = colibriContent.getChannel(channel.getID()); + + if (colibriChannel != null) + { + ColibriConferenceIQ.Channel channelRequest + = new ColibriConferenceIQ.Channel(); + + channelRequest.setExpire(0); + channelRequest.setID(colibriChannel.getID()); + contentRequest.addChannel(channelRequest); + } + } + } + } + + /* + * Remove the channels which are to be expired from the internal + * state of the conference managed by this CallJabberImpl. + */ + for (ColibriConferenceIQ.Content contentRequest + : conferenceRequest.getContents()) + { + ColibriConferenceIQ.Content colibriContent + = colibri.getContent(contentRequest.getName()); + + for (ColibriConferenceIQ.Channel channelRequest + : contentRequest.getChannels()) + { + ColibriConferenceIQ.Channel colibriChannel + = colibriContent.getChannel(channelRequest.getID()); + + colibriContent.removeChannel(colibriChannel); + + /* + * If the last remote channel is to be expired, expire + * the local channel as well. + */ + if (colibriContent.getChannelCount() == 1) + { + colibriChannel = colibriContent.getChannel(0); + + channelRequest = new ColibriConferenceIQ.Channel(); + channelRequest.setExpire(0); + channelRequest.setID(colibriChannel.getID()); + contentRequest.addChannel(channelRequest); + + colibriContent.removeChannel(colibriChannel); + + break; + } + } + } + + /* + * At long last, send the ColibriConferenceIQ request to expire + * the channels. + */ + conferenceRequest.setTo(colibri.getFrom()); + conferenceRequest.setType(IQ.Type.SET); + getProtocolProvider().getConnection().sendPacket( + conferenceRequest); + } + } + } + + /** + * Sends a ColibriConferenceIQ to the videobridge used by this + * CallJabberImpl, in order to request the the direction of + * the channel with ID channelID be set to + * direction + * @param channelID the ID of the channel for which to set the + * direction. + * @param mediaType the MediaType of the channel (we can deduce this + * by searching the ColibriConferenceIQ, but it's more convenient + * to have it) + * @param direction the MediaDirection to set. + */ + public void setChannelDirection(String channelID, + MediaType mediaType, + MediaDirection direction) + { + if ((colibri != null) && (channelID != null)) + { + ColibriConferenceIQ.Content content + = colibri.getContent(mediaType.toString()); + + if (content != null) + { + ColibriConferenceIQ.Channel channel + = content.getChannel(channelID); + + /* + * Note that we send requests even when the local Channel's + * direction and the direction we are setting are the same. We + * can easily avoid this, but we risk not sending necessary + * packets if local Channel and the actual channel on the + * videobridge are out of sync. + */ + if (channel != null) + { + ColibriConferenceIQ.Channel requestChannel + = new ColibriConferenceIQ.Channel(); + + requestChannel.setID(channelID); + requestChannel.setDirection(direction); + + ColibriConferenceIQ.Content requestContent + = new ColibriConferenceIQ.Content(); + + requestContent.setName(mediaType.toString()); + requestContent.addChannel(requestChannel); + + ColibriConferenceIQ conferenceRequest + = new ColibriConferenceIQ(); + + conferenceRequest.setID(colibri.getID()); + conferenceRequest.setTo(colibri.getFrom()); + conferenceRequest.setType(IQ.Type.SET); + conferenceRequest.addContent(requestContent); + + getProtocolProvider().getConnection().sendPacket( + conferenceRequest); + } + } + } + } + + /** + * Creates a CallPeerJabberImpl from calleeJID and sends + * them session-initiate IQ request. + * + * @param calleeJID the party that we would like to invite to this call. + * @param discoverInfo any discovery information that we have for the jid + * we are trying to reach and that we are passing in order to avoid having + * to ask for it again. + * @param sessionInitiateExtensions a collection of additional and optional + * PacketExtensions to be added to the session-initiate + * {@link JingleIQ} which is to init this CallJabberImpl + * @param supportedTransports the XML namespaces of the jingle transports + * to use. + * + * @return the newly created CallPeerJabberImpl corresponding to + * calleeJID. All following state change events will be + * delivered through this call peer. + * + * @throws OperationFailedException with the corresponding code if we fail + * to create the call. + */ + public CallPeerJabberImpl initiateSession( + String calleeJID, + DiscoverInfo discoverInfo, + Iterable sessionInitiateExtensions, + Collection supportedTransports) + throws OperationFailedException + { + // create the session-initiate IQ + CallPeerJabberImpl callPeer = new CallPeerJabberImpl(calleeJID, this); + + callPeer.setDiscoveryInfo(discoverInfo); + + addCallPeer(callPeer); + + callPeer.setState(CallPeerState.INITIATING_CALL); + + // If this was the first peer we added in this call, then the call is + // new and we need to notify everyone of its creation. + if (getCallPeerCount() == 1) + parentOpSet.fireCallEvent(CallEvent.CALL_INITIATED, this); + + CallPeerMediaHandlerJabberImpl mediaHandler + = callPeer.getMediaHandler(); + + //set the supported transports before the transport manager is created + mediaHandler.setSupportedTransports(supportedTransports); + + /* enable video if it is a video call */ + mediaHandler.setLocalVideoTransmissionEnabled(localVideoAllowed); + /* enable remote-control if it is a desktop sharing session */ + mediaHandler.setLocalInputEvtAware(getLocalInputEvtAware()); + + /* + * Set call state to connecting so that the user interface would start + * playing the tones. We do that here because we may be harvesting + * STUN/TURN addresses in initiateSession() which would take a while. + */ + callPeer.setState(CallPeerState.CONNECTING); + + // if initializing session fails, set peer to failed + boolean sessionInitiated = false; + + try + { + callPeer.initiateSession(sessionInitiateExtensions); + sessionInitiated = true; + } + finally + { + // if initialization throws an exception + if (!sessionInitiated) + callPeer.setState(CallPeerState.FAILED); + } + return callPeer; + } + + /** + * Updates the Jingle sessions for the CallPeers of this + * Call, to reflect the current state of the the video contents of + * this Call. Sends a content-modify, content-add + * or content-remove message to each of the current + * CallPeers. + * + * @throws OperationFailedException if a problem occurred during message + * generation or there was a network problem + */ + @Override + public void modifyVideoContent() + throws OperationFailedException + { + if (logger.isDebugEnabled()) + logger.debug("Updating video content for " + this); + + boolean change = false; + for (CallPeerJabberImpl peer : getCallPeerList()) + change |= peer.sendModifyVideoContent(); + + if (change) + fireCallChangeEvent( + CallChangeEvent.CALL_PARTICIPANTS_CHANGE, null, null); + } + + /** + * Notifies this instance that a specific ColibriConferenceIQ has + * been received. + * + * @param conferenceIQ the ColibriConferenceIQ which has been + * received + * @return true if the specified conferenceIQ was + * processed by this instance and no further processing is to be performed + * by other possible processors of ColibriConferenceIQs; otherwise, + * false. Because a ColibriConferenceIQ request sent from + * the Jitsi Videobridge server to the application as its client concerns a + * specific CallJabberImpl implementation, no further processing by + * other CallJabberImpl instances is necessary once the + * ColibriConferenceIQ is processed by the associated + * CallJabberImpl instance. + */ + boolean processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) + { + if (colibri == null) + { + /* + * This instance has not set up any conference using the Jitsi + * Videobridge server-side technology yet so it cannot be bothered + * with related requests. + */ + return false; + } + else if (conferenceIQ.getID().equals(colibri.getID())) + { + /* + * Remove the local Channels (from the specified conferenceIQ) i.e. + * the Channels on which the local peer/user is sending to the Jitsi + * Videobridge server because they concern this Call only and not + * its CallPeers. + */ + for (MediaType mediaType : MediaType.values()) + { + String contentName = mediaType.toString(); + ColibriConferenceIQ.Content content + = conferenceIQ.getContent(contentName); + + if (content != null) + { + ColibriConferenceIQ.Content thisContent + = colibri.getContent(contentName); + + if ((thisContent != null) + && (thisContent.getChannelCount() > 0)) + { + ColibriConferenceIQ.Channel thisChannel + = thisContent.getChannel(0); + ColibriConferenceIQ.Channel channel + = content.getChannel(thisChannel.getID()); + + if (channel != null) + content.removeChannel(channel); + } + } + } + + for (CallPeerJabberImpl callPeer : getCallPeerList()) + callPeer.processColibriConferenceIQ(conferenceIQ); + + /* + * We have removed the local Channels from the specified + * conferenceIQ. Consequently, it is no longer the same and fit for + * processing by other CallJabberImpl instances. + */ + return true; + } + else + { + /* + * This instance has set up a conference using the Jitsi Videobridge + * server-side technology but it is not the one referred to by the + * specified conferenceIQ i.e. the specified conferenceIQ does not + * concern this instance. + */ + return false; + } + } + + /** + * Creates a new call peer and sends a RINGING response. + * + * @param jingleIQ the {@link JingleIQ} that created the session. + * + * @return the newly created {@link CallPeerJabberImpl} (the one that sent + * the INVITE). + */ + public CallPeerJabberImpl processSessionInitiate(JingleIQ jingleIQ) + { + // Use the IQs 'from', instead of the jingle 'initiator' field, + // because we want to make sure that following IQs are sent with the + // correct 'to'. + String remoteParty = jingleIQ.getFrom(); + + boolean autoAnswer = false; + CallPeerJabberImpl attendant = null; + OperationSetBasicTelephonyJabberImpl basicTelephony = null; + + CallPeerJabberImpl callPeer + = new CallPeerJabberImpl(remoteParty, this, jingleIQ); + + addCallPeer(callPeer); + + /* + * We've already sent ack to the specified session-initiate so if it has + * been sent as part of an attended transfer, we have to hang up on the + * attendant. + */ + try + { + TransferPacketExtension transfer + = (TransferPacketExtension) + jingleIQ.getExtension( + TransferPacketExtension.ELEMENT_NAME, + TransferPacketExtension.NAMESPACE); + + if (transfer != null) + { + String sid = transfer.getSID(); + + if (sid != null) + { + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + basicTelephony + = (OperationSetBasicTelephonyJabberImpl) + protocolProvider.getOperationSet( + OperationSetBasicTelephony.class); + CallJabberImpl attendantCall + = basicTelephony + .getActiveCallsRepository() + .findSID(sid); + + if (attendantCall != null) + { + attendant = attendantCall.getPeer(sid); + if ((attendant != null) + && basicTelephony + .getFullCalleeURI(attendant.getAddress()) + .equals(transfer.getFrom()) + && protocolProvider.getOurJID().equals( + transfer.getTo())) + { + //basicTelephony.hangupCallPeer(attendant); + autoAnswer = true; + } + } + } + } + } + catch (Throwable t) + { + logger.error( + "Failed to hang up on attendant" + + " as part of session transfer", + t); + + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + } + + CoinPacketExtension coin + = (CoinPacketExtension) + jingleIQ.getExtension( + CoinPacketExtension.ELEMENT_NAME, + CoinPacketExtension.NAMESPACE); + + if (coin != null) + { + boolean b + = Boolean.parseBoolean( + (String) + coin.getAttribute( + CoinPacketExtension.ISFOCUS_ATTR_NAME)); + + callPeer.setConferenceFocus(b); + } + + //before notifying about this call, make sure that it looks alright + callPeer.processSessionInitiate(jingleIQ); + + // if paranoia is set, to accept the call we need to know that + // the other party has support for media encryption + if (getProtocolProvider().getAccountID().getAccountPropertyBoolean( + ProtocolProviderFactory.MODE_PARANOIA, false) + && callPeer.getMediaHandler().getAdvertisedEncryptionMethods() + .length + == 0) + { + //send an error response; + String reasonText + = JabberActivator.getResources().getI18NString( + "service.gui.security.encryption.required"); + JingleIQ errResp + = JinglePacketFactory.createSessionTerminate( + jingleIQ.getTo(), + jingleIQ.getFrom(), + jingleIQ.getSID(), + Reason.SECURITY_ERROR, + reasonText); + + callPeer.setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + + return null; + } + + if (callPeer.getState() == CallPeerState.FAILED) + return null; + + callPeer.setState( CallPeerState.INCOMING_CALL ); + + // in case of attended transfer, auto answer the call + if (autoAnswer) + { + /* answer directly */ + try + { + callPeer.answer(); + } + catch(Exception e) + { + logger.info( + "Exception occurred while answer transferred call", + e); + callPeer = null; + } + + // hang up now + try + { + basicTelephony.hangupCallPeer(attendant); + } + catch(OperationFailedException e) + { + logger.error( + "Failed to hang up on attendant as part of session" + + " transfer", + e); + } + + return callPeer; + } + + /* see if offer contains audio and video so that we can propose + * option to the user (i.e. answer with video if it is a video call...) + */ + List offer + = callPeer.getSessionIQ().getContentList(); + Map directions + = new HashMap(); + + directions.put(MediaType.AUDIO, MediaDirection.INACTIVE); + directions.put(MediaType.VIDEO, MediaDirection.INACTIVE); + + for (ContentPacketExtension c : offer) + { + String contentName = c.getName(); + MediaDirection remoteDirection + = JingleUtils.getDirection(c, callPeer.isInitiator()); + + if (MediaType.AUDIO.toString().equals(contentName)) + directions.put(MediaType.AUDIO, remoteDirection); + else if (MediaType.VIDEO.toString().equals(contentName)) + directions.put(MediaType.VIDEO, remoteDirection); + } + + // If this was the first peer we added in this call, then the call is + // new and we need to notify everyone of its creation. + if (getCallPeerCount() == 1) + { + parentOpSet.fireCallEvent( + CallEvent.CALL_RECEIVED, + this, + directions); + } + + // Manages auto answer with "audio only", or "audio/video" answer. + OperationSetAutoAnswerJabberImpl autoAnswerOpSet + = (OperationSetAutoAnswerJabberImpl) + getProtocolProvider().getOperationSet( + OperationSetBasicAutoAnswer.class); + + if (autoAnswerOpSet != null) + autoAnswerOpSet.autoAnswer(this, directions); + + return callPeer; + } + + /** + * Updates the state of the local DTLS-SRTP endpoint (i.e. the local + * DtlsControl instance) from the state of the remote DTLS-SRTP + * endpoint represented by a specific ColibriConferenceIQ.Channel. + * + * @param peer the CallPeer associated with the method invocation + * @param channel the ColibriConferenceIQ.Channel which represents + * the state of the remote DTLS-SRTP endpoint + * @param mediaType the MediaType of the media to be transmitted + * over the DTLS-SRTP session + */ + private boolean addDtlsAdvertisedEncryptions( + CallPeerJabberImpl peer, + ColibriConferenceIQ.Channel channel, + MediaType mediaType) + { + CallPeerMediaHandlerJabberImpl peerMediaHandler + = peer.getMediaHandler(); + DtlsControl dtlsControl + = (DtlsControl) + peerMediaHandler.getSrtpControls().get( + mediaType, + SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + dtlsControl.setSetup( + peer.isInitiator() + ? DtlsControl.Setup.ACTIVE + : DtlsControl.Setup.PASSIVE); + } + + IceUdpTransportPacketExtension remoteTransport = channel.getTransport(); + + return + peerMediaHandler.addDtlsAdvertisedEncryptions( + true, + remoteTransport, + mediaType); + } + + /** + * Updates the state of the remote DTLS-SRTP endpoint represented by a + * specific ColibriConferenceIQ.Channel from the state of the local + * DTLS-SRTP endpoint. The specified channel is to be used by the + * conference focus for the purposes of transmitting media between a remote + * peer and the Jitsi Videobridge server. + * + * @param mediaType the MediaType of the media to be transmitted + * over the DTLS-SRTP session + * @param localContent the ContentPacketExtension of the local peer + * in the negotiation between the local and the remote peers. If + * remoteContent is null, represents an offer from the + * local peer to the remote peer; otherwise, represents an answer from the + * local peer to an offer from the remote peer. + * @param remoteContent the ContentPacketExtension, if any, of the + * remote peer in the negotiation between the local and the remote peers. If + * null, localContent represents an offer from the local + * peer to the remote peer; otherwise, localContent represents an + * answer from the local peer to an offer from the remote peer + * @param peer the CallPeer which represents the remote peer and + * which is associated with the specified channel + * @param channel the ColibriConferenceIQ.Channel which represents + * the state of the remote DTLS-SRTP endpoint. + */ + private void setDtlsEncryptionOnChannel( + MediaType mediaType, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent, + CallPeerJabberImpl peer, + ColibriConferenceIQ.Channel channel) + { + AccountID accountID = getProtocolProvider().getAccountID(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + SrtpControlType.DTLS_SRTP) + && (remoteContent != null)) + { + IceUdpTransportPacketExtension remoteTransport + = remoteContent.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + if (remoteTransport != null) + { + List remoteFingerprints + = remoteTransport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + + if (!remoteFingerprints.isEmpty()) + { + IceUdpTransportPacketExtension localTransport + = ensureTransportOnChannel(channel, peer); + + if (localTransport != null) + { + List localFingerprints + = localTransport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + + if (localFingerprints.isEmpty()) + { + for (DtlsFingerprintPacketExtension remoteFingerprint + : remoteFingerprints) + { + DtlsFingerprintPacketExtension localFingerprint + = new DtlsFingerprintPacketExtension(); + + localFingerprint.setFingerprint( + remoteFingerprint.getFingerprint()); + localFingerprint.setHash( + remoteFingerprint.getHash()); + localTransport.addChildExtension( + localFingerprint); + } + } + } + } + } + } + } + + /** + * Updates the state of the remote DTLS-SRTP endpoint represented by a + * specific ColibriConferenceIQ.Channel from the state of the local + * DTLS-SRTP endpoint (i.e. the local DtlsControl instance). The + * specified channel is to be used by the conference focus for the + * purposes of transmitting media between the local peer and the Jitsi + * Videobridge server. + * + * @param jitsiVideobridge the address/JID of the Jitsi Videobridge + * @param peer the CallPeer associated with the method invocation + * @param mediaType the MediaType of the media to be transmitted + * over the DTLS-SRTP session + * @param channel the ColibriConferenceIQ.Channel which represents + * the state of the remote DTLS-SRTP endpoint. + */ + private void setDtlsEncryptionOnChannel( + String jitsiVideobridge, + CallPeerJabberImpl peer, + MediaType mediaType, + ColibriConferenceIQ.Channel channel) + { + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + AccountID accountID = protocolProvider.getAccountID(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + SrtpControlType.DTLS_SRTP) + && protocolProvider.isFeatureSupported( + jitsiVideobridge, + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_DTLS_SRTP)) + { + CallPeerMediaHandlerJabberImpl mediaHandler + = peer.getMediaHandler(); + DtlsControl dtlsControl + = (DtlsControl) + mediaHandler.getSrtpControls().getOrCreate( + mediaType, + SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + IceUdpTransportPacketExtension transport + = ensureTransportOnChannel(channel, peer); + + if (transport != null) + setDtlsEncryptionOnTransport(dtlsControl, transport); + } + } + } + + /** + * Sets the properties (i.e. fingerprint and hash function) of a specific + * DtlsControl on the specific + * IceUdpTransportPacketExtension. + * + * @param dtlsControl the DtlsControl the properties of which are + * to be set on the specified localTransport + * @param localTransport the IceUdpTransportPacketExtension on + * which the properties of the specified dtlsControl are to be set + */ + static void setDtlsEncryptionOnTransport( + DtlsControl dtlsControl, + IceUdpTransportPacketExtension localTransport) + { + String fingerprint = dtlsControl.getLocalFingerprint(); + String hash = dtlsControl.getLocalFingerprintHashFunction(); + + DtlsFingerprintPacketExtension fingerprintPE + = localTransport.getFirstChildOfType( + DtlsFingerprintPacketExtension.class); + + if (fingerprintPE == null) + { + fingerprintPE = new DtlsFingerprintPacketExtension(); + localTransport.addChildExtension(fingerprintPE); + } + fingerprintPE.setFingerprint(fingerprint); + fingerprintPE.setHash(hash); + } + + private void setTransportOnChannel( + CallPeerJabberImpl peer, + String media, + ColibriConferenceIQ.Channel channel) + throws OperationFailedException + { + PacketExtension transport + = peer.getMediaHandler().getTransportManager().createTransport( + media); + + if (transport instanceof IceUdpTransportPacketExtension) + channel.setTransport((IceUdpTransportPacketExtension) transport); + } + + private void setTransportOnChannel( + String media, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent, + CallPeerJabberImpl peer, + ColibriConferenceIQ.Channel channel) + throws OperationFailedException + { + if (remoteContent != null) + { + IceUdpTransportPacketExtension transport + = remoteContent.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + channel.setTransport( + TransportManagerJabberImpl.cloneTransportAndCandidates( + transport)); + } + } + + /** + * Makes an attempt to ensure that a specific + * ColibriConferenceIQ.Channel has a non-null + * transport set. If the specified channel does not have + * a transport, the method invokes the TransportManager of + * the specified CallPeerJabberImpl to initialize a new + * PacketExtension. + * + * @param channel the ColibriConferenceIQ.Channel to ensure the + * transport on + * @param peer the CallPeerJabberImpl which is associated with the + * specified channel and which specifies the + * TransportManager to be described in the specified + * channel + * @return the transport of the specified channel + */ + private IceUdpTransportPacketExtension ensureTransportOnChannel( + ColibriConferenceIQ.Channel channel, + CallPeerJabberImpl peer) + { + IceUdpTransportPacketExtension transport + = channel.getTransport(); + + if (transport == null) + { + PacketExtension pe + = peer + .getMediaHandler() + .getTransportManager() + .createTransportPacketExtension(); + + if (pe instanceof IceUdpTransportPacketExtension) + { + transport = (IceUdpTransportPacketExtension) pe; + channel.setTransport(transport); + } + } + return transport; + } + + /** + * Gets the entity ID of the Jitsi Videobridge to be utilized by this + * Call for the purposes of establishing a server-assisted + * telephony conference. + * + * @return the entity ID of the Jitsi Videobridge to be utilized by this + * Call for the purposes of establishing a server-assisted + * telephony conference. + */ + public String getJitsiVideobridge() + { + if ((this.jitsiVideobridge == null) + && getConference().isJitsiVideobridge()) + { + String jitsiVideobridge + = getProtocolProvider().getJitsiVideobridge(); + + if (jitsiVideobridge != null) + this.jitsiVideobridge = jitsiVideobridge; + } + return this.jitsiVideobridge; + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java index b1d1b669e..be1fba7d3 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java @@ -1,2825 +1,2825 @@ -/* - * Jitsi, the OpenSource Java VoIP and Instant Messaging client. - * - * Distributable under LGPL license. - * See terms of license at gnu.org. - */ -package net.java.sip.communicator.impl.protocol.jabber; - -import java.awt.*; -import java.beans.*; -import java.lang.reflect.*; -import java.util.*; -import java.util.List; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.ContentPacketExtension.SendersEnum; -import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.libjitsi.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.device.*; -import org.jitsi.service.neomedia.format.*; -import org.jivesoftware.smackx.packet.*; - -/** - * An XMPP specific extension of the generic media handler. - * - * @author Emil Ivov - * @author Lyubomir Marinov - * @author Hristo Terezov - * @author Boris Grozev - */ -public class CallPeerMediaHandlerJabberImpl - extends AbstractCallPeerMediaHandlerJabberGTalkImpl -{ - /** - * The Logger used by the CallPeerMediaHandlerJabberImpl - * class and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(CallPeerMediaHandlerJabberImpl.class); - - /** - * Determines whether a specific XMPP feature is supported by both a - * specific ScServiceDiscoveryManager (may be referred to as the - * local peer) and a specific DiscoverInfo (may be thought of as - * the remote peer). - * - * @param discoveryManager the ScServiceDiscoveryManager to be - * checked whether it includes the specified feature - * @param discoverInfo the DiscoveryInfo which is to be checked - * whether it contains the specified feature. If discoverInfo is - * null, it is considered to contain the specified feature. - * @param feature the feature to be determined whether it is supported by - * both the specified discoveryManager and the specified - * discoverInfo - * @return true if the specified feature is supported by - * both the specified discoveryManager and the specified - * discoverInfo; otherwise, false - */ - private static boolean isFeatureSupported( - ScServiceDiscoveryManager discoveryManager, - DiscoverInfo discoverInfo, - String feature) - { - return - discoveryManager.includesFeature(feature) - && ((discoverInfo == null) - || discoverInfo.containsFeature(feature)); - } - - /** - * The current description of the streams that we have going toward the - * remote side. We use {@link LinkedHashMap}s to make sure that we preserve - * the order of the individual content extensions. - */ - private final Map localContentMap - = new LinkedHashMap(); - - /** - * The QualityControl of this CallPeerMediaHandler. - */ - private final QualityControlWrapper qualityControls; - - /** - * The current description of the streams that the remote side has with us. - * We use {@link LinkedHashMap}s to make sure that we preserve - * the order of the individual content extensions. - */ - private final Map remoteContentMap - = new LinkedHashMap(); - - /** - * Indicates whether the remote party has placed us on hold. - */ - private boolean remotelyOnHold = false; - - /** - * Whether other party is able to change video quality settings. - * Normally its whether we have detected existence of imageattr in sdp. - */ - private boolean supportQualityControls = false; - - /** - * The TransportManager implementation handling our address - * management. - */ - private TransportManagerJabberImpl transportManager; - - /** - * The Object which is used for synchronization (e.g. wait - * and notify) related to {@link #transportManager}. - */ - private final Object transportManagerSyncRoot = new Object(); - - /** - * The ordered by preference array of the XML namespaces of the jingle - * transports that this peer supports. If it is non-null, it will be used - * instead of checking disco#info in order to select an appropriate - * transport manager. - */ - private String[] supportedTransports = null; - - /** - * Object used to synchronize access to supportedTransports - */ - private final Object supportedTransportsSyncRoot = new Object(); - - /** - * Creates a new handler that will be managing media streams for - * peer. - * - * @param peer that CallPeerJabberImpl instance that we will be - * managing media for. - */ - public CallPeerMediaHandlerJabberImpl(CallPeerJabberImpl peer) - { - super(peer); - - qualityControls = new QualityControlWrapper(peer); - } - - /** - * Determines the direction that a stream, which has been placed on - * hold by the remote party, would need to go back to after being - * re-activated. If the stream is not currently on hold (i.e. it is still - * sending media), this method simply returns its current direction. - * - * @param stream the {@link MediaStreamTarget} whose post-hold direction - * we'd like to determine. - * - * @return the {@link MediaDirection} that we need to set on stream - * once it is reactivate. - */ - private MediaDirection calculatePostHoldDirection(MediaStream stream) - { - MediaDirection streamDirection = stream.getDirection(); - - if (streamDirection.allowsSending()) - return streamDirection; - - /* - * When calculating a direction we need to take into account 1) what - * direction the remote party had asked for before putting us on hold, - * 2) what the user preference is for the stream's media type, 3) our - * local hold status, 4) the direction supported by the device this - * stream is reading from. - */ - - // 1. what the remote party originally told us (from our perspective) - ContentPacketExtension content = remoteContentMap.get(stream.getName()); - MediaDirection postHoldDir - = JingleUtils.getDirection(content, !getPeer().isInitiator()); - - // 2. the user preference - MediaDevice device = stream.getDevice(); - - postHoldDir - = postHoldDir.and( - getDirectionUserPreference(device.getMediaType())); - - // 3. our local hold status - if (isLocallyOnHold()) - postHoldDir = postHoldDir.and(MediaDirection.SENDONLY); - - // 4. the device direction - postHoldDir = postHoldDir.and(device.getDirection()); - - return postHoldDir; - } - - /** - * Closes the CallPeerMediaHandler. - */ - @Override - public synchronized void close() - { - super.close(); - - OperationSetDesktopSharingClientJabberImpl client - = (OperationSetDesktopSharingClientJabberImpl) - getPeer().getProtocolProvider().getOperationSet( - OperationSetDesktopSharingClient.class); - - if (client != null) - client.fireRemoteControlRevoked(getPeer()); - } - - /** - * Creates a {@link ContentPacketExtension}s of the streams for a - * specific MediaDevice. - * - * @param dev MediaDevice - * @return the {@link ContentPacketExtension}s of stream that this - * handler is prepared to initiate. - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like problems with device interaction, allocating ports, etc. - */ - private ContentPacketExtension createContent(MediaDevice dev) - throws OperationFailedException - { - MediaType mediaType = dev.getMediaType(); - //this is the direction to be used in the jingle session - MediaDirection direction = dev.getDirection(); - CallPeerJabberImpl peer = getPeer(); - - /* - * In the case of RTP translation performed by the conference focus, - * the conference focus is not required to capture media. - */ - if (!(MediaType.VIDEO.equals(mediaType) - && isRTPTranslationEnabled(mediaType))) - direction = direction.and(getDirectionUserPreference(mediaType)); - - /* - * Check if we need to announce sending on behalf of other peers - */ - CallJabberImpl call = peer.getCall(); - - if (call.isConferenceFocus()) - { - for (CallPeerJabberImpl anotherPeer : call.getCallPeerList()) - { - if ((anotherPeer != peer) - && anotherPeer.getDirection(mediaType) - .allowsReceiving()) - { - direction = direction.or(MediaDirection.SENDONLY); - break; - } - } - } - - if (isLocallyOnHold()) - direction = direction.and(MediaDirection.SENDONLY); - - QualityPreset sendQualityPreset = null; - QualityPreset receiveQualityPreset = null; - - if(qualityControls != null) - { - // the one we will send is the one the remote has announced as - // receive - sendQualityPreset = qualityControls.getRemoteReceivePreset(); - // the one we want to receive is the one the remote can send - receiveQualityPreset = qualityControls.getRemoteSendMaxPreset(); - } - if(direction != MediaDirection.INACTIVE) - { - ContentPacketExtension content - = createContentForOffer( - getLocallySupportedFormats( - dev, - sendQualityPreset, - receiveQualityPreset), - direction, - dev.getSupportedExtensions()); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - - // DTLS-SRTP - setDtlsEncryptionOnContent(mediaType, content, null); - /* - * Neither SDES nor ZRTP is supported in telephony conferences - * utilizing the server-side technology Jitsi Videobridge yet. - */ - if (!call.getConference().isJitsiVideobridge()) - { - // SDES - // It is important to set SDES before ZRTP in order to make - // GTALK application able to work with SDES. - setSDesEncryptionOnDescription(mediaType, description, null); - // ZRTP - setZrtpEncryptionOnDescription(mediaType, description, null); - } - - return content; - } - return null; - } - - /** - * Creates a {@link ContentPacketExtension} for a particular stream. - * - * @param mediaType MediaType of the content - * @return a {@link ContentPacketExtension} - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like - problems with device interaction, allocating ports, - * etc. - */ - public ContentPacketExtension createContentForMedia(MediaType mediaType) - throws OperationFailedException - { - MediaDevice dev = getDefaultDevice(mediaType); - - if(isDeviceActive(dev)) - return createContent(dev); - return null; - } - - /** - * Generates an Jingle {@link ContentPacketExtension} for the specified - * {@link MediaFormat} list, direction and RTP extensions taking account - * the local streaming preference for the corresponding media type. - * - * @param supportedFormats the list of MediaFormats that we'd - * like to advertise. - * @param direction the MediaDirection that we'd like to establish - * the stream in. - * @param supportedExtensions the list of RTPExtensions that we'd - * like to advertise in the MediaDescription. - * - * @return a newly created {@link ContentPacketExtension} representing - * streams that we'd be able to handle. - */ - private ContentPacketExtension createContentForOffer( - List supportedFormats, - MediaDirection direction, - List supportedExtensions) - { - ContentPacketExtension content - = JingleUtils.createDescription( - ContentPacketExtension.CreatorEnum.initiator, - supportedFormats.get(0).getMediaType().toString(), - JingleUtils.getSenders(direction, !getPeer().isInitiator()), - supportedFormats, - supportedExtensions, - getDynamicPayloadTypes(), - getRtpExtensionsRegistry()); - - this.localContentMap.put(content.getName(), content); - return content; - } - - /** - * Creates a List containing the {@link ContentPacketExtension}s of - * the streams that this handler is prepared to initiate depending on - * available MediaDevices and local on-hold and video transmission - * preferences. - * - * @return a {@link List} containing the {@link ContentPacketExtension}s of - * streams that this handler is prepared to initiate. - * - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like problems with device interaction, allocating ports, etc. - */ - public List createContentList() - throws OperationFailedException - { - // Describe the media. - List mediaDescs - = new ArrayList(); - boolean jitsiVideobridge - = getPeer().getCall().getConference().isJitsiVideobridge(); - - for (MediaType mediaType : MediaType.values()) - { - MediaDevice dev = getDefaultDevice(mediaType); - - if (isDeviceActive(dev)) - { - MediaDirection direction = dev.getDirection(); - - /* - * In the case of RTP translation performed by the conference - * focus, the conference focus is not required to capture media. - */ - if (!(MediaType.VIDEO.equals(mediaType) - && isRTPTranslationEnabled(mediaType))) - { - direction - = direction.and(getDirectionUserPreference(mediaType)); - } - if (isLocallyOnHold()) - direction = direction.and(MediaDirection.SENDONLY); - - /* - * If we're only able to receive, we don't have to offer it at - * all. For example, we have to offer audio and no video when we - * start an audio call. - */ - if (MediaDirection.RECVONLY.equals(direction)) - direction = MediaDirection.INACTIVE; - - if (direction != MediaDirection.INACTIVE) - { - ContentPacketExtension content - = createContentForOffer( - getLocallySupportedFormats(dev), - direction, - dev.getSupportedExtensions()); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - - // DTLS-SRTP - setDtlsEncryptionOnContent(mediaType, content, null); - /* - * Neither SDES nor ZRTP is supported in telephony - * conferences utilizing the server-side technology Jitsi - * Videobridge yet. - */ - if (!jitsiVideobridge) - { - // SDES - // It is important to set SDES before ZRTP in order to - // make GTALK application able to work with SDES. - setSDesEncryptionOnDescription( - mediaType, - description, - null); - //ZRTP - setZrtpEncryptionOnDescription( - mediaType, - description, - null); - } - - // we request a desktop sharing session so add the inputevt - // extension in the "video" content - if (description.getMedia().equals( - MediaType.VIDEO.toString()) - && getLocalInputEvtAware()) - { - content.addChildExtension( - new InputEvtPacketExtension()); - } - - mediaDescs.add(content); - } - } - } - - // Fail if no media is described (e.g. all devices are inactive). - if (mediaDescs.isEmpty()) - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "We couldn't find any active Audio/Video devices" - + " and couldn't create a call", - OperationFailedException.GENERAL_ERROR, - null, - logger); - } - - // Describe the transport(s). - return harvestCandidates(null, mediaDescs, null); - } - - /** - * Creates a List containing the {@link ContentPacketExtension}s of - * the streams of a specific MediaType that this handler is - * prepared to initiate depending on available MediaDevices and - * local on-hold and video transmission preferences. - * - * @param mediaType MediaType of the content - * @return a {@link List} containing the {@link ContentPacketExtension}s of - * streams that this handler is prepared to initiate. - * - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like - problems with device interaction, allocating ports, - * etc. - */ - public List createContentList(MediaType mediaType) - throws OperationFailedException - { - MediaDevice dev = getDefaultDevice(mediaType); - List mediaDescs - = new ArrayList(); - - if (isDeviceActive(dev)) - { - ContentPacketExtension content = createContent(dev); - - if (content != null) - mediaDescs.add(content); - } - - // Fail if no media is described (e.g. all devices are inactive). - if (mediaDescs.isEmpty()) - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "We couldn't find any active Audio/Video devices and " - + "couldn't create a call", - OperationFailedException.GENERAL_ERROR, - null, - logger); - } - - // Describe the transport(s). - return harvestCandidates(null, mediaDescs, null); - } - - /** - * Overrides to give access to the transport manager to send events - * about ICE state changes. - * - * @param property the name of the property of this - * PropertyChangeNotifier which had its value changed - * @param oldValue the value of the property with the specified name before - * the change - * @param newValue the value of the property with the specified name after - */ - @Override - protected void firePropertyChange( - String property, - Object oldValue, Object newValue) - { - super.firePropertyChange(property, oldValue, newValue); - } - - /** - * Wraps up any ongoing candidate harvests and returns our response to the - * last offer we've received, so that the peer could use it to send a - * session-accept. - * - * @return the last generated list of {@link ContentPacketExtension}s that - * the call peer could use to send a session-accept. - * - * @throws OperationFailedException if we fail to configure the media stream - */ - public Iterable generateSessionAccept() - throws OperationFailedException - { - TransportManagerJabberImpl transportManager = getTransportManager(); - Iterable sessAccept - = transportManager.wrapupCandidateHarvest(); - CallPeerJabberImpl peer = getPeer(); - - //user answered an incoming call so we go through whatever content - //entries we are initializing and init their corresponding streams - - // First parse content so we know how many streams and what type of - // content we have - Map contents - = new HashMap(); - - for(ContentPacketExtension ourContent : sessAccept) - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(ourContent); - - contents.put(ourContent, description); - } - - boolean masterStreamSet = false; - - for(Map.Entry en - : contents.entrySet()) - { - ContentPacketExtension ourContent = en.getKey(); - - RtpDescriptionPacketExtension description = en.getValue(); - MediaType type = MediaType.parseString(description.getMedia()); - - // stream connector - StreamConnector connector - = transportManager.getStreamConnector(type); - - //the device this stream would be reading from and writing to. - MediaDevice dev = getDefaultDevice(type); - - if(!isDeviceActive(dev)) - continue; - - // stream target - MediaStreamTarget target = transportManager.getStreamTarget(type); - - //stream direction - MediaDirection direction - = JingleUtils.getDirection(ourContent, !peer.isInitiator()); - - // if we answer with video, tell remote peer that video direction is - // sendrecv, and whether video device can capture/send - if (MediaType.VIDEO.equals(type) - && (isLocalVideoTransmissionEnabled() - || isRTPTranslationEnabled(type)) - && dev.getDirection().allowsSending()) - { - direction = MediaDirection.SENDRECV; - ourContent.setSenders(ContentPacketExtension.SendersEnum.both); - } - - //let's now see what was the format we announced as first and - //configure the stream with it. - String contentName = ourContent.getName(); - ContentPacketExtension theirContent - = this.remoteContentMap.get(contentName); - RtpDescriptionPacketExtension theirDescription - = JingleUtils.getRtpDescription(theirContent); - MediaFormat format = null; - - List localFormats = getLocallySupportedFormats(dev); - for(PayloadTypePacketExtension payload - : theirDescription.getPayloadTypes()) - { - MediaFormat remoteFormat = JingleUtils.payloadTypeToMediaFormat( - payload, getDynamicPayloadTypes()); - if(remoteFormat != null - && (format = findMediaFormat(localFormats, remoteFormat)) - != null) - break; - } - - if(format == null) - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "No matching codec.", - OperationFailedException.ILLEGAL_ARGUMENT, - null, - logger); - } - - //extract the extensions that we are advertising: - // check whether we will be exchanging any RTP extensions. - List rtpExtensions - = JingleUtils.extractRTPExtensions( - description, - this.getRtpExtensionsRegistry()); - - Map adv = format.getAdvancedAttributes(); - if(adv != null) - { - for(Map.Entry f : adv.entrySet()) - { - if(f.getKey().equals("imageattr")) - supportQualityControls = true; - } - } - - boolean masterStream = false; - // if we have more than one stream, lets the audio be the master - if(!masterStreamSet) - { - if(contents.size() > 1) - { - if(type.equals(MediaType.AUDIO)) - { - masterStream = true; - masterStreamSet = true; - } - } - else - { - masterStream = true; - masterStreamSet = true; - } - } - - // create the corresponding stream... - MediaStream stream = initStream(contentName, - connector, - dev, - format, - target, - direction, - rtpExtensions, - masterStream); - - long ourSsrc = stream.getLocalSourceID() & 0xffffffffL; - if (direction.allowsSending() && ourSsrc != -1) - { - description.setSsrc(Long.toString(ourSsrc)); - addSourceExtension(description, ourSsrc); - } - } - return sessAccept; - } - - /** - * Adds a SourcePacketExtension as a child element of - * description. See XEP-0339. - * - * @param description the RtpDescriptionPacketExtension to which - * a child element will be added. - * @param ssrc the SSRC for the SourcePacketExtension to use. - */ - private void addSourceExtension(RtpDescriptionPacketExtension description, - long ssrc) - { - MediaType type = MediaType.parseString(description.getMedia()); - - SourcePacketExtension sourcePacketExtension - = new SourcePacketExtension(); - - sourcePacketExtension.setSSRC(ssrc); - sourcePacketExtension.addChildExtension( - new ParameterPacketExtension("cname", - LibJitsi.getMediaService() - .getRtpCname())); - sourcePacketExtension.addChildExtension( - new ParameterPacketExtension("msid", getMsid(type))); - sourcePacketExtension.addChildExtension( - new ParameterPacketExtension("mslabel", getMsLabel())); - sourcePacketExtension.addChildExtension( - new ParameterPacketExtension("label", getLabel(type))); - - description.addChildExtension(sourcePacketExtension); - } - - /** - * Returns the local content of a specific content type (like audio or - * video). - * - * @param contentType content type name - * @return remote ContentPacketExtension or null if not found - */ - public ContentPacketExtension getLocalContent(String contentType) - { - for(String key : localContentMap.keySet()) - { - ContentPacketExtension content = localContentMap.get(key); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - - if(description.getMedia().equals(contentType)) - return content; - } - return null; - } - - /** - * Returns a complete list of call currently known local content-s. - * - * @return a list of {@link ContentPacketExtension} null if not found - */ - public Iterable getLocalContentList() - { - return localContentMap.values(); - } - - /** - * Returns the quality control for video calls if any. - * - * @return the implemented quality control. - */ - public QualityControl getQualityControl() - { - if(supportQualityControls) - { - return qualityControls; - } - else - { - // we have detected that its not supported and return null - // and control ui won't be visible - return null; - } - } - - /** - * Get the remote content of a specific content type (like audio or video). - * - * @param contentType content type name - * @return remote ContentPacketExtension or null if not found - */ - public ContentPacketExtension getRemoteContent(String contentType) - { - for(String key : remoteContentMap.keySet()) - { - ContentPacketExtension content = remoteContentMap.get(key); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - - if(description.getMedia().equals(contentType)) - return content; - } - return null; - } - - /** - * {@inheritDoc} - * - * In the case of a telephony conference organized by the local peer/user - * via the Jitsi Videobridge server-side technology, returns an SSRC - * reported by the server as received on the channel allocated by the local - * peer/user for the purposes of communicating with the CallPeer - * associated with this instance. - */ - @Override - public long getRemoteSSRC(MediaType mediaType) - { - int[] ssrcs = getRemoteSSRCs(mediaType); - - /* - * A peer (regardless of whether it is local or remote) may send - * multiple RTP streams at any time. In such a case, it is not clear - * which one of their SSRCs is to be returned. Anyway, the super says - * that the returned is the last known. We will presume that the last - * known in the list reported by the Jitsi Videobridge server is the - * last. - */ - if (ssrcs.length != 0) - return 0xFFFFFFFFL & ssrcs[ssrcs.length - 1]; - - /* - * XXX In the case of Jitsi Videobridge, the super implementation of - * getRemoteSSRC(MediaType) cannot be trusted because there is a single - * VideoMediaStream with multiple ReceiveStreams. - */ - return - getPeer().isJitsiVideobridge() - ? SSRC_UNKNOWN - : super.getRemoteSSRC(mediaType); - } - - /** - * Gets the SSRCs of RTP streams with a specific MediaType known to - * be received by a MediaStream associated with this instance. - *

- * Warning: The method may return only one of the many possible - * remote SSRCs in the case of no utilization of the Jitsi Videobridge - * server-side technology because the super implementation does not - * currently provide support for keeping track of multiple remote SSRCs. - *

- * - * @param mediaType the MediaType of the RTP streams the SSRCs of - * which are to be returned - * @return an array of int values which represent the SSRCs of RTP - * streams with the specified mediaType known to be received by a - * MediaStream associated with this instance - */ - private int[] getRemoteSSRCs(MediaType mediaType) - { - /* - * If the Jitsi Videobridge server-side technology is utilized, a single - * MediaStream (per MediaType) is shared among the participating - * CallPeers and, consequently, the remote SSRCs cannot be associated - * with the CallPeers from which they are actually being sent. That's - * why the server will report them to the conference focus. - */ - ColibriConferenceIQ.Channel channel = getColibriChannel(mediaType); - - if (channel != null) - return channel.getSSRCs(); - - /* - * XXX The fallback to the super implementation that follows may lead to - * unexpected behavior due to the lack of ability to keep track of - * multiple remote SSRCs. - */ - long ssrc = super.getRemoteSSRC(mediaType); - - return - (ssrc == SSRC_UNKNOWN) - ? ColibriConferenceIQ.NO_SSRCS - : new int[] { (int) ssrc }; - } - - /** - * Gets the TransportManager implementation handling our address - * management. - * - * TODO: this method can and should be simplified. - * - * @return the TransportManager implementation handling our address - * management - * @see CallPeerMediaHandler#getTransportManager() - */ - @Override - protected synchronized TransportManagerJabberImpl getTransportManager() - { - if (transportManager == null) - { - CallPeerJabberImpl peer = getPeer(); - - if (peer.isInitiator()) - { - synchronized(transportManagerSyncRoot) - { - try - { - transportManagerSyncRoot.wait(5000); - } - catch(InterruptedException e) - { - } - } - if(transportManager == null) - { - throw new IllegalStateException( - "The initiator is expected to specify the transport" - + " in their offer."); - } - else - return transportManager; - } - else - { - ProtocolProviderServiceJabberImpl protocolProvider - = peer.getProtocolProvider(); - ScServiceDiscoveryManager discoveryManager - = protocolProvider.getDiscoveryManager(); - DiscoverInfo peerDiscoverInfo = peer.getDiscoveryInfo(); - - /* - * If this.supportedTransports has been explicitly set, we use - * it to select the transport manager -- we use the first - * transport in the list which we recognize (e.g. the first - * that is either ice or raw-udp - */ - synchronized (supportedTransportsSyncRoot) - { - if (supportedTransports != null - && supportedTransports.length > 0) - { - for (int i = 0; i < supportedTransports.length; i++) - { - if (ProtocolProviderServiceJabberImpl. - URN_XMPP_JINGLE_ICE_UDP_1. - equals(supportedTransports[i])) - { - transportManager - = new IceUdpTransportManager(peer); - break; - } - else if (ProtocolProviderServiceJabberImpl. - URN_XMPP_JINGLE_RAW_UDP_0. - equals(supportedTransports[i])) - { - transportManager - = new RawUdpTransportManager(peer); - break; - } - } - if (transportManager == null) - { - logger.warn( - "Could not find a supported" - + " TransportManager in" - + " supportedTransports. Will try to" - + " select one based on disco#info."); - } - } - } - - if (transportManager == null) - { - /* - * The list of possible transports ordered by decreasing - * preference. - */ - String[] transports - = new String[] - { - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_ICE_UDP_1, - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_RAW_UDP_0 - }; - - /* - * If Jitsi Videobridge is to be employed, pick up a Jingle - * transport supported by it. - */ - if (peer.isJitsiVideobridge()) - { - CallJabberImpl call = peer.getCall(); - - if (call != null) - { - String jitsiVideobridge - = peer.getCall().getJitsiVideobridge(); - - /* - * Jitsi Videobridge supports the Jingle Raw UDP - * transport from its inception. But that is not the - * case with the Jingle ICE-UDP transport. - */ - if ((jitsiVideobridge != null) - && !protocolProvider.isFeatureSupported( - jitsiVideobridge, - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_ICE_UDP_1)) - { - for (int i = transports.length - 1; i >= 0; i--) - { - if (ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_ICE_UDP_1 - .equals(transports[i])) - { - transports[i] = null; - } - } - } - } - } - - /* - * Select the first transport from the list of possible - * transports ordered by decreasing preference which is - * supported by the local and the remote peers. - */ - for (String transport : transports) - { - if (transport == null) - continue; - if (isFeatureSupported( - discoveryManager, - peerDiscoverInfo, - transport)) - { - if (ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_ICE_UDP_1 - .equals(transport)) - { - transportManager - = new IceUdpTransportManager(peer); - } - else if (ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_RAW_UDP_0 - .equals(transport)) - { - transportManager - = new RawUdpTransportManager(peer); - } - - if (transportManager != null) - break; - } - } - - if ((transportManager == null) && logger.isDebugEnabled()) - { - logger.debug( - "No known Jingle transport supported by Jabber" - + " call peer " + peer); - } - } - } - } - return transportManager; - } - - /** - * {@inheritDoc} - * - * @see CallPeerMediaHandler#queryTransportManager() - */ - @Override - protected synchronized TransportManagerJabberImpl queryTransportManager() - { - return transportManager; - } - - /** - * {@inheritDoc} - * - * In the case of utilization of the Jitsi Videobridge server-side - * technology, returns the visual Components which display RTP - * video streams reported by the server to be sent by the remote peer - * represented by this instance. - */ - @Override - public List getVisualComponents() - { - /* - * TODO The super is currently unable to provide the complete set of - * remote SSRCs (i.e. in the case of no utilization of the Jitsi - * Videobridge server-side technology) so we have to explicitly check - * for Jitsi Videobridge instead of just relying on the implementation - * of the getRemoteSSRCs(MediaType) method to abstract away that detail. - */ - CallJabberImpl call; - MediaAwareCallConference conference; - - if (((call = getPeer().getCall()) != null) - && ((conference = call.getConference()) != null) - && conference.isJitsiVideobridge()) - { - MediaStream stream = getStream(MediaType.VIDEO); - - if (stream == null) - return Collections.emptyList(); - else - { - int[] remoteSSRCs = getRemoteSSRCs(MediaType.VIDEO); - - if (remoteSSRCs.length == 0) - return Collections.emptyList(); - else - { - VideoMediaStream videoStream = (VideoMediaStream) stream; - List visualComponents - = new LinkedList(); - - for (int i = 0; i < remoteSSRCs.length; i++) - { - int remoteSSRC = remoteSSRCs[i]; - Component visualComponent - = videoStream.getVisualComponent( - 0xFFFFFFFFL & remoteSSRC); - - if (visualComponent != null) - visualComponents.add(visualComponent); - } - return visualComponents; - } - } - } - - return super.getVisualComponents(); - } - - /** - * Gathers local candidate addresses. - * - * @param remote the media descriptions received from the remote peer if any - * or null if local represents an offer from the local - * peer to be sent to the remote peer - * @param local the media descriptions sent or to be sent from the local - * peer to the remote peer. If remote is null, - * local represents an offer from the local peer to be sent to the - * remote peer - * @param transportInfoSender the TransportInfoSender to be used by - * this TransportManagerJabberImpl to send transport-info - * JingleIQs from the local peer to the remote peer if this - * TransportManagerJabberImpl wishes to utilize - * transport-info - * @return the media descriptions of the local peer after the local - * candidate addresses have been gathered as returned by - * {@link TransportManagerJabberImpl#wrapupCandidateHarvest()} - * @throws OperationFailedException if anything goes wrong while starting or - * wrapping up the gathering of local candidate addresses - */ - private List harvestCandidates( - List remote, - List local, - TransportInfoSender transportInfoSender) - throws OperationFailedException - { - long startCandidateHarvestTime = System.currentTimeMillis(); - TransportManagerJabberImpl transportManager = getTransportManager(); - - if (remote == null) - { - /* - * We'll be harvesting candidates in order to make an offer so it - * doesn't make sense to send them in transport-info. - */ - if (transportInfoSender != null) - throw new IllegalArgumentException("transportInfoSender"); - - transportManager.startCandidateHarvest(local, transportInfoSender); - } - else - { - transportManager.startCandidateHarvest( - remote, - local, - transportInfoSender); - } - - long stopCandidateHarvestTime = System.currentTimeMillis(); - - if (logger.isInfoEnabled()) - { - long candidateHarvestTime - = stopCandidateHarvestTime - startCandidateHarvestTime; - - logger.info( - "End candidate harvest within " + candidateHarvestTime - + " ms"); - } - - setDtlsEncryptionOnTransports(remote, local); - - if (transportManager.startConnectivityEstablishmentWithJitsiVideobridge) - { - Map map - = new LinkedHashMap(); - - for (MediaType mediaType : MediaType.values()) - { - ColibriConferenceIQ.Channel channel - = transportManager.getColibriChannel( - mediaType, - true /* local */); - - if (channel != null) - { - IceUdpTransportPacketExtension transport - = channel.getTransport(); - - if (transport != null) - map.put(mediaType.toString(), transport); - } - } - if (!map.isEmpty()) - { - transportManager - .startConnectivityEstablishmentWithJitsiVideobridge - = false; - transportManager.startConnectivityEstablishment(map); - } - } - - /* - * TODO Ideally, we wouldn't wrap up that quickly. We need to revisit - * this. - */ - return transportManager.wrapupCandidateHarvest(); - } - - /** - * Creates if necessary, and configures the stream that this - * MediaHandler is using for the MediaType matching the - * one of the MediaDevice. This method extends the one already - * available by adding a stream name, corresponding to a stream's content - * name. - * - * @param streamName the name of the stream as indicated in the XMPP - * content element. - * @param connector the MediaConnector that we'd like to bind the - * newly created stream to. - * @param device the MediaDevice that we'd like to attach the newly - * created MediaStream to. - * @param format the MediaFormat that we'd like the new - * MediaStream to be set to transmit in. - * @param target the MediaStreamTarget containing the RTP and RTCP - * address:port couples that the new stream would be sending packets to. - * @param direction the MediaDirection that we'd like the new - * stream to use (i.e. sendonly, sendrecv, recvonly, or inactive). - * @param rtpExtensions the list of RTPExtensions that should be - * enabled for this stream. - * @param masterStream whether the stream to be used as master if secured - * - * @return the newly created MediaStream. - * - * @throws OperationFailedException if creating the stream fails for any - * reason (like for example accessing the device or setting the format). - */ - protected MediaStream initStream(String streamName, - StreamConnector connector, - MediaDevice device, - MediaFormat format, - MediaStreamTarget target, - MediaDirection direction, - List rtpExtensions, - boolean masterStream) - throws OperationFailedException - { - MediaStream stream - = super.initStream( - connector, - device, - format, - target, - direction, - rtpExtensions, - masterStream); - - if (stream != null) - stream.setName(streamName); - - return stream; - } - - /** - * {@inheritDoc} - * - * In the case of a telephony conference organized by the local peer/user - * and utilizing the Jitsi Videobridge server-side technology, a single - * MediaHandler is shared by multiple - * CallPeerMediaHandlers in order to have a single - * AudioMediaStream and a single VideoMediaStream. - * However, CallPeerMediaHandlerJabberImpl has redefined the - * reading/getting the remote audio and video SSRCs. Consequently, - * CallPeerMediaHandlerJabberImpl has to COMPLETELY redefine the - * writing/setting as well i.e. it has to stop related - * PropertyChangeEvents fired by the super. - */ - @Override - protected void mediaHandlerPropertyChange(PropertyChangeEvent ev) - { - String propertyName = ev.getPropertyName(); - - if ((AUDIO_REMOTE_SSRC.equals(propertyName) - || VIDEO_REMOTE_SSRC.equals(propertyName)) - && getPeer().isJitsiVideobridge()) - return; - - super.mediaHandlerPropertyChange(ev); - } - - /** - * Handles the specified answer by creating and initializing the - * corresponding MediaStreams. - * - * @param answer the Jingle answer - * - * @throws OperationFailedException if we fail to handle answer for - * reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of answer. Method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). - */ - public void processAnswer(List answer) - throws OperationFailedException, - IllegalArgumentException - { - /* - * The answer given in session-accept may contain transport-related - * information compatible with that carried in transport-info. - */ - processTransportInfo(answer); - - boolean masterStreamSet = false; - - for (ContentPacketExtension content : answer) - { - remoteContentMap.put(content.getName(), content); - - boolean masterStream = false; - - // if we have more than one stream, let the audio be the master - if(!masterStreamSet) - { - if(answer.size() > 1) - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - - if(MediaType.AUDIO.toString().equals( - description.getMedia())) - { - masterStream = true; - masterStreamSet = true; - } - } - else - { - masterStream = true; - masterStreamSet = true; - } - } - - processContent(content, false, masterStream); - } - } - - /** - * Notifies this instance that a specific ColibriConferenceIQ has - * been received. This CallPeerMediaHandler uses the part of the - * information provided in the specified conferenceIQ which - * concerns it only. - * - * @param conferenceIQ the ColibriConferenceIQ which has been - * received - */ - void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) - { - /* - * This CallPeerMediaHandler stores the media information but it does - * not store the colibri Channels (which contain both media and transport - * information). The TransportManager associated with this instance - * stores the colibri Channels but does not store media information (such - * as the remote SSRCs). An design/implementation choice has to be made - * though and the present one is to have this CallPeerMediaHandler - * transparently (with respect to the TransportManager) store the media - * information inside the TransportManager. - */ - TransportManagerJabberImpl transportManager = this.transportManager; - - if (transportManager != null) - { - long oldAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); - long oldVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); - - for (MediaType mediaType : MediaType.values()) - { - ColibriConferenceIQ.Channel dst - = transportManager.getColibriChannel( - mediaType, - false /* remote */); - - if (dst != null) - { - ColibriConferenceIQ.Content content - = conferenceIQ.getContent(mediaType.toString()); - - if (content != null) - { - ColibriConferenceIQ.Channel src - = content.getChannel(dst.getID()); - - if (src != null) - { - int[] ssrcs = src.getSSRCs(); - int[] dstSSRCs = dst.getSSRCs(); - - if (!Arrays.equals(dstSSRCs, ssrcs)) - dst.setSSRCs(ssrcs); - } - } - } - } - - /* - * Do fire new PropertyChangeEvents for the properties - * AUDIO_REMOTE_SSRC and VIDEO_REMOTE_SSRC if necessary. - */ - long newAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); - long newVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); - - if (oldAudioRemoteSSRC != newAudioRemoteSSRC) - { - firePropertyChange( - AUDIO_REMOTE_SSRC, - oldAudioRemoteSSRC, newAudioRemoteSSRC); - } - if (oldVideoRemoteSSRC != newVideoRemoteSSRC) - { - firePropertyChange( - VIDEO_REMOTE_SSRC, - oldVideoRemoteSSRC, newVideoRemoteSSRC); - } - } - } - - /** - * Process a ContentPacketExtension and initialize its - * corresponding MediaStream. - * - * @param content a ContentPacketExtension - * @param modify if it correspond to a content-modify for resolution change - * @param masterStream whether the stream to be used as master - * @throws OperationFailedException if we fail to handle content - * for reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of content. The method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). - */ - private void processContent( - ContentPacketExtension content, - boolean modify, - boolean masterStream) - throws OperationFailedException, - IllegalArgumentException - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - MediaType mediaType - = MediaType.parseString(description.getMedia()); - - //stream target - TransportManagerJabberImpl transportManager = getTransportManager(); - MediaStreamTarget target = transportManager.getStreamTarget(mediaType); - - if (target == null) - target = JingleUtils.extractDefaultTarget(content); - - // no target port - try next media description - if((target == null) || (target.getDataAddress().getPort() == 0)) - { - closeStream(mediaType); - return; - } - - List supportedFormats = JingleUtils.extractFormats( - description, getDynamicPayloadTypes()); - - MediaDevice dev = getDefaultDevice(mediaType); - - if(!isDeviceActive(dev)) - { - closeStream(mediaType); - return; - } - - MediaDirection devDirection - = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); - - // Take the preference of the user with respect to streaming - // mediaType into account. - devDirection - = devDirection.and(getDirectionUserPreference(mediaType)); - - if (supportedFormats.isEmpty()) - { - //remote party must have messed up our Jingle description. - //throw an exception. - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "Remote party sent an invalid Jingle answer.", - OperationFailedException.ILLEGAL_ARGUMENT, - null, - logger); - } - - CallJabberImpl call = getPeer().getCall(); - CallConference conference - = (call == null) ? null : call.getConference(); - - /* - * Neither SDES nor ZRTP is supported in telephony conferences utilizing - * the server-side technology Jitsi Videobridge yet. - */ - if ((conference == null) || !conference.isJitsiVideobridge()) - { - addZrtpAdvertisedEncryptions(true, description, mediaType); - addSDesAdvertisedEncryptions(true, description, mediaType); - } - addDtlsAdvertisedEncryptions(true, content, mediaType); - - StreamConnector connector - = transportManager.getStreamConnector(mediaType); - - //determine the direction that we need to announce. - MediaDirection remoteDirection - = JingleUtils.getDirection(content, getPeer().isInitiator()); - /* - * If we are the focus of a conference, we need to take into account the - * other participants. - */ - if ((conference != null) && conference.isConferenceFocus()) - { - for (CallPeerJabberImpl peer : call.getCallPeerList()) - { - SendersEnum senders - = peer.getSenders(mediaType); - boolean initiator = peer.isInitiator(); - //check if the direction of the jingle session we have with - //this peer allows us receiving media. If senders is null, - //assume the default of 'both' - if ((senders == null) - || (SendersEnum.both == senders) - || (initiator && SendersEnum.initiator == senders) - || (!initiator && SendersEnum.responder == senders)) - { - remoteDirection - = remoteDirection.or(MediaDirection.SENDONLY); - } - } - } - - MediaDirection direction - = devDirection.getDirectionForAnswer(remoteDirection); - - // update the RTP extensions that we will be exchanging. - List remoteRTPExtensions - = JingleUtils.extractRTPExtensions( - description, - getRtpExtensionsRegistry()); - List supportedExtensions - = getExtensionsForType(mediaType); - List rtpExtensions - = intersectRTPExtensions(remoteRTPExtensions, supportedExtensions); - - Map adv - = supportedFormats.get(0).getAdvancedAttributes(); - - if(adv != null) - { - for(Map.Entry f : adv.entrySet()) - { - if(f.getKey().equals("imageattr")) - supportQualityControls = true; - } - } - - // check for options from remote party and set them locally - if(mediaType.equals(MediaType.VIDEO) && modify) - { - // update stream - MediaStream stream = getStream(MediaType.VIDEO); - - if(stream != null && dev != null) - { - List fmts = supportedFormats; - - if(fmts.size() > 0) - { - MediaFormat fmt = fmts.get(0); - - ((VideoMediaStream)stream).updateQualityControl( - fmt.getAdvancedAttributes()); - } - } - - if(qualityControls != null) - { - QualityPreset receiveQualityPreset - = qualityControls.getRemoteReceivePreset(); - QualityPreset sendQualityPreset - = qualityControls.getRemoteSendMaxPreset(); - - supportedFormats - = (dev == null) - ? null - : intersectFormats( - supportedFormats, - getLocallySupportedFormats( - dev, - sendQualityPreset, - receiveQualityPreset)); - } - } - - // create the corresponding stream... - initStream( - content.getName(), - connector, - dev, - supportedFormats.get(0), - target, - direction, - rtpExtensions, - masterStream); - } - - /** - * Parses and handles the specified offer and returns a content - * extension representing the current state of this media handler. This - * method MUST only be called when offer is the first session - * description that this MediaHandler is seeing. - * - * @param offer the offer that we'd like to parse, handle and get an answer - * for. - * - * @throws OperationFailedException if we have a problem satisfying the - * description received in offer (e.g. failed to open a device or - * initialize a stream ...). - * @throws IllegalArgumentException if there's a problem with - * offer's format or semantics. - */ - public void processOffer(List offer) - throws OperationFailedException, - IllegalArgumentException - { - // prepare to generate answers to all the incoming descriptions - List answer - = new ArrayList(offer.size()); - boolean atLeastOneValidDescription = false; - - for (ContentPacketExtension content : offer) - { - remoteContentMap.put(content.getName(), content); - - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - MediaType mediaType - = MediaType.parseString( description.getMedia() ); - - List remoteFormats - = JingleUtils.extractFormats( - description, - getDynamicPayloadTypes()); - - MediaDevice dev = getDefaultDevice(mediaType); - - MediaDirection devDirection - = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); - - // Take the preference of the user with respect to streaming - // mediaType into account. - devDirection - = devDirection.and(getDirectionUserPreference(mediaType)); - - // determine the direction that we need to announce. - MediaDirection remoteDirection = JingleUtils.getDirection( - content, getPeer().isInitiator()); - MediaDirection direction - = devDirection.getDirectionForAnswer(remoteDirection); - - // intersect the MediaFormats of our device with remote ones - List mutuallySupportedFormats - = intersectFormats( - remoteFormats, - getLocallySupportedFormats(dev)); - - // check whether we will be exchanging any RTP extensions. - List offeredRTPExtensions - = JingleUtils.extractRTPExtensions( - description, this.getRtpExtensionsRegistry()); - - List supportedExtensions - = getExtensionsForType(mediaType); - - List rtpExtensions = intersectRTPExtensions( - offeredRTPExtensions, supportedExtensions); - - // transport - /* - * RawUdpTransportPacketExtension extends - * IceUdpTransportPacketExtension so getting - * IceUdpTransportPacketExtension should suffice. - */ - IceUdpTransportPacketExtension transport - = content.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - // stream target - MediaStreamTarget target = null; - - try - { - target = JingleUtils.extractDefaultTarget(content); - } - catch(IllegalArgumentException e) - { - logger.warn("Fail to extract default target", e); - } - - // according to XEP-176, transport element in session-initiate - // "MAY instead be empty (with each candidate to be sent as the - // payload of a transport-info message)". - int targetDataPort - = (target == null && transport != null) - ? -1 - : (target != null) ? target.getDataAddress().getPort() : 0; - - /* - * TODO If the offered transport is not supported, attempt to fall - * back to a supported one using transport-replace. - */ - setTransportManager(transport.getNamespace()); - - if (mutuallySupportedFormats.isEmpty() - || (devDirection == MediaDirection.INACTIVE) - || (targetDataPort == 0)) - { - // skip stream and continue. contrary to sip we don't seem to - // need to send per-stream disabling answer and only one at the - // end. - - //close the stream in case it already exists - closeStream(mediaType); - continue; - } - - SendersEnum senders = JingleUtils.getSenders( - direction, - !getPeer().isInitiator()); - // create the answer description - ContentPacketExtension ourContent - = JingleUtils.createDescription( - content.getCreator(), - content.getName(), - senders, - mutuallySupportedFormats, - rtpExtensions, - getDynamicPayloadTypes(), - getRtpExtensionsRegistry()); - - /* - * Sets ZRTP, SDES or DTLS-SRTP depending on the preferences for - * this account. - */ - setAndAddPreferredEncryptionProtocol( - mediaType, - ourContent, - content); - - // Got a content which has inputevt. It means that the peer requests - // a desktop sharing session so tell it we support inputevt. - if(content.getChildExtensionsOfType(InputEvtPacketExtension.class) - != null) - { - ourContent.addChildExtension(new InputEvtPacketExtension()); - } - - answer.add(ourContent); - localContentMap.put(content.getName(), ourContent); - - atLeastOneValidDescription = true; - } - - if (!atLeastOneValidDescription) - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "Offer contained no media formats" - + " or no valid media descriptions.", - OperationFailedException.ILLEGAL_ARGUMENT, - null, - logger); - } - - /* - * In order to minimize post-pickup delay, start establishing the - * connectivity prior to ringing. - */ - harvestCandidates( - offer, - answer, - new TransportInfoSender() - { - public void sendTransportInfo( - Iterable contents) - { - getPeer().sendTransportInfo(contents); - } - }); - - /* - * While it may sound like we can completely eliminate the post-pickup - * delay by waiting for the connectivity establishment to finish, it may - * not be possible in all cases. We are the Jingle session responder so, - * in the case of the ICE UDP transport, we are not the controlling ICE - * Agent and we cannot be sure when the controlling ICE Agent will - * perform the nomination. It could, for example, choose to wait for our - * session-accept to perform the nomination which will deadlock us if we - * have chosen to wait for the connectivity establishment to finish - * before we begin ringing and send session-accept. - */ - getTransportManager().startConnectivityEstablishment(offer); - } - - /** - * Processes the transport-related information provided by the remote - * peer in a specific set of ContentPacketExtensions. - * - * @param contents the ContentPacketExtenions provided by the - * remote peer and containing the transport-related information to - * be processed - * @throws OperationFailedException if anything goes wrong while processing - * the transport-related information provided by the remote peer in - * the specified set of ContentPacketExtensions - */ - public void processTransportInfo(Iterable contents) - throws OperationFailedException - { - if (getTransportManager().startConnectivityEstablishment(contents)) - { - //Emil: why the heck is this here and why is it commented? - //wrapupConnectivityEstablishment(); - } - } - - /** - * Reinitialize all media contents. - * - * @throws OperationFailedException if we fail to handle content - * for reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of content. Method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). - */ - public void reinitAllContents() - throws OperationFailedException, - IllegalArgumentException - { - boolean masterStreamSet = false; - for(String key : remoteContentMap.keySet()) - { - ContentPacketExtension ext = remoteContentMap.get(key); - - boolean masterStream = false; - // if we have more than one stream, lets the audio be the master - if(!masterStreamSet) - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(ext); - MediaType mediaType - = MediaType.parseString( description.getMedia() ); - - if(remoteContentMap.size() > 1) - { - if(mediaType.equals(MediaType.AUDIO)) - { - masterStream = true; - masterStreamSet = true; - } - } - else - { - masterStream = true; - masterStreamSet = true; - } - } - - if(ext != null) - processContent(ext, false, masterStream); - } - } - - /** - * Reinitialize a media content such as video. - * - * @param name name of the Jingle content - * @param content media content - * @param modify if it correspond to a content-modify for resolution change - * @throws OperationFailedException if we fail to handle content - * for reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of content. Method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). - */ - public void reinitContent( - String name, - ContentPacketExtension content, - boolean modify) - throws OperationFailedException, - IllegalArgumentException - { - ContentPacketExtension ext = remoteContentMap.get(name); - - if(ext != null) - { - if(modify) - { - processContent(content, modify, false); - remoteContentMap.put(name, content); - } - else - { - ext.setSenders(content.getSenders()); - processContent(ext, modify, false); - remoteContentMap.put(name, ext); - } - } - } - - /** - * Removes a media content with a specific name from the session represented - * by this CallPeerMediaHandlerJabberImpl and closes its associated - * media stream. - * - * @param contentMap the Map in which the specified name - * has an association with the media content to be removed - * @param name the name of the media content to be removed from this session - */ - private void removeContent( - Map contentMap, - String name) - { - ContentPacketExtension content = contentMap.remove(name); - - if (content != null) - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - String media = description.getMedia(); - - if (media != null) - closeStream(MediaType.parseString(media)); - } - } - - /** - * Removes a media content with a specific name from the session represented - * by this CallPeerMediaHandlerJabberImpl and closes its associated - * media stream. - * - * @param name the name of the media content to be removed from this session - */ - public void removeContent(String name) - { - removeContent(localContentMap, name); - removeContent(remoteContentMap, name); - - TransportManagerJabberImpl transportManager = queryTransportManager(); - - if (transportManager != null) - transportManager.removeContent(name); - } - - /** - * Acts upon a notification received from the remote party indicating that - * they've put us on/off hold. - * - * @param onHold true if the remote party has put us on hold - * and false if they've just put us off hold. - */ - public void setRemotelyOnHold(boolean onHold) - { - this.remotelyOnHold = onHold; - - for (MediaType mediaType : MediaType.values()) - { - MediaStream stream = getStream(mediaType); - - if (stream == null) - continue; - - if (getPeer().isJitsiVideobridge()) - { - /* - * If we are the focus of a Videobridge conference, we need to - * ask the Videobridge to change the stream direction on our - * behalf. - */ - ColibriConferenceIQ.Channel channel - = getColibriChannel(mediaType); - MediaDirection direction; - - if(remotelyOnHold) - { - direction = MediaDirection.INACTIVE; - } - else - { - // TODO Does SENDRECV always make sense? - direction = MediaDirection.SENDRECV; - } - getPeer().getCall().setChannelDirection( - channel.getID(), - mediaType, - direction); - } - else //no Videobridge - { - if (remotelyOnHold) - { - /* - * In conferences we use INACTIVE to prevent, for example, - * on-hold music from being played to all the participants. - */ - MediaDirection newDirection - = getPeer().getCall().isConferenceFocus() - ? MediaDirection.INACTIVE - : stream.getDirection().and( - MediaDirection.RECVONLY); - - stream.setDirection(newDirection); - } - else - { - stream.setDirection(calculatePostHoldDirection(stream)); - } - } - } - } - - /** - * Sometimes as initing a call with custom preset can set and we force - * that quality controls is supported. - * - * @param value whether quality controls is supported.. - */ - public void setSupportQualityControls(boolean value) - { - this.supportQualityControls = value; - } - - /** - * Sets the TransportManager implementation to handle our address - * management by Jingle transport XML namespace. - * - * @param xmlns the Jingle transport XML namespace specifying the - * TransportManager implementation type to be set on this instance - * to handle our address management - * @throws IllegalArgumentException if the specified xmlns does not - * specify a (supported) TransportManager implementation type - */ - private void setTransportManager(String xmlns) - throws IllegalArgumentException - { - // Is this really going to be an actual change? - if ((transportManager != null) - && transportManager.getXmlNamespace().equals(xmlns)) - { - return; - } - - CallPeerJabberImpl peer = getPeer(); - - if (!peer.getProtocolProvider().getDiscoveryManager().includesFeature( - xmlns)) - { - throw new IllegalArgumentException( - "Unsupported Jingle transport " + xmlns); - } - - /* - * TODO The transportManager is going to be changed so it may need to be - * disposed of prior to the change. - */ - - if (xmlns.equals( - ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1)) - { - transportManager = new IceUdpTransportManager(peer); - } - else if (xmlns.equals( - ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0)) - { - transportManager = new RawUdpTransportManager(peer); - } - else - { - throw new IllegalArgumentException( - "Unsupported Jingle transport " + xmlns); - } - - synchronized(transportManagerSyncRoot) - { - transportManagerSyncRoot.notify(); - } - } - - /** - * Waits for the associated TransportManagerJabberImpl to conclude - * any started connectivity establishment and then starts this - * CallPeerMediaHandler. - * - * @throws IllegalStateException if no offer or answer has been provided or - * generated earlier - */ - @Override - public void start() - throws IllegalStateException - { - try - { - wrapupConnectivityEstablishment(); - } - catch (OperationFailedException ofe) - { - throw new UndeclaredThrowableException(ofe); - } - - super.start(); - } - - /** - * Lets the underlying implementation take note of this error and only - * then throws it to the using bundles. - * - * @param message the message to be logged and then wrapped in a new - * OperationFailedException - * @param errorCode the error code to be assigned to the new - * OperationFailedException - * @param cause the Throwable that has caused the necessity to log - * an error and have a new OperationFailedException thrown - * - * @throws OperationFailedException the exception that we wanted this method - * to throw. - */ - @Override - protected void throwOperationFailedException( - String message, - int errorCode, - Throwable cause) - throws OperationFailedException - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - message, - errorCode, - cause, - logger); - } - - /** - * Notifies the associated TransportManagerJabberImpl that it - * should conclude any connectivity establishment, waits for it to actually - * do so and sets the connectors and targets of the - * MediaStreams managed by this CallPeerMediaHandler. - * - * @throws OperationFailedException if anything goes wrong while setting the - * connectors and/or targets of the MediaStreams - * managed by this CallPeerMediaHandler - */ - private void wrapupConnectivityEstablishment() - throws OperationFailedException - { - TransportManagerJabberImpl transportManager = getTransportManager(); - - transportManager.wrapupConnectivityEstablishment(); - for (MediaType mediaType : MediaType.values()) - { - MediaStream stream = getStream(mediaType); - - if (stream != null) - { - stream.setConnector( - transportManager.getStreamConnector(mediaType)); - stream.setTarget(transportManager.getStreamTarget(mediaType)); - } - } - } - - - - /** - * If Jitsi Videobridge is in use, returns the - * ColibriConferenceIQ.Channel that this - * CallPeerMediaHandler uses for media of type mediaType. - * Otherwise, returns null - * - * @param mediaType the MediaType for which to return a - * ColibriConferenceIQ.Channel - * @return the ColibriConferenceIQ.Channel that this - * CallPeerMediaHandler uses for media of type mediaType - * or null. - */ - private ColibriConferenceIQ.Channel getColibriChannel(MediaType mediaType) - { - ColibriConferenceIQ.Channel channel = null; - - if (getPeer().isJitsiVideobridge()) - { - TransportManagerJabberImpl transportManager = this.transportManager; - - if (transportManager != null) - { - channel - = transportManager.getColibriChannel( - mediaType, - false /* remote */); - } - } - return channel; - } - - /** - * {@inheritDoc} - * - * The super implementation relies on the direction of the streams and is - * therefore not accurate when we use a Videobridge. - */ - @Override - public boolean isRemotelyOnHold() - { - return remotelyOnHold; - } - - /** - * {@inheritDoc} - * - * Handles the case when a Videobridge is in use. - * - * @param locallyOnHold true if we are to make our streams - * stop transmitting and false if we are to start transmitting - */ - @Override - public void setLocallyOnHold(boolean locallyOnHold) - { - CallPeerJabberImpl peer = getPeer(); - - if (peer.isJitsiVideobridge()) - { - this.locallyOnHold = locallyOnHold; - - if (locallyOnHold - || !CallPeerState.ON_HOLD_MUTUALLY.equals(peer.getState())) - { - for (MediaType mediaType : MediaType.values()) - { - ColibriConferenceIQ.Channel channel - = getColibriChannel(mediaType); - - if (channel != null) - { - MediaDirection direction - = locallyOnHold - ? MediaDirection.INACTIVE - : MediaDirection.SENDRECV; - - peer.getCall().setChannelDirection( - channel.getID(), - mediaType, - direction); - } - } - } - } - else - { - super.setLocallyOnHold(locallyOnHold); - } - } - - /** - * Detects and adds DTLS-SRTP available encryption method present in the - * content (description) given in parameter. - * - * @param isInitiator true if the local call instance is the - * initiator of the call; false, otherwise. - * @param content The CONTENT element of the JINGLE element which contains - * the TRANSPORT element - * @param mediaType The type of media (AUDIO or VIDEO). - */ - private boolean addDtlsAdvertisedEncryptions( - boolean isInitiator, - ContentPacketExtension content, - MediaType mediaType) - { - if (getPeer().isJitsiVideobridge()) - { - // TODO Auto-generated method stub - return false; - } - else - { - IceUdpTransportPacketExtension remoteTransport - = content.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - return - addDtlsAdvertisedEncryptions( - isInitiator, - remoteTransport, - mediaType); - } - } - - /** - * Detects and adds DTLS-SRTP available encryption method present in the - * transport (description) given in parameter. - * - * @param isInitiator true if the local call instance is the - * initiator of the call; false, otherwise. - * @param remoteTransport the TRANSPORT element - * @param mediaType The type of media (AUDIO or VIDEO). - */ - boolean addDtlsAdvertisedEncryptions( - boolean isInitiator, - IceUdpTransportPacketExtension remoteTransport, - MediaType mediaType) - { - SrtpControls srtpControls = getSrtpControls(); - boolean b = false; - - if (remoteTransport != null) - { - List remoteFingerpintPEs - = remoteTransport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - - if (!remoteFingerpintPEs.isEmpty()) - { - AccountID accountID - = getPeer().getProtocolProvider().getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME)) - { - Map remoteFingerprints - = new LinkedHashMap(); - - for (DtlsFingerprintPacketExtension remoteFingerprintPE - : remoteFingerpintPEs) - { - String remoteFingerprint - = remoteFingerprintPE.getFingerprint(); - String remoteHash = remoteFingerprintPE.getHash(); - - remoteFingerprints.put( - remoteHash, - remoteFingerprint); - } - - DtlsControl dtlsControl; - DtlsControl.Setup setup; - - if (isInitiator) - { - dtlsControl - = (DtlsControl) - srtpControls.get( - mediaType, - SrtpControlType.DTLS_SRTP); - setup = DtlsControl.Setup.PASSIVE; - } - else - { - dtlsControl - = (DtlsControl) - srtpControls.getOrCreate( - mediaType, - SrtpControlType.DTLS_SRTP); - setup = DtlsControl.Setup.ACTIVE; - } - if (dtlsControl != null) - { - dtlsControl.setRemoteFingerprints(remoteFingerprints); - dtlsControl.setSetup(setup); - removeAndCleanupOtherSrtpControls( - mediaType, - SrtpControlType.DTLS_SRTP); - addAdvertisedEncryptionMethod( - SrtpControlType.DTLS_SRTP); - b = true; - } - } - } - } - /* - * If they haven't advertised DTLS-SRTP in their (media) description, - * then DTLS-SRTP shouldn't be functioning as far as we're concerned. - */ - if (!b) - { - SrtpControl dtlsControl - = srtpControls.get(mediaType, SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - srtpControls.remove(mediaType, SrtpControlType.DTLS_SRTP); - dtlsControl.cleanup(); - } - } - return b; - } - - /** - * Selects the preferred encryption protocol (only used by the callee). - * - * @param mediaType The type of media (AUDIO or VIDEO). - * @param localContent The element containing the media DESCRIPTION and - * its encryption. - * @param remoteContent The element containing the media DESCRIPTION and - * its encryption for the remote peer; null if the local peer is - * the initiator of the call. - */ - private void setAndAddPreferredEncryptionProtocol( - MediaType mediaType, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent) - { +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.jabber; + +import java.awt.*; +import java.beans.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.List; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.ContentPacketExtension.SendersEnum; +import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.libjitsi.*; +import org.jitsi.service.neomedia.*; +import org.jitsi.service.neomedia.device.*; +import org.jitsi.service.neomedia.format.*; +import org.jivesoftware.smackx.packet.*; + +/** + * An XMPP specific extension of the generic media handler. + * + * @author Emil Ivov + * @author Lyubomir Marinov + * @author Hristo Terezov + * @author Boris Grozev + */ +public class CallPeerMediaHandlerJabberImpl + extends AbstractCallPeerMediaHandlerJabberGTalkImpl +{ + /** + * The Logger used by the CallPeerMediaHandlerJabberImpl + * class and its instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(CallPeerMediaHandlerJabberImpl.class); + + /** + * Determines whether a specific XMPP feature is supported by both a + * specific ScServiceDiscoveryManager (may be referred to as the + * local peer) and a specific DiscoverInfo (may be thought of as + * the remote peer). + * + * @param discoveryManager the ScServiceDiscoveryManager to be + * checked whether it includes the specified feature + * @param discoverInfo the DiscoveryInfo which is to be checked + * whether it contains the specified feature. If discoverInfo is + * null, it is considered to contain the specified feature. + * @param feature the feature to be determined whether it is supported by + * both the specified discoveryManager and the specified + * discoverInfo + * @return true if the specified feature is supported by + * both the specified discoveryManager and the specified + * discoverInfo; otherwise, false + */ + private static boolean isFeatureSupported( + ScServiceDiscoveryManager discoveryManager, + DiscoverInfo discoverInfo, + String feature) + { + return + discoveryManager.includesFeature(feature) + && ((discoverInfo == null) + || discoverInfo.containsFeature(feature)); + } + + /** + * The current description of the streams that we have going toward the + * remote side. We use {@link LinkedHashMap}s to make sure that we preserve + * the order of the individual content extensions. + */ + private final Map localContentMap + = new LinkedHashMap(); + + /** + * The QualityControl of this CallPeerMediaHandler. + */ + private final QualityControlWrapper qualityControls; + + /** + * The current description of the streams that the remote side has with us. + * We use {@link LinkedHashMap}s to make sure that we preserve + * the order of the individual content extensions. + */ + private final Map remoteContentMap + = new LinkedHashMap(); + + /** + * Indicates whether the remote party has placed us on hold. + */ + private boolean remotelyOnHold = false; + + /** + * Whether other party is able to change video quality settings. + * Normally its whether we have detected existence of imageattr in sdp. + */ + private boolean supportQualityControls = false; + + /** + * The TransportManager implementation handling our address + * management. + */ + private TransportManagerJabberImpl transportManager; + + /** + * The Object which is used for synchronization (e.g. wait + * and notify) related to {@link #transportManager}. + */ + private final Object transportManagerSyncRoot = new Object(); + + /** + * The ordered by preference array of the XML namespaces of the jingle + * transports that this peer supports. If it is non-null, it will be used + * instead of checking disco#info in order to select an appropriate + * transport manager. + */ + private String[] supportedTransports = null; + + /** + * Object used to synchronize access to supportedTransports + */ + private final Object supportedTransportsSyncRoot = new Object(); + + /** + * Creates a new handler that will be managing media streams for + * peer. + * + * @param peer that CallPeerJabberImpl instance that we will be + * managing media for. + */ + public CallPeerMediaHandlerJabberImpl(CallPeerJabberImpl peer) + { + super(peer); + + qualityControls = new QualityControlWrapper(peer); + } + + /** + * Determines the direction that a stream, which has been placed on + * hold by the remote party, would need to go back to after being + * re-activated. If the stream is not currently on hold (i.e. it is still + * sending media), this method simply returns its current direction. + * + * @param stream the {@link MediaStreamTarget} whose post-hold direction + * we'd like to determine. + * + * @return the {@link MediaDirection} that we need to set on stream + * once it is reactivate. + */ + private MediaDirection calculatePostHoldDirection(MediaStream stream) + { + MediaDirection streamDirection = stream.getDirection(); + + if (streamDirection.allowsSending()) + return streamDirection; + + /* + * When calculating a direction we need to take into account 1) what + * direction the remote party had asked for before putting us on hold, + * 2) what the user preference is for the stream's media type, 3) our + * local hold status, 4) the direction supported by the device this + * stream is reading from. + */ + + // 1. what the remote party originally told us (from our perspective) + ContentPacketExtension content = remoteContentMap.get(stream.getName()); + MediaDirection postHoldDir + = JingleUtils.getDirection(content, !getPeer().isInitiator()); + + // 2. the user preference + MediaDevice device = stream.getDevice(); + + postHoldDir + = postHoldDir.and( + getDirectionUserPreference(device.getMediaType())); + + // 3. our local hold status + if (isLocallyOnHold()) + postHoldDir = postHoldDir.and(MediaDirection.SENDONLY); + + // 4. the device direction + postHoldDir = postHoldDir.and(device.getDirection()); + + return postHoldDir; + } + + /** + * Closes the CallPeerMediaHandler. + */ + @Override + public synchronized void close() + { + super.close(); + + OperationSetDesktopSharingClientJabberImpl client + = (OperationSetDesktopSharingClientJabberImpl) + getPeer().getProtocolProvider().getOperationSet( + OperationSetDesktopSharingClient.class); + + if (client != null) + client.fireRemoteControlRevoked(getPeer()); + } + + /** + * Creates a {@link ContentPacketExtension}s of the streams for a + * specific MediaDevice. + * + * @param dev MediaDevice + * @return the {@link ContentPacketExtension}s of stream that this + * handler is prepared to initiate. + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like problems with device interaction, allocating ports, etc. + */ + private ContentPacketExtension createContent(MediaDevice dev) + throws OperationFailedException + { + MediaType mediaType = dev.getMediaType(); + //this is the direction to be used in the jingle session + MediaDirection direction = dev.getDirection(); + CallPeerJabberImpl peer = getPeer(); + + /* + * In the case of RTP translation performed by the conference focus, + * the conference focus is not required to capture media. + */ + if (!(MediaType.VIDEO.equals(mediaType) + && isRTPTranslationEnabled(mediaType))) + direction = direction.and(getDirectionUserPreference(mediaType)); + + /* + * Check if we need to announce sending on behalf of other peers + */ + CallJabberImpl call = peer.getCall(); + + if (call.isConferenceFocus()) + { + for (CallPeerJabberImpl anotherPeer : call.getCallPeerList()) + { + if ((anotherPeer != peer) + && anotherPeer.getDirection(mediaType) + .allowsReceiving()) + { + direction = direction.or(MediaDirection.SENDONLY); + break; + } + } + } + + if (isLocallyOnHold()) + direction = direction.and(MediaDirection.SENDONLY); + + QualityPreset sendQualityPreset = null; + QualityPreset receiveQualityPreset = null; + + if(qualityControls != null) + { + // the one we will send is the one the remote has announced as + // receive + sendQualityPreset = qualityControls.getRemoteReceivePreset(); + // the one we want to receive is the one the remote can send + receiveQualityPreset = qualityControls.getRemoteSendMaxPreset(); + } + if(direction != MediaDirection.INACTIVE) + { + ContentPacketExtension content + = createContentForOffer( + getLocallySupportedFormats( + dev, + sendQualityPreset, + receiveQualityPreset), + direction, + dev.getSupportedExtensions()); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + + // DTLS-SRTP + setDtlsEncryptionOnContent(mediaType, content, null); + /* + * Neither SDES nor ZRTP is supported in telephony conferences + * utilizing the server-side technology Jitsi Videobridge yet. + */ + if (!call.getConference().isJitsiVideobridge()) + { + // SDES + // It is important to set SDES before ZRTP in order to make + // GTALK application able to work with SDES. + setSDesEncryptionOnDescription(mediaType, description, null); + // ZRTP + setZrtpEncryptionOnDescription(mediaType, description, null); + } + + return content; + } + return null; + } + + /** + * Creates a {@link ContentPacketExtension} for a particular stream. + * + * @param mediaType MediaType of the content + * @return a {@link ContentPacketExtension} + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like - problems with device interaction, allocating ports, + * etc. + */ + public ContentPacketExtension createContentForMedia(MediaType mediaType) + throws OperationFailedException + { + MediaDevice dev = getDefaultDevice(mediaType); + + if(isDeviceActive(dev)) + return createContent(dev); + return null; + } + + /** + * Generates an Jingle {@link ContentPacketExtension} for the specified + * {@link MediaFormat} list, direction and RTP extensions taking account + * the local streaming preference for the corresponding media type. + * + * @param supportedFormats the list of MediaFormats that we'd + * like to advertise. + * @param direction the MediaDirection that we'd like to establish + * the stream in. + * @param supportedExtensions the list of RTPExtensions that we'd + * like to advertise in the MediaDescription. + * + * @return a newly created {@link ContentPacketExtension} representing + * streams that we'd be able to handle. + */ + private ContentPacketExtension createContentForOffer( + List supportedFormats, + MediaDirection direction, + List supportedExtensions) + { + ContentPacketExtension content + = JingleUtils.createDescription( + ContentPacketExtension.CreatorEnum.initiator, + supportedFormats.get(0).getMediaType().toString(), + JingleUtils.getSenders(direction, !getPeer().isInitiator()), + supportedFormats, + supportedExtensions, + getDynamicPayloadTypes(), + getRtpExtensionsRegistry()); + + this.localContentMap.put(content.getName(), content); + return content; + } + + /** + * Creates a List containing the {@link ContentPacketExtension}s of + * the streams that this handler is prepared to initiate depending on + * available MediaDevices and local on-hold and video transmission + * preferences. + * + * @return a {@link List} containing the {@link ContentPacketExtension}s of + * streams that this handler is prepared to initiate. + * + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like problems with device interaction, allocating ports, etc. + */ + public List createContentList() + throws OperationFailedException + { + // Describe the media. + List mediaDescs + = new ArrayList(); + boolean jitsiVideobridge + = getPeer().getCall().getConference().isJitsiVideobridge(); + + for (MediaType mediaType : MediaType.values()) + { + MediaDevice dev = getDefaultDevice(mediaType); + + if (isDeviceActive(dev)) + { + MediaDirection direction = dev.getDirection(); + + /* + * In the case of RTP translation performed by the conference + * focus, the conference focus is not required to capture media. + */ + if (!(MediaType.VIDEO.equals(mediaType) + && isRTPTranslationEnabled(mediaType))) + { + direction + = direction.and(getDirectionUserPreference(mediaType)); + } + if (isLocallyOnHold()) + direction = direction.and(MediaDirection.SENDONLY); + + /* + * If we're only able to receive, we don't have to offer it at + * all. For example, we have to offer audio and no video when we + * start an audio call. + */ + if (MediaDirection.RECVONLY.equals(direction)) + direction = MediaDirection.INACTIVE; + + if (direction != MediaDirection.INACTIVE) + { + ContentPacketExtension content + = createContentForOffer( + getLocallySupportedFormats(dev), + direction, + dev.getSupportedExtensions()); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + + // DTLS-SRTP + setDtlsEncryptionOnContent(mediaType, content, null); + /* + * Neither SDES nor ZRTP is supported in telephony + * conferences utilizing the server-side technology Jitsi + * Videobridge yet. + */ + if (!jitsiVideobridge) + { + // SDES + // It is important to set SDES before ZRTP in order to + // make GTALK application able to work with SDES. + setSDesEncryptionOnDescription( + mediaType, + description, + null); + //ZRTP + setZrtpEncryptionOnDescription( + mediaType, + description, + null); + } + + // we request a desktop sharing session so add the inputevt + // extension in the "video" content + if (description.getMedia().equals( + MediaType.VIDEO.toString()) + && getLocalInputEvtAware()) + { + content.addChildExtension( + new InputEvtPacketExtension()); + } + + mediaDescs.add(content); + } + } + } + + // Fail if no media is described (e.g. all devices are inactive). + if (mediaDescs.isEmpty()) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "We couldn't find any active Audio/Video devices" + + " and couldn't create a call", + OperationFailedException.GENERAL_ERROR, + null, + logger); + } + + // Describe the transport(s). + return harvestCandidates(null, mediaDescs, null); + } + + /** + * Creates a List containing the {@link ContentPacketExtension}s of + * the streams of a specific MediaType that this handler is + * prepared to initiate depending on available MediaDevices and + * local on-hold and video transmission preferences. + * + * @param mediaType MediaType of the content + * @return a {@link List} containing the {@link ContentPacketExtension}s of + * streams that this handler is prepared to initiate. + * + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like - problems with device interaction, allocating ports, + * etc. + */ + public List createContentList(MediaType mediaType) + throws OperationFailedException + { + MediaDevice dev = getDefaultDevice(mediaType); + List mediaDescs + = new ArrayList(); + + if (isDeviceActive(dev)) + { + ContentPacketExtension content = createContent(dev); + + if (content != null) + mediaDescs.add(content); + } + + // Fail if no media is described (e.g. all devices are inactive). + if (mediaDescs.isEmpty()) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "We couldn't find any active Audio/Video devices and " + + "couldn't create a call", + OperationFailedException.GENERAL_ERROR, + null, + logger); + } + + // Describe the transport(s). + return harvestCandidates(null, mediaDescs, null); + } + + /** + * Overrides to give access to the transport manager to send events + * about ICE state changes. + * + * @param property the name of the property of this + * PropertyChangeNotifier which had its value changed + * @param oldValue the value of the property with the specified name before + * the change + * @param newValue the value of the property with the specified name after + */ + @Override + protected void firePropertyChange( + String property, + Object oldValue, Object newValue) + { + super.firePropertyChange(property, oldValue, newValue); + } + + /** + * Wraps up any ongoing candidate harvests and returns our response to the + * last offer we've received, so that the peer could use it to send a + * session-accept. + * + * @return the last generated list of {@link ContentPacketExtension}s that + * the call peer could use to send a session-accept. + * + * @throws OperationFailedException if we fail to configure the media stream + */ + public Iterable generateSessionAccept() + throws OperationFailedException + { + TransportManagerJabberImpl transportManager = getTransportManager(); + Iterable sessAccept + = transportManager.wrapupCandidateHarvest(); + CallPeerJabberImpl peer = getPeer(); + + //user answered an incoming call so we go through whatever content + //entries we are initializing and init their corresponding streams + + // First parse content so we know how many streams and what type of + // content we have + Map contents + = new HashMap(); + + for(ContentPacketExtension ourContent : sessAccept) + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(ourContent); + + contents.put(ourContent, description); + } + + boolean masterStreamSet = false; + + for(Map.Entry en + : contents.entrySet()) + { + ContentPacketExtension ourContent = en.getKey(); + + RtpDescriptionPacketExtension description = en.getValue(); + MediaType type = MediaType.parseString(description.getMedia()); + + // stream connector + StreamConnector connector + = transportManager.getStreamConnector(type); + + //the device this stream would be reading from and writing to. + MediaDevice dev = getDefaultDevice(type); + + if(!isDeviceActive(dev)) + continue; + + // stream target + MediaStreamTarget target = transportManager.getStreamTarget(type); + + //stream direction + MediaDirection direction + = JingleUtils.getDirection(ourContent, !peer.isInitiator()); + + // if we answer with video, tell remote peer that video direction is + // sendrecv, and whether video device can capture/send + if (MediaType.VIDEO.equals(type) + && (isLocalVideoTransmissionEnabled() + || isRTPTranslationEnabled(type)) + && dev.getDirection().allowsSending()) + { + direction = MediaDirection.SENDRECV; + ourContent.setSenders(ContentPacketExtension.SendersEnum.both); + } + + //let's now see what was the format we announced as first and + //configure the stream with it. + String contentName = ourContent.getName(); + ContentPacketExtension theirContent + = this.remoteContentMap.get(contentName); + RtpDescriptionPacketExtension theirDescription + = JingleUtils.getRtpDescription(theirContent); + MediaFormat format = null; + + List localFormats = getLocallySupportedFormats(dev); + for(PayloadTypePacketExtension payload + : theirDescription.getPayloadTypes()) + { + MediaFormat remoteFormat = JingleUtils.payloadTypeToMediaFormat( + payload, getDynamicPayloadTypes()); + if(remoteFormat != null + && (format = findMediaFormat(localFormats, remoteFormat)) + != null) + break; + } + + if(format == null) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "No matching codec.", + OperationFailedException.ILLEGAL_ARGUMENT, + null, + logger); + } + + //extract the extensions that we are advertising: + // check whether we will be exchanging any RTP extensions. + List rtpExtensions + = JingleUtils.extractRTPExtensions( + description, + this.getRtpExtensionsRegistry()); + + Map adv = format.getAdvancedAttributes(); + if(adv != null) + { + for(Map.Entry f : adv.entrySet()) + { + if(f.getKey().equals("imageattr")) + supportQualityControls = true; + } + } + + boolean masterStream = false; + // if we have more than one stream, lets the audio be the master + if(!masterStreamSet) + { + if(contents.size() > 1) + { + if(type.equals(MediaType.AUDIO)) + { + masterStream = true; + masterStreamSet = true; + } + } + else + { + masterStream = true; + masterStreamSet = true; + } + } + + // create the corresponding stream... + MediaStream stream = initStream(contentName, + connector, + dev, + format, + target, + direction, + rtpExtensions, + masterStream); + + long ourSsrc = stream.getLocalSourceID() & 0xffffffffL; + if (direction.allowsSending() && ourSsrc != -1) + { + description.setSsrc(Long.toString(ourSsrc)); + addSourceExtension(description, ourSsrc); + } + } + return sessAccept; + } + + /** + * Adds a SourcePacketExtension as a child element of + * description. See XEP-0339. + * + * @param description the RtpDescriptionPacketExtension to which + * a child element will be added. + * @param ssrc the SSRC for the SourcePacketExtension to use. + */ + private void addSourceExtension(RtpDescriptionPacketExtension description, + long ssrc) + { + MediaType type = MediaType.parseString(description.getMedia()); + + SourcePacketExtension sourcePacketExtension + = new SourcePacketExtension(); + + sourcePacketExtension.setSSRC(ssrc); + sourcePacketExtension.addChildExtension( + new ParameterPacketExtension("cname", + LibJitsi.getMediaService() + .getRtpCname())); + sourcePacketExtension.addChildExtension( + new ParameterPacketExtension("msid", getMsid(type))); + sourcePacketExtension.addChildExtension( + new ParameterPacketExtension("mslabel", getMsLabel())); + sourcePacketExtension.addChildExtension( + new ParameterPacketExtension("label", getLabel(type))); + + description.addChildExtension(sourcePacketExtension); + } + + /** + * Returns the local content of a specific content type (like audio or + * video). + * + * @param contentType content type name + * @return remote ContentPacketExtension or null if not found + */ + public ContentPacketExtension getLocalContent(String contentType) + { + for(String key : localContentMap.keySet()) + { + ContentPacketExtension content = localContentMap.get(key); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + + if(description.getMedia().equals(contentType)) + return content; + } + return null; + } + + /** + * Returns a complete list of call currently known local content-s. + * + * @return a list of {@link ContentPacketExtension} null if not found + */ + public Iterable getLocalContentList() + { + return localContentMap.values(); + } + + /** + * Returns the quality control for video calls if any. + * + * @return the implemented quality control. + */ + public QualityControl getQualityControl() + { + if(supportQualityControls) + { + return qualityControls; + } + else + { + // we have detected that its not supported and return null + // and control ui won't be visible + return null; + } + } + + /** + * Get the remote content of a specific content type (like audio or video). + * + * @param contentType content type name + * @return remote ContentPacketExtension or null if not found + */ + public ContentPacketExtension getRemoteContent(String contentType) + { + for(String key : remoteContentMap.keySet()) + { + ContentPacketExtension content = remoteContentMap.get(key); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + + if(description.getMedia().equals(contentType)) + return content; + } + return null; + } + + /** + * {@inheritDoc} + * + * In the case of a telephony conference organized by the local peer/user + * via the Jitsi Videobridge server-side technology, returns an SSRC + * reported by the server as received on the channel allocated by the local + * peer/user for the purposes of communicating with the CallPeer + * associated with this instance. + */ + @Override + public long getRemoteSSRC(MediaType mediaType) + { + int[] ssrcs = getRemoteSSRCs(mediaType); + + /* + * A peer (regardless of whether it is local or remote) may send + * multiple RTP streams at any time. In such a case, it is not clear + * which one of their SSRCs is to be returned. Anyway, the super says + * that the returned is the last known. We will presume that the last + * known in the list reported by the Jitsi Videobridge server is the + * last. + */ + if (ssrcs.length != 0) + return 0xFFFFFFFFL & ssrcs[ssrcs.length - 1]; + + /* + * XXX In the case of Jitsi Videobridge, the super implementation of + * getRemoteSSRC(MediaType) cannot be trusted because there is a single + * VideoMediaStream with multiple ReceiveStreams. + */ + return + getPeer().isJitsiVideobridge() + ? SSRC_UNKNOWN + : super.getRemoteSSRC(mediaType); + } + + /** + * Gets the SSRCs of RTP streams with a specific MediaType known to + * be received by a MediaStream associated with this instance. + *

+ * Warning: The method may return only one of the many possible + * remote SSRCs in the case of no utilization of the Jitsi Videobridge + * server-side technology because the super implementation does not + * currently provide support for keeping track of multiple remote SSRCs. + *

+ * + * @param mediaType the MediaType of the RTP streams the SSRCs of + * which are to be returned + * @return an array of int values which represent the SSRCs of RTP + * streams with the specified mediaType known to be received by a + * MediaStream associated with this instance + */ + private int[] getRemoteSSRCs(MediaType mediaType) + { + /* + * If the Jitsi Videobridge server-side technology is utilized, a single + * MediaStream (per MediaType) is shared among the participating + * CallPeers and, consequently, the remote SSRCs cannot be associated + * with the CallPeers from which they are actually being sent. That's + * why the server will report them to the conference focus. + */ + ColibriConferenceIQ.Channel channel = getColibriChannel(mediaType); + + if (channel != null) + return channel.getSSRCs(); + + /* + * XXX The fallback to the super implementation that follows may lead to + * unexpected behavior due to the lack of ability to keep track of + * multiple remote SSRCs. + */ + long ssrc = super.getRemoteSSRC(mediaType); + + return + (ssrc == SSRC_UNKNOWN) + ? ColibriConferenceIQ.NO_SSRCS + : new int[] { (int) ssrc }; + } + + /** + * Gets the TransportManager implementation handling our address + * management. + * + * TODO: this method can and should be simplified. + * + * @return the TransportManager implementation handling our address + * management + * @see CallPeerMediaHandler#getTransportManager() + */ + @Override + protected synchronized TransportManagerJabberImpl getTransportManager() + { + if (transportManager == null) + { + CallPeerJabberImpl peer = getPeer(); + + if (peer.isInitiator()) + { + synchronized(transportManagerSyncRoot) + { + try + { + transportManagerSyncRoot.wait(5000); + } + catch(InterruptedException e) + { + } + } + if(transportManager == null) + { + throw new IllegalStateException( + "The initiator is expected to specify the transport" + + " in their offer."); + } + else + return transportManager; + } + else + { + ProtocolProviderServiceJabberImpl protocolProvider + = peer.getProtocolProvider(); + ScServiceDiscoveryManager discoveryManager + = protocolProvider.getDiscoveryManager(); + DiscoverInfo peerDiscoverInfo = peer.getDiscoveryInfo(); + + /* + * If this.supportedTransports has been explicitly set, we use + * it to select the transport manager -- we use the first + * transport in the list which we recognize (e.g. the first + * that is either ice or raw-udp + */ + synchronized (supportedTransportsSyncRoot) + { + if (supportedTransports != null + && supportedTransports.length > 0) + { + for (int i = 0; i < supportedTransports.length; i++) + { + if (ProtocolProviderServiceJabberImpl. + URN_XMPP_JINGLE_ICE_UDP_1. + equals(supportedTransports[i])) + { + transportManager + = new IceUdpTransportManager(peer); + break; + } + else if (ProtocolProviderServiceJabberImpl. + URN_XMPP_JINGLE_RAW_UDP_0. + equals(supportedTransports[i])) + { + transportManager + = new RawUdpTransportManager(peer); + break; + } + } + if (transportManager == null) + { + logger.warn( + "Could not find a supported" + + " TransportManager in" + + " supportedTransports. Will try to" + + " select one based on disco#info."); + } + } + } + + if (transportManager == null) + { + /* + * The list of possible transports ordered by decreasing + * preference. + */ + String[] transports + = new String[] + { + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1, + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_RAW_UDP_0 + }; + + /* + * If Jitsi Videobridge is to be employed, pick up a Jingle + * transport supported by it. + */ + if (peer.isJitsiVideobridge()) + { + CallJabberImpl call = peer.getCall(); + + if (call != null) + { + String jitsiVideobridge + = peer.getCall().getJitsiVideobridge(); + + /* + * Jitsi Videobridge supports the Jingle Raw UDP + * transport from its inception. But that is not the + * case with the Jingle ICE-UDP transport. + */ + if ((jitsiVideobridge != null) + && !protocolProvider.isFeatureSupported( + jitsiVideobridge, + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1)) + { + for (int i = transports.length - 1; i >= 0; i--) + { + if (ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1 + .equals(transports[i])) + { + transports[i] = null; + } + } + } + } + } + + /* + * Select the first transport from the list of possible + * transports ordered by decreasing preference which is + * supported by the local and the remote peers. + */ + for (String transport : transports) + { + if (transport == null) + continue; + if (isFeatureSupported( + discoveryManager, + peerDiscoverInfo, + transport)) + { + if (ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1 + .equals(transport)) + { + transportManager + = new IceUdpTransportManager(peer); + } + else if (ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_RAW_UDP_0 + .equals(transport)) + { + transportManager + = new RawUdpTransportManager(peer); + } + + if (transportManager != null) + break; + } + } + + if ((transportManager == null) && logger.isDebugEnabled()) + { + logger.debug( + "No known Jingle transport supported by Jabber" + + " call peer " + peer); + } + } + } + } + return transportManager; + } + + /** + * {@inheritDoc} + * + * @see CallPeerMediaHandler#queryTransportManager() + */ + @Override + protected synchronized TransportManagerJabberImpl queryTransportManager() + { + return transportManager; + } + + /** + * {@inheritDoc} + * + * In the case of utilization of the Jitsi Videobridge server-side + * technology, returns the visual Components which display RTP + * video streams reported by the server to be sent by the remote peer + * represented by this instance. + */ + @Override + public List getVisualComponents() + { + /* + * TODO The super is currently unable to provide the complete set of + * remote SSRCs (i.e. in the case of no utilization of the Jitsi + * Videobridge server-side technology) so we have to explicitly check + * for Jitsi Videobridge instead of just relying on the implementation + * of the getRemoteSSRCs(MediaType) method to abstract away that detail. + */ + CallJabberImpl call; + MediaAwareCallConference conference; + + if (((call = getPeer().getCall()) != null) + && ((conference = call.getConference()) != null) + && conference.isJitsiVideobridge()) + { + MediaStream stream = getStream(MediaType.VIDEO); + + if (stream == null) + return Collections.emptyList(); + else + { + int[] remoteSSRCs = getRemoteSSRCs(MediaType.VIDEO); + + if (remoteSSRCs.length == 0) + return Collections.emptyList(); + else + { + VideoMediaStream videoStream = (VideoMediaStream) stream; + List visualComponents + = new LinkedList(); + + for (int i = 0; i < remoteSSRCs.length; i++) + { + int remoteSSRC = remoteSSRCs[i]; + Component visualComponent + = videoStream.getVisualComponent( + 0xFFFFFFFFL & remoteSSRC); + + if (visualComponent != null) + visualComponents.add(visualComponent); + } + return visualComponents; + } + } + } + + return super.getVisualComponents(); + } + + /** + * Gathers local candidate addresses. + * + * @param remote the media descriptions received from the remote peer if any + * or null if local represents an offer from the local + * peer to be sent to the remote peer + * @param local the media descriptions sent or to be sent from the local + * peer to the remote peer. If remote is null, + * local represents an offer from the local peer to be sent to the + * remote peer + * @param transportInfoSender the TransportInfoSender to be used by + * this TransportManagerJabberImpl to send transport-info + * JingleIQs from the local peer to the remote peer if this + * TransportManagerJabberImpl wishes to utilize + * transport-info + * @return the media descriptions of the local peer after the local + * candidate addresses have been gathered as returned by + * {@link TransportManagerJabberImpl#wrapupCandidateHarvest()} + * @throws OperationFailedException if anything goes wrong while starting or + * wrapping up the gathering of local candidate addresses + */ + private List harvestCandidates( + List remote, + List local, + TransportInfoSender transportInfoSender) + throws OperationFailedException + { + long startCandidateHarvestTime = System.currentTimeMillis(); + TransportManagerJabberImpl transportManager = getTransportManager(); + + if (remote == null) + { + /* + * We'll be harvesting candidates in order to make an offer so it + * doesn't make sense to send them in transport-info. + */ + if (transportInfoSender != null) + throw new IllegalArgumentException("transportInfoSender"); + + transportManager.startCandidateHarvest(local, transportInfoSender); + } + else + { + transportManager.startCandidateHarvest( + remote, + local, + transportInfoSender); + } + + long stopCandidateHarvestTime = System.currentTimeMillis(); + + if (logger.isInfoEnabled()) + { + long candidateHarvestTime + = stopCandidateHarvestTime - startCandidateHarvestTime; + + logger.info( + "End candidate harvest within " + candidateHarvestTime + + " ms"); + } + + setDtlsEncryptionOnTransports(remote, local); + + if (transportManager.startConnectivityEstablishmentWithJitsiVideobridge) + { + Map map + = new LinkedHashMap(); + + for (MediaType mediaType : MediaType.values()) + { + ColibriConferenceIQ.Channel channel + = transportManager.getColibriChannel( + mediaType, + true /* local */); + + if (channel != null) + { + IceUdpTransportPacketExtension transport + = channel.getTransport(); + + if (transport != null) + map.put(mediaType.toString(), transport); + } + } + if (!map.isEmpty()) + { + transportManager + .startConnectivityEstablishmentWithJitsiVideobridge + = false; + transportManager.startConnectivityEstablishment(map); + } + } + + /* + * TODO Ideally, we wouldn't wrap up that quickly. We need to revisit + * this. + */ + return transportManager.wrapupCandidateHarvest(); + } + + /** + * Creates if necessary, and configures the stream that this + * MediaHandler is using for the MediaType matching the + * one of the MediaDevice. This method extends the one already + * available by adding a stream name, corresponding to a stream's content + * name. + * + * @param streamName the name of the stream as indicated in the XMPP + * content element. + * @param connector the MediaConnector that we'd like to bind the + * newly created stream to. + * @param device the MediaDevice that we'd like to attach the newly + * created MediaStream to. + * @param format the MediaFormat that we'd like the new + * MediaStream to be set to transmit in. + * @param target the MediaStreamTarget containing the RTP and RTCP + * address:port couples that the new stream would be sending packets to. + * @param direction the MediaDirection that we'd like the new + * stream to use (i.e. sendonly, sendrecv, recvonly, or inactive). + * @param rtpExtensions the list of RTPExtensions that should be + * enabled for this stream. + * @param masterStream whether the stream to be used as master if secured + * + * @return the newly created MediaStream. + * + * @throws OperationFailedException if creating the stream fails for any + * reason (like for example accessing the device or setting the format). + */ + protected MediaStream initStream(String streamName, + StreamConnector connector, + MediaDevice device, + MediaFormat format, + MediaStreamTarget target, + MediaDirection direction, + List rtpExtensions, + boolean masterStream) + throws OperationFailedException + { + MediaStream stream + = super.initStream( + connector, + device, + format, + target, + direction, + rtpExtensions, + masterStream); + + if (stream != null) + stream.setName(streamName); + + return stream; + } + + /** + * {@inheritDoc} + * + * In the case of a telephony conference organized by the local peer/user + * and utilizing the Jitsi Videobridge server-side technology, a single + * MediaHandler is shared by multiple + * CallPeerMediaHandlers in order to have a single + * AudioMediaStream and a single VideoMediaStream. + * However, CallPeerMediaHandlerJabberImpl has redefined the + * reading/getting the remote audio and video SSRCs. Consequently, + * CallPeerMediaHandlerJabberImpl has to COMPLETELY redefine the + * writing/setting as well i.e. it has to stop related + * PropertyChangeEvents fired by the super. + */ + @Override + protected void mediaHandlerPropertyChange(PropertyChangeEvent ev) + { + String propertyName = ev.getPropertyName(); + + if ((AUDIO_REMOTE_SSRC.equals(propertyName) + || VIDEO_REMOTE_SSRC.equals(propertyName)) + && getPeer().isJitsiVideobridge()) + return; + + super.mediaHandlerPropertyChange(ev); + } + + /** + * Handles the specified answer by creating and initializing the + * corresponding MediaStreams. + * + * @param answer the Jingle answer + * + * @throws OperationFailedException if we fail to handle answer for + * reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of answer. Method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + public void processAnswer(List answer) + throws OperationFailedException, + IllegalArgumentException + { + /* + * The answer given in session-accept may contain transport-related + * information compatible with that carried in transport-info. + */ + processTransportInfo(answer); + + boolean masterStreamSet = false; + + for (ContentPacketExtension content : answer) + { + remoteContentMap.put(content.getName(), content); + + boolean masterStream = false; + + // if we have more than one stream, let the audio be the master + if(!masterStreamSet) + { + if(answer.size() > 1) + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + + if(MediaType.AUDIO.toString().equals( + description.getMedia())) + { + masterStream = true; + masterStreamSet = true; + } + } + else + { + masterStream = true; + masterStreamSet = true; + } + } + + processContent(content, false, masterStream); + } + } + + /** + * Notifies this instance that a specific ColibriConferenceIQ has + * been received. This CallPeerMediaHandler uses the part of the + * information provided in the specified conferenceIQ which + * concerns it only. + * + * @param conferenceIQ the ColibriConferenceIQ which has been + * received + */ + void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) + { + /* + * This CallPeerMediaHandler stores the media information but it does + * not store the colibri Channels (which contain both media and transport + * information). The TransportManager associated with this instance + * stores the colibri Channels but does not store media information (such + * as the remote SSRCs). An design/implementation choice has to be made + * though and the present one is to have this CallPeerMediaHandler + * transparently (with respect to the TransportManager) store the media + * information inside the TransportManager. + */ + TransportManagerJabberImpl transportManager = this.transportManager; + + if (transportManager != null) + { + long oldAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); + long oldVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); + + for (MediaType mediaType : MediaType.values()) + { + ColibriConferenceIQ.Channel dst + = transportManager.getColibriChannel( + mediaType, + false /* remote */); + + if (dst != null) + { + ColibriConferenceIQ.Content content + = conferenceIQ.getContent(mediaType.toString()); + + if (content != null) + { + ColibriConferenceIQ.Channel src + = content.getChannel(dst.getID()); + + if (src != null) + { + int[] ssrcs = src.getSSRCs(); + int[] dstSSRCs = dst.getSSRCs(); + + if (!Arrays.equals(dstSSRCs, ssrcs)) + dst.setSSRCs(ssrcs); + } + } + } + } + + /* + * Do fire new PropertyChangeEvents for the properties + * AUDIO_REMOTE_SSRC and VIDEO_REMOTE_SSRC if necessary. + */ + long newAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); + long newVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); + + if (oldAudioRemoteSSRC != newAudioRemoteSSRC) + { + firePropertyChange( + AUDIO_REMOTE_SSRC, + oldAudioRemoteSSRC, newAudioRemoteSSRC); + } + if (oldVideoRemoteSSRC != newVideoRemoteSSRC) + { + firePropertyChange( + VIDEO_REMOTE_SSRC, + oldVideoRemoteSSRC, newVideoRemoteSSRC); + } + } + } + + /** + * Process a ContentPacketExtension and initialize its + * corresponding MediaStream. + * + * @param content a ContentPacketExtension + * @param modify if it correspond to a content-modify for resolution change + * @param masterStream whether the stream to be used as master + * @throws OperationFailedException if we fail to handle content + * for reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of content. The method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + private void processContent( + ContentPacketExtension content, + boolean modify, + boolean masterStream) + throws OperationFailedException, + IllegalArgumentException + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + MediaType mediaType + = MediaType.parseString(description.getMedia()); + + //stream target + TransportManagerJabberImpl transportManager = getTransportManager(); + MediaStreamTarget target = transportManager.getStreamTarget(mediaType); + + if (target == null) + target = JingleUtils.extractDefaultTarget(content); + + // no target port - try next media description + if((target == null) || (target.getDataAddress().getPort() == 0)) + { + closeStream(mediaType); + return; + } + + List supportedFormats = JingleUtils.extractFormats( + description, getDynamicPayloadTypes()); + + MediaDevice dev = getDefaultDevice(mediaType); + + if(!isDeviceActive(dev)) + { + closeStream(mediaType); + return; + } + + MediaDirection devDirection + = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); + + // Take the preference of the user with respect to streaming + // mediaType into account. + devDirection + = devDirection.and(getDirectionUserPreference(mediaType)); + + if (supportedFormats.isEmpty()) + { + //remote party must have messed up our Jingle description. + //throw an exception. + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "Remote party sent an invalid Jingle answer.", + OperationFailedException.ILLEGAL_ARGUMENT, + null, + logger); + } + + CallJabberImpl call = getPeer().getCall(); + CallConference conference + = (call == null) ? null : call.getConference(); + + /* + * Neither SDES nor ZRTP is supported in telephony conferences utilizing + * the server-side technology Jitsi Videobridge yet. + */ + if ((conference == null) || !conference.isJitsiVideobridge()) + { + addZrtpAdvertisedEncryptions(true, description, mediaType); + addSDesAdvertisedEncryptions(true, description, mediaType); + } + addDtlsAdvertisedEncryptions(true, content, mediaType); + + StreamConnector connector + = transportManager.getStreamConnector(mediaType); + + //determine the direction that we need to announce. + MediaDirection remoteDirection + = JingleUtils.getDirection(content, getPeer().isInitiator()); + /* + * If we are the focus of a conference, we need to take into account the + * other participants. + */ + if ((conference != null) && conference.isConferenceFocus()) + { + for (CallPeerJabberImpl peer : call.getCallPeerList()) + { + SendersEnum senders + = peer.getSenders(mediaType); + boolean initiator = peer.isInitiator(); + //check if the direction of the jingle session we have with + //this peer allows us receiving media. If senders is null, + //assume the default of 'both' + if ((senders == null) + || (SendersEnum.both == senders) + || (initiator && SendersEnum.initiator == senders) + || (!initiator && SendersEnum.responder == senders)) + { + remoteDirection + = remoteDirection.or(MediaDirection.SENDONLY); + } + } + } + + MediaDirection direction + = devDirection.getDirectionForAnswer(remoteDirection); + + // update the RTP extensions that we will be exchanging. + List remoteRTPExtensions + = JingleUtils.extractRTPExtensions( + description, + getRtpExtensionsRegistry()); + List supportedExtensions + = getExtensionsForType(mediaType); + List rtpExtensions + = intersectRTPExtensions(remoteRTPExtensions, supportedExtensions); + + Map adv + = supportedFormats.get(0).getAdvancedAttributes(); + + if(adv != null) + { + for(Map.Entry f : adv.entrySet()) + { + if(f.getKey().equals("imageattr")) + supportQualityControls = true; + } + } + + // check for options from remote party and set them locally + if(mediaType.equals(MediaType.VIDEO) && modify) + { + // update stream + MediaStream stream = getStream(MediaType.VIDEO); + + if(stream != null && dev != null) + { + List fmts = supportedFormats; + + if(fmts.size() > 0) + { + MediaFormat fmt = fmts.get(0); + + ((VideoMediaStream)stream).updateQualityControl( + fmt.getAdvancedAttributes()); + } + } + + if(qualityControls != null) + { + QualityPreset receiveQualityPreset + = qualityControls.getRemoteReceivePreset(); + QualityPreset sendQualityPreset + = qualityControls.getRemoteSendMaxPreset(); + + supportedFormats + = (dev == null) + ? null + : intersectFormats( + supportedFormats, + getLocallySupportedFormats( + dev, + sendQualityPreset, + receiveQualityPreset)); + } + } + + // create the corresponding stream... + initStream( + content.getName(), + connector, + dev, + supportedFormats.get(0), + target, + direction, + rtpExtensions, + masterStream); + } + + /** + * Parses and handles the specified offer and returns a content + * extension representing the current state of this media handler. This + * method MUST only be called when offer is the first session + * description that this MediaHandler is seeing. + * + * @param offer the offer that we'd like to parse, handle and get an answer + * for. + * + * @throws OperationFailedException if we have a problem satisfying the + * description received in offer (e.g. failed to open a device or + * initialize a stream ...). + * @throws IllegalArgumentException if there's a problem with + * offer's format or semantics. + */ + public void processOffer(List offer) + throws OperationFailedException, + IllegalArgumentException + { + // prepare to generate answers to all the incoming descriptions + List answer + = new ArrayList(offer.size()); + boolean atLeastOneValidDescription = false; + + for (ContentPacketExtension content : offer) + { + remoteContentMap.put(content.getName(), content); + + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + MediaType mediaType + = MediaType.parseString( description.getMedia() ); + + List remoteFormats + = JingleUtils.extractFormats( + description, + getDynamicPayloadTypes()); + + MediaDevice dev = getDefaultDevice(mediaType); + + MediaDirection devDirection + = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); + + // Take the preference of the user with respect to streaming + // mediaType into account. + devDirection + = devDirection.and(getDirectionUserPreference(mediaType)); + + // determine the direction that we need to announce. + MediaDirection remoteDirection = JingleUtils.getDirection( + content, getPeer().isInitiator()); + MediaDirection direction + = devDirection.getDirectionForAnswer(remoteDirection); + + // intersect the MediaFormats of our device with remote ones + List mutuallySupportedFormats + = intersectFormats( + remoteFormats, + getLocallySupportedFormats(dev)); + + // check whether we will be exchanging any RTP extensions. + List offeredRTPExtensions + = JingleUtils.extractRTPExtensions( + description, this.getRtpExtensionsRegistry()); + + List supportedExtensions + = getExtensionsForType(mediaType); + + List rtpExtensions = intersectRTPExtensions( + offeredRTPExtensions, supportedExtensions); + + // transport + /* + * RawUdpTransportPacketExtension extends + * IceUdpTransportPacketExtension so getting + * IceUdpTransportPacketExtension should suffice. + */ + IceUdpTransportPacketExtension transport + = content.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + // stream target + MediaStreamTarget target = null; + + try + { + target = JingleUtils.extractDefaultTarget(content); + } + catch(IllegalArgumentException e) + { + logger.warn("Fail to extract default target", e); + } + + // according to XEP-176, transport element in session-initiate + // "MAY instead be empty (with each candidate to be sent as the + // payload of a transport-info message)". + int targetDataPort + = (target == null && transport != null) + ? -1 + : (target != null) ? target.getDataAddress().getPort() : 0; + + /* + * TODO If the offered transport is not supported, attempt to fall + * back to a supported one using transport-replace. + */ + setTransportManager(transport.getNamespace()); + + if (mutuallySupportedFormats.isEmpty() + || (devDirection == MediaDirection.INACTIVE) + || (targetDataPort == 0)) + { + // skip stream and continue. contrary to sip we don't seem to + // need to send per-stream disabling answer and only one at the + // end. + + //close the stream in case it already exists + closeStream(mediaType); + continue; + } + + SendersEnum senders = JingleUtils.getSenders( + direction, + !getPeer().isInitiator()); + // create the answer description + ContentPacketExtension ourContent + = JingleUtils.createDescription( + content.getCreator(), + content.getName(), + senders, + mutuallySupportedFormats, + rtpExtensions, + getDynamicPayloadTypes(), + getRtpExtensionsRegistry()); + + /* + * Sets ZRTP, SDES or DTLS-SRTP depending on the preferences for + * this account. + */ + setAndAddPreferredEncryptionProtocol( + mediaType, + ourContent, + content); + + // Got a content which has inputevt. It means that the peer requests + // a desktop sharing session so tell it we support inputevt. + if(content.getChildExtensionsOfType(InputEvtPacketExtension.class) + != null) + { + ourContent.addChildExtension(new InputEvtPacketExtension()); + } + + answer.add(ourContent); + localContentMap.put(content.getName(), ourContent); + + atLeastOneValidDescription = true; + } + + if (!atLeastOneValidDescription) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "Offer contained no media formats" + + " or no valid media descriptions.", + OperationFailedException.ILLEGAL_ARGUMENT, + null, + logger); + } + + /* + * In order to minimize post-pickup delay, start establishing the + * connectivity prior to ringing. + */ + harvestCandidates( + offer, + answer, + new TransportInfoSender() + { + public void sendTransportInfo( + Iterable contents) + { + getPeer().sendTransportInfo(contents); + } + }); + + /* + * While it may sound like we can completely eliminate the post-pickup + * delay by waiting for the connectivity establishment to finish, it may + * not be possible in all cases. We are the Jingle session responder so, + * in the case of the ICE UDP transport, we are not the controlling ICE + * Agent and we cannot be sure when the controlling ICE Agent will + * perform the nomination. It could, for example, choose to wait for our + * session-accept to perform the nomination which will deadlock us if we + * have chosen to wait for the connectivity establishment to finish + * before we begin ringing and send session-accept. + */ + getTransportManager().startConnectivityEstablishment(offer); + } + + /** + * Processes the transport-related information provided by the remote + * peer in a specific set of ContentPacketExtensions. + * + * @param contents the ContentPacketExtenions provided by the + * remote peer and containing the transport-related information to + * be processed + * @throws OperationFailedException if anything goes wrong while processing + * the transport-related information provided by the remote peer in + * the specified set of ContentPacketExtensions + */ + public void processTransportInfo(Iterable contents) + throws OperationFailedException + { + if (getTransportManager().startConnectivityEstablishment(contents)) + { + //Emil: why the heck is this here and why is it commented? + //wrapupConnectivityEstablishment(); + } + } + + /** + * Reinitialize all media contents. + * + * @throws OperationFailedException if we fail to handle content + * for reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of content. Method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + public void reinitAllContents() + throws OperationFailedException, + IllegalArgumentException + { + boolean masterStreamSet = false; + for(String key : remoteContentMap.keySet()) + { + ContentPacketExtension ext = remoteContentMap.get(key); + + boolean masterStream = false; + // if we have more than one stream, lets the audio be the master + if(!masterStreamSet) + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(ext); + MediaType mediaType + = MediaType.parseString( description.getMedia() ); + + if(remoteContentMap.size() > 1) + { + if(mediaType.equals(MediaType.AUDIO)) + { + masterStream = true; + masterStreamSet = true; + } + } + else + { + masterStream = true; + masterStreamSet = true; + } + } + + if(ext != null) + processContent(ext, false, masterStream); + } + } + + /** + * Reinitialize a media content such as video. + * + * @param name name of the Jingle content + * @param content media content + * @param modify if it correspond to a content-modify for resolution change + * @throws OperationFailedException if we fail to handle content + * for reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of content. Method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + public void reinitContent( + String name, + ContentPacketExtension content, + boolean modify) + throws OperationFailedException, + IllegalArgumentException + { + ContentPacketExtension ext = remoteContentMap.get(name); + + if(ext != null) + { + if(modify) + { + processContent(content, modify, false); + remoteContentMap.put(name, content); + } + else + { + ext.setSenders(content.getSenders()); + processContent(ext, modify, false); + remoteContentMap.put(name, ext); + } + } + } + + /** + * Removes a media content with a specific name from the session represented + * by this CallPeerMediaHandlerJabberImpl and closes its associated + * media stream. + * + * @param contentMap the Map in which the specified name + * has an association with the media content to be removed + * @param name the name of the media content to be removed from this session + */ + private void removeContent( + Map contentMap, + String name) + { + ContentPacketExtension content = contentMap.remove(name); + + if (content != null) + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + String media = description.getMedia(); + + if (media != null) + closeStream(MediaType.parseString(media)); + } + } + + /** + * Removes a media content with a specific name from the session represented + * by this CallPeerMediaHandlerJabberImpl and closes its associated + * media stream. + * + * @param name the name of the media content to be removed from this session + */ + public void removeContent(String name) + { + removeContent(localContentMap, name); + removeContent(remoteContentMap, name); + + TransportManagerJabberImpl transportManager = queryTransportManager(); + + if (transportManager != null) + transportManager.removeContent(name); + } + + /** + * Acts upon a notification received from the remote party indicating that + * they've put us on/off hold. + * + * @param onHold true if the remote party has put us on hold + * and false if they've just put us off hold. + */ + public void setRemotelyOnHold(boolean onHold) + { + this.remotelyOnHold = onHold; + + for (MediaType mediaType : MediaType.values()) + { + MediaStream stream = getStream(mediaType); + + if (stream == null) + continue; + + if (getPeer().isJitsiVideobridge()) + { + /* + * If we are the focus of a Videobridge conference, we need to + * ask the Videobridge to change the stream direction on our + * behalf. + */ + ColibriConferenceIQ.Channel channel + = getColibriChannel(mediaType); + MediaDirection direction; + + if(remotelyOnHold) + { + direction = MediaDirection.INACTIVE; + } + else + { + // TODO Does SENDRECV always make sense? + direction = MediaDirection.SENDRECV; + } + getPeer().getCall().setChannelDirection( + channel.getID(), + mediaType, + direction); + } + else //no Videobridge + { + if (remotelyOnHold) + { + /* + * In conferences we use INACTIVE to prevent, for example, + * on-hold music from being played to all the participants. + */ + MediaDirection newDirection + = getPeer().getCall().isConferenceFocus() + ? MediaDirection.INACTIVE + : stream.getDirection().and( + MediaDirection.RECVONLY); + + stream.setDirection(newDirection); + } + else + { + stream.setDirection(calculatePostHoldDirection(stream)); + } + } + } + } + + /** + * Sometimes as initing a call with custom preset can set and we force + * that quality controls is supported. + * + * @param value whether quality controls is supported.. + */ + public void setSupportQualityControls(boolean value) + { + this.supportQualityControls = value; + } + + /** + * Sets the TransportManager implementation to handle our address + * management by Jingle transport XML namespace. + * + * @param xmlns the Jingle transport XML namespace specifying the + * TransportManager implementation type to be set on this instance + * to handle our address management + * @throws IllegalArgumentException if the specified xmlns does not + * specify a (supported) TransportManager implementation type + */ + private void setTransportManager(String xmlns) + throws IllegalArgumentException + { + // Is this really going to be an actual change? + if ((transportManager != null) + && transportManager.getXmlNamespace().equals(xmlns)) + { + return; + } + + CallPeerJabberImpl peer = getPeer(); + + if (!peer.getProtocolProvider().getDiscoveryManager().includesFeature( + xmlns)) + { + throw new IllegalArgumentException( + "Unsupported Jingle transport " + xmlns); + } + + /* + * TODO The transportManager is going to be changed so it may need to be + * disposed of prior to the change. + */ + + if (xmlns.equals( + ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1)) + { + transportManager = new IceUdpTransportManager(peer); + } + else if (xmlns.equals( + ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0)) + { + transportManager = new RawUdpTransportManager(peer); + } + else + { + throw new IllegalArgumentException( + "Unsupported Jingle transport " + xmlns); + } + + synchronized(transportManagerSyncRoot) + { + transportManagerSyncRoot.notify(); + } + } + + /** + * Waits for the associated TransportManagerJabberImpl to conclude + * any started connectivity establishment and then starts this + * CallPeerMediaHandler. + * + * @throws IllegalStateException if no offer or answer has been provided or + * generated earlier + */ + @Override + public void start() + throws IllegalStateException + { + try + { + wrapupConnectivityEstablishment(); + } + catch (OperationFailedException ofe) + { + throw new UndeclaredThrowableException(ofe); + } + + super.start(); + } + + /** + * Lets the underlying implementation take note of this error and only + * then throws it to the using bundles. + * + * @param message the message to be logged and then wrapped in a new + * OperationFailedException + * @param errorCode the error code to be assigned to the new + * OperationFailedException + * @param cause the Throwable that has caused the necessity to log + * an error and have a new OperationFailedException thrown + * + * @throws OperationFailedException the exception that we wanted this method + * to throw. + */ + @Override + protected void throwOperationFailedException( + String message, + int errorCode, + Throwable cause) + throws OperationFailedException + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + message, + errorCode, + cause, + logger); + } + + /** + * Notifies the associated TransportManagerJabberImpl that it + * should conclude any connectivity establishment, waits for it to actually + * do so and sets the connectors and targets of the + * MediaStreams managed by this CallPeerMediaHandler. + * + * @throws OperationFailedException if anything goes wrong while setting the + * connectors and/or targets of the MediaStreams + * managed by this CallPeerMediaHandler + */ + private void wrapupConnectivityEstablishment() + throws OperationFailedException + { + TransportManagerJabberImpl transportManager = getTransportManager(); + + transportManager.wrapupConnectivityEstablishment(); + for (MediaType mediaType : MediaType.values()) + { + MediaStream stream = getStream(mediaType); + + if (stream != null) + { + stream.setConnector( + transportManager.getStreamConnector(mediaType)); + stream.setTarget(transportManager.getStreamTarget(mediaType)); + } + } + } + + + + /** + * If Jitsi Videobridge is in use, returns the + * ColibriConferenceIQ.Channel that this + * CallPeerMediaHandler uses for media of type mediaType. + * Otherwise, returns null + * + * @param mediaType the MediaType for which to return a + * ColibriConferenceIQ.Channel + * @return the ColibriConferenceIQ.Channel that this + * CallPeerMediaHandler uses for media of type mediaType + * or null. + */ + private ColibriConferenceIQ.Channel getColibriChannel(MediaType mediaType) + { + ColibriConferenceIQ.Channel channel = null; + + if (getPeer().isJitsiVideobridge()) + { + TransportManagerJabberImpl transportManager = this.transportManager; + + if (transportManager != null) + { + channel + = transportManager.getColibriChannel( + mediaType, + false /* remote */); + } + } + return channel; + } + + /** + * {@inheritDoc} + * + * The super implementation relies on the direction of the streams and is + * therefore not accurate when we use a Videobridge. + */ + @Override + public boolean isRemotelyOnHold() + { + return remotelyOnHold; + } + + /** + * {@inheritDoc} + * + * Handles the case when a Videobridge is in use. + * + * @param locallyOnHold true if we are to make our streams + * stop transmitting and false if we are to start transmitting + */ + @Override + public void setLocallyOnHold(boolean locallyOnHold) + { + CallPeerJabberImpl peer = getPeer(); + + if (peer.isJitsiVideobridge()) + { + this.locallyOnHold = locallyOnHold; + + if (locallyOnHold + || !CallPeerState.ON_HOLD_MUTUALLY.equals(peer.getState())) + { + for (MediaType mediaType : MediaType.values()) + { + ColibriConferenceIQ.Channel channel + = getColibriChannel(mediaType); + + if (channel != null) + { + MediaDirection direction + = locallyOnHold + ? MediaDirection.INACTIVE + : MediaDirection.SENDRECV; + + peer.getCall().setChannelDirection( + channel.getID(), + mediaType, + direction); + } + } + } + } + else + { + super.setLocallyOnHold(locallyOnHold); + } + } + + /** + * Detects and adds DTLS-SRTP available encryption method present in the + * content (description) given in parameter. + * + * @param isInitiator true if the local call instance is the + * initiator of the call; false, otherwise. + * @param content The CONTENT element of the JINGLE element which contains + * the TRANSPORT element + * @param mediaType The type of media (AUDIO or VIDEO). + */ + private boolean addDtlsAdvertisedEncryptions( + boolean isInitiator, + ContentPacketExtension content, + MediaType mediaType) + { + if (getPeer().isJitsiVideobridge()) + { + // TODO Auto-generated method stub + return false; + } + else + { + IceUdpTransportPacketExtension remoteTransport + = content.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + return + addDtlsAdvertisedEncryptions( + isInitiator, + remoteTransport, + mediaType); + } + } + + /** + * Detects and adds DTLS-SRTP available encryption method present in the + * transport (description) given in parameter. + * + * @param isInitiator true if the local call instance is the + * initiator of the call; false, otherwise. + * @param remoteTransport the TRANSPORT element + * @param mediaType The type of media (AUDIO or VIDEO). + */ + boolean addDtlsAdvertisedEncryptions( + boolean isInitiator, + IceUdpTransportPacketExtension remoteTransport, + MediaType mediaType) + { + SrtpControls srtpControls = getSrtpControls(); + boolean b = false; + + if (remoteTransport != null) + { + List remoteFingerpintPEs + = remoteTransport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + + if (!remoteFingerpintPEs.isEmpty()) + { + AccountID accountID + = getPeer().getProtocolProvider().getAccountID(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + SrtpControlType.DTLS_SRTP)) + { + Map remoteFingerprints + = new LinkedHashMap(); + + for (DtlsFingerprintPacketExtension remoteFingerprintPE + : remoteFingerpintPEs) + { + String remoteFingerprint + = remoteFingerprintPE.getFingerprint(); + String remoteHash = remoteFingerprintPE.getHash(); + + remoteFingerprints.put( + remoteHash, + remoteFingerprint); + } + + DtlsControl dtlsControl; + DtlsControl.Setup setup; + + if (isInitiator) + { + dtlsControl + = (DtlsControl) + srtpControls.get( + mediaType, + SrtpControlType.DTLS_SRTP); + setup = DtlsControl.Setup.PASSIVE; + } + else + { + dtlsControl + = (DtlsControl) + srtpControls.getOrCreate( + mediaType, + SrtpControlType.DTLS_SRTP); + setup = DtlsControl.Setup.ACTIVE; + } + if (dtlsControl != null) + { + dtlsControl.setRemoteFingerprints(remoteFingerprints); + dtlsControl.setSetup(setup); + removeAndCleanupOtherSrtpControls( + mediaType, + SrtpControlType.DTLS_SRTP); + addAdvertisedEncryptionMethod( + SrtpControlType.DTLS_SRTP); + b = true; + } + } + } + } + /* + * If they haven't advertised DTLS-SRTP in their (media) description, + * then DTLS-SRTP shouldn't be functioning as far as we're concerned. + */ + if (!b) + { + SrtpControl dtlsControl + = srtpControls.get(mediaType, SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + srtpControls.remove(mediaType, SrtpControlType.DTLS_SRTP); + dtlsControl.cleanup(); + } + } + return b; + } + + /** + * Selects the preferred encryption protocol (only used by the callee). + * + * @param mediaType The type of media (AUDIO or VIDEO). + * @param localContent The element containing the media DESCRIPTION and + * its encryption. + * @param remoteContent The element containing the media DESCRIPTION and + * its encryption for the remote peer; null if the local peer is + * the initiator of the call. + */ + private void setAndAddPreferredEncryptionProtocol( + MediaType mediaType, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent) + { List preferredEncryptionProtocols - = getPeer() - .getProtocolProvider() - .getAccountID() - .getSortedEnabledEncryptionProtocolList(); - + = getPeer() + .getProtocolProvider() + .getAccountID() + .getSortedEnabledEncryptionProtocolList(); + for (SrtpControlType srtpControlType : preferredEncryptionProtocols) - { - // DTLS-SRTP + { + // DTLS-SRTP if (srtpControlType == SrtpControlType.DTLS_SRTP) - { - addDtlsAdvertisedEncryptions( - false, - remoteContent, - mediaType); - if (setDtlsEncryptionOnContent( - mediaType, - localContent, - remoteContent)) - { - // Stop once an encryption advertisement has been chosen. - return; - } - } - else - { - RtpDescriptionPacketExtension localDescription - = (localContent == null) - ? null - : JingleUtils.getRtpDescription(localContent); - RtpDescriptionPacketExtension remoteDescription - = (remoteContent == null) - ? null - : JingleUtils.getRtpDescription(remoteContent); - - if (setAndAddPreferredEncryptionProtocol( + { + addDtlsAdvertisedEncryptions( + false, + remoteContent, + mediaType); + if (setDtlsEncryptionOnContent( + mediaType, + localContent, + remoteContent)) + { + // Stop once an encryption advertisement has been chosen. + return; + } + } + else + { + RtpDescriptionPacketExtension localDescription + = (localContent == null) + ? null + : JingleUtils.getRtpDescription(localContent); + RtpDescriptionPacketExtension remoteDescription + = (remoteContent == null) + ? null + : JingleUtils.getRtpDescription(remoteContent); + + if (setAndAddPreferredEncryptionProtocol( srtpControlType, - mediaType, - localDescription, - remoteDescription)) - { - // Stop once an encryption advertisement has been chosen. - return; - } - } - } - } - - /** - * Sets DTLS-SRTP element(s) to the TRANSPORT element of the CONTENT for a - * given media. - * - * @param mediaType The type of media we are modifying the CONTENT to - * integrate the DTLS-SRTP element(s). - * @param localContent The element containing the media CONTENT and its - * TRANSPORT. - * @param remoteContent The element containing the media CONTENT and its - * TRANSPORT for the remote peer. Null, if the local peer is the initiator - * of the call. - * @return true if any DTLS-SRTP element has been added to the - * specified localContent; false, otherwise. - */ - private boolean setDtlsEncryptionOnContent( - MediaType mediaType, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent) - { - CallPeerJabberImpl peer = getPeer(); - boolean b = false; - - if (peer.isJitsiVideobridge()) - { - b - = setDtlsEncryptionOnTransport( - mediaType, - localContent, - remoteContent); - return b; - } - - ProtocolProviderServiceJabberImpl protocolProvider - = peer.getProtocolProvider(); - AccountID accountID = protocolProvider.getAccountID(); - SrtpControls srtpControls = getSrtpControls(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME)) - { - boolean addFingerprintToLocalTransport; - - if (remoteContent == null) // initiator - { - addFingerprintToLocalTransport - = protocolProvider.isFeatureSupported( - peer.getAddress(), - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_DTLS_SRTP); - } - else // responder - { - addFingerprintToLocalTransport - = addDtlsAdvertisedEncryptions( - false, - remoteContent, - mediaType); - } - if (addFingerprintToLocalTransport) - { - DtlsControl dtlsControl - = (DtlsControl) - srtpControls.getOrCreate( - mediaType, - SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - DtlsControl.Setup setup - = (remoteContent == null) - ? DtlsControl.Setup.PASSIVE - : DtlsControl.Setup.ACTIVE; - - dtlsControl.setSetup(setup); - b = true; - - setDtlsEncryptionOnTransport( - mediaType, - localContent, - remoteContent); - } - } - } - /* - * If we haven't advertised DTLS-SRTP in our (media) description, then - * DTLS-SRTP shouldn't be functioning as far as we're concerned. - */ - if (!b) - { - SrtpControl dtlsControl - = srtpControls.get(mediaType, SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - srtpControls.remove(mediaType, SrtpControlType.DTLS_SRTP); - dtlsControl.cleanup(); - } - } - return b; - } - - /** - * Sets DTLS-SRTP element(s) to the TRANSPORT element of the CONTENT for a - * given media. - * - * @param mediaType The type of media we are modifying the CONTENT to - * integrate the DTLS-SRTP element(s). - * @param localContent The element containing the media CONTENT and its - * TRANSPORT. - */ - private boolean setDtlsEncryptionOnTransport( - MediaType mediaType, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent) - { - IceUdpTransportPacketExtension localTransport - = localContent.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - boolean b = false; - - if (localTransport == null) - return b; - - CallPeerJabberImpl peer = getPeer(); - - if (peer.isJitsiVideobridge()) - { - ProtocolProviderServiceJabberImpl protocolProvider - = peer.getProtocolProvider(); - AccountID accountID = protocolProvider.getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME)) - { - // Gather the local fingerprints to be sent to the remote peer. - ColibriConferenceIQ.Channel channel - = getColibriChannel(mediaType); - List localFingerprints = null; - - if (channel != null) - { - IceUdpTransportPacketExtension transport - = channel.getTransport(); - - if (transport != null) - { - localFingerprints - = transport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - } - } - - /* - * Determine whether the local fingerprints are to be sent to - * the remote peer. - */ - if ((localFingerprints != null) && !localFingerprints.isEmpty()) - { - if (remoteContent == null) // initiator - { - if (!protocolProvider.isFeatureSupported( - peer.getAddress(), - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_DTLS_SRTP)) - { - localFingerprints = null; - } - } - else // responder - { - IceUdpTransportPacketExtension transport - = remoteContent.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - if (transport == null) - { - localFingerprints = null; - } - else - { - List - remoteFingerprints - = transport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension - .class); - - if (remoteFingerprints.isEmpty()) - localFingerprints = null; - } - } - // Send the local fingerprints to the remote peer. - if (localFingerprints != null) - { - List fingerprintPEs - = localTransport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - - if (fingerprintPEs.isEmpty()) - { - for (DtlsFingerprintPacketExtension localFingerprint - : localFingerprints) - { - DtlsFingerprintPacketExtension fingerprintPE - = new DtlsFingerprintPacketExtension(); - - fingerprintPE.setFingerprint( - localFingerprint.getFingerprint()); - fingerprintPE.setHash( - localFingerprint.getHash()); - localTransport.addChildExtension(fingerprintPE); - } - } - b = true; - } - } - } - } - else - { - SrtpControls srtpControls = getSrtpControls(); - DtlsControl dtlsControl - = (DtlsControl) - srtpControls.get(mediaType, SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - CallJabberImpl.setDtlsEncryptionOnTransport( - dtlsControl, - localTransport); - b = true; - } - } - return b; - } - - /** - * Sets DTLS-SRTP element(s) to the TRANSPORT element of a specified list of - * CONTENT elements. - * - * @param localContents The elements containing the media CONTENT elements - * and their respective TRANSPORT elements. - */ - private void setDtlsEncryptionOnTransports( - List remoteContents, - List localContents) - { - for (ContentPacketExtension localContent : localContents) - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(localContent); - - if (description != null) - { - MediaType mediaType = JingleUtils.getMediaType(localContent); - - if (mediaType != null) - { - ContentPacketExtension remoteContent - = (remoteContents == null) - ? null - : TransportManagerJabberImpl.findContentByName( - remoteContents, - localContent.getName()); - - setDtlsEncryptionOnTransport( - mediaType, - localContent, - remoteContent); - } - } - } - } - - /** - * Sets the jingle transports that this - * CallPeerMediaHandlerJabberImpl supports. Unknown transports are - * ignored, and the transports Collection is put into - * order depending on local preference. - * - * Currently only ice and raw-udp are recognized, with ice being preffered - * over raw-udp - * - * @param transports A Collection of XML namespaces of jingle - * transport elements to be set as the supported jingle transports for this - * CallPeerMediaHandlerJabberImpl - */ - public void setSupportedTransports(Collection transports) - { - if (transports == null) - return; - - String ice - = ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1; - String rawUdp - = ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0; - - int size = 0; - for(String transport : transports) - if (ice.equals(transport) || rawUdp.equals(transport)) - size++; - - if (size > 0) - { - synchronized (supportedTransportsSyncRoot) - { - supportedTransports = new String[size]; - int i = 0; - - // we prefer ice over raw-udp - if (transports.contains(ice)) - { - supportedTransports[i] = ice; - i++; - } - - if (transports.contains(rawUdp)) - { - supportedTransports[i] = rawUdp; - i++; - } - } - } - } -} + mediaType, + localDescription, + remoteDescription)) + { + // Stop once an encryption advertisement has been chosen. + return; + } + } + } + } + + /** + * Sets DTLS-SRTP element(s) to the TRANSPORT element of the CONTENT for a + * given media. + * + * @param mediaType The type of media we are modifying the CONTENT to + * integrate the DTLS-SRTP element(s). + * @param localContent The element containing the media CONTENT and its + * TRANSPORT. + * @param remoteContent The element containing the media CONTENT and its + * TRANSPORT for the remote peer. Null, if the local peer is the initiator + * of the call. + * @return true if any DTLS-SRTP element has been added to the + * specified localContent; false, otherwise. + */ + private boolean setDtlsEncryptionOnContent( + MediaType mediaType, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent) + { + CallPeerJabberImpl peer = getPeer(); + boolean b = false; + + if (peer.isJitsiVideobridge()) + { + b + = setDtlsEncryptionOnTransport( + mediaType, + localContent, + remoteContent); + return b; + } + + ProtocolProviderServiceJabberImpl protocolProvider + = peer.getProtocolProvider(); + AccountID accountID = protocolProvider.getAccountID(); + SrtpControls srtpControls = getSrtpControls(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + SrtpControlType.DTLS_SRTP)) + { + boolean addFingerprintToLocalTransport; + + if (remoteContent == null) // initiator + { + addFingerprintToLocalTransport + = protocolProvider.isFeatureSupported( + peer.getAddress(), + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_DTLS_SRTP); + } + else // responder + { + addFingerprintToLocalTransport + = addDtlsAdvertisedEncryptions( + false, + remoteContent, + mediaType); + } + if (addFingerprintToLocalTransport) + { + DtlsControl dtlsControl + = (DtlsControl) + srtpControls.getOrCreate( + mediaType, + SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + DtlsControl.Setup setup + = (remoteContent == null) + ? DtlsControl.Setup.PASSIVE + : DtlsControl.Setup.ACTIVE; + + dtlsControl.setSetup(setup); + b = true; + + setDtlsEncryptionOnTransport( + mediaType, + localContent, + remoteContent); + } + } + } + /* + * If we haven't advertised DTLS-SRTP in our (media) description, then + * DTLS-SRTP shouldn't be functioning as far as we're concerned. + */ + if (!b) + { + SrtpControl dtlsControl + = srtpControls.get(mediaType, SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + srtpControls.remove(mediaType, SrtpControlType.DTLS_SRTP); + dtlsControl.cleanup(); + } + } + return b; + } + + /** + * Sets DTLS-SRTP element(s) to the TRANSPORT element of the CONTENT for a + * given media. + * + * @param mediaType The type of media we are modifying the CONTENT to + * integrate the DTLS-SRTP element(s). + * @param localContent The element containing the media CONTENT and its + * TRANSPORT. + */ + private boolean setDtlsEncryptionOnTransport( + MediaType mediaType, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent) + { + IceUdpTransportPacketExtension localTransport + = localContent.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + boolean b = false; + + if (localTransport == null) + return b; + + CallPeerJabberImpl peer = getPeer(); + + if (peer.isJitsiVideobridge()) + { + ProtocolProviderServiceJabberImpl protocolProvider + = peer.getProtocolProvider(); + AccountID accountID = protocolProvider.getAccountID(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + SrtpControlType.DTLS_SRTP)) + { + // Gather the local fingerprints to be sent to the remote peer. + ColibriConferenceIQ.Channel channel + = getColibriChannel(mediaType); + List localFingerprints = null; + + if (channel != null) + { + IceUdpTransportPacketExtension transport + = channel.getTransport(); + + if (transport != null) + { + localFingerprints + = transport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + } + } + + /* + * Determine whether the local fingerprints are to be sent to + * the remote peer. + */ + if ((localFingerprints != null) && !localFingerprints.isEmpty()) + { + if (remoteContent == null) // initiator + { + if (!protocolProvider.isFeatureSupported( + peer.getAddress(), + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_DTLS_SRTP)) + { + localFingerprints = null; + } + } + else // responder + { + IceUdpTransportPacketExtension transport + = remoteContent.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + if (transport == null) + { + localFingerprints = null; + } + else + { + List + remoteFingerprints + = transport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension + .class); + + if (remoteFingerprints.isEmpty()) + localFingerprints = null; + } + } + // Send the local fingerprints to the remote peer. + if (localFingerprints != null) + { + List fingerprintPEs + = localTransport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + + if (fingerprintPEs.isEmpty()) + { + for (DtlsFingerprintPacketExtension localFingerprint + : localFingerprints) + { + DtlsFingerprintPacketExtension fingerprintPE + = new DtlsFingerprintPacketExtension(); + + fingerprintPE.setFingerprint( + localFingerprint.getFingerprint()); + fingerprintPE.setHash( + localFingerprint.getHash()); + localTransport.addChildExtension(fingerprintPE); + } + } + b = true; + } + } + } + } + else + { + SrtpControls srtpControls = getSrtpControls(); + DtlsControl dtlsControl + = (DtlsControl) + srtpControls.get(mediaType, SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + CallJabberImpl.setDtlsEncryptionOnTransport( + dtlsControl, + localTransport); + b = true; + } + } + return b; + } + + /** + * Sets DTLS-SRTP element(s) to the TRANSPORT element of a specified list of + * CONTENT elements. + * + * @param localContents The elements containing the media CONTENT elements + * and their respective TRANSPORT elements. + */ + private void setDtlsEncryptionOnTransports( + List remoteContents, + List localContents) + { + for (ContentPacketExtension localContent : localContents) + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(localContent); + + if (description != null) + { + MediaType mediaType = JingleUtils.getMediaType(localContent); + + if (mediaType != null) + { + ContentPacketExtension remoteContent + = (remoteContents == null) + ? null + : TransportManagerJabberImpl.findContentByName( + remoteContents, + localContent.getName()); + + setDtlsEncryptionOnTransport( + mediaType, + localContent, + remoteContent); + } + } + } + } + + /** + * Sets the jingle transports that this + * CallPeerMediaHandlerJabberImpl supports. Unknown transports are + * ignored, and the transports Collection is put into + * order depending on local preference. + * + * Currently only ice and raw-udp are recognized, with ice being preffered + * over raw-udp + * + * @param transports A Collection of XML namespaces of jingle + * transport elements to be set as the supported jingle transports for this + * CallPeerMediaHandlerJabberImpl + */ + public void setSupportedTransports(Collection transports) + { + if (transports == null) + return; + + String ice + = ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1; + String rawUdp + = ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0; + + int size = 0; + for(String transport : transports) + if (ice.equals(transport) || rawUdp.equals(transport)) + size++; + + if (size > 0) + { + synchronized (supportedTransportsSyncRoot) + { + supportedTransports = new String[size]; + int i = 0; + + // we prefer ice over raw-udp + if (transports.contains(ice)) + { + supportedTransports[i] = ice; + i++; + } + + if (transports.contains(rawUdp)) + { + supportedTransports[i] = rawUdp; + i++; + } + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java index f9ca61ad1..7e20957ac 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java @@ -1,2950 +1,2950 @@ -/* - * Jitsi, the OpenSource Java VoIP and Instant Messaging client. - * - * Distributable under LGPL license. - * See terms of license at gnu.org. - */ -package net.java.sip.communicator.impl.protocol.jabber; - -import java.math.*; -import java.net.*; -import java.security.*; -import java.security.cert.*; -import java.text.*; -import java.util.*; - -import javax.net.ssl.*; - -import net.java.sip.communicator.impl.protocol.jabber.debugger.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.caps.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.carbon.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.inputevt.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingleinfo.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.keepalive.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.messagecorrection.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.version.*; -import net.java.sip.communicator.service.certificate.*; -import net.java.sip.communicator.service.dns.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.jabberconstants.*; -import net.java.sip.communicator.util.*; -import net.java.sip.communicator.util.Logger; - -import org.jitsi.service.configuration.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.util.*; -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.packet.*; -import org.jivesoftware.smack.provider.*; -import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smackx.*; -import org.jivesoftware.smackx.packet.*; -import org.osgi.framework.*; -import org.xmpp.jnodes.smack.*; - -/** - * An implementation of the protocol provider service over the Jabber protocol - * - * @author Damian Minkov - * @author Symphorien Wanko - * @author Lyubomir Marinov - * @author Yana Stamcheva - * @author Emil Ivov - */ -public class ProtocolProviderServiceJabberImpl - extends AbstractProtocolProviderService -{ - /** - * Logger of this class - */ - private static final Logger logger = - Logger.getLogger(ProtocolProviderServiceJabberImpl.class); - - /** - * Jingle's Discovery Info common URN. - */ - public static final String URN_XMPP_JINGLE = JingleIQ.NAMESPACE; - - /** - * Jingle's Discovery Info URN for RTP support. - */ - public static final String URN_XMPP_JINGLE_RTP - = RtpDescriptionPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for RTP support with audio. - */ - public static final String URN_XMPP_JINGLE_RTP_AUDIO - = "urn:xmpp:jingle:apps:rtp:audio"; - - /** - * Jingle's Discovery Info URN for RTP support with video. - */ - public static final String URN_XMPP_JINGLE_RTP_VIDEO - = "urn:xmpp:jingle:apps:rtp:video"; - - /** - * Jingle's Discovery Info URN for ZRTP support with RTP. - */ - public static final String URN_XMPP_JINGLE_RTP_ZRTP - = ZrtpHashPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for ICE_UDP transport support. - */ - public static final String URN_XMPP_JINGLE_RAW_UDP_0 - = RawUdpTransportPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for ICE_UDP transport support. - */ - public static final String URN_XMPP_JINGLE_ICE_UDP_1 - = IceUdpTransportPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for Jingle Nodes support. - */ - public static final String URN_XMPP_JINGLE_NODES - = "http://jabber.org/protocol/jinglenodes"; - - /** - * Jingle's Discovery Info URN for "XEP-0251: Jingle Session Transfer" - * support. - */ - public static final String URN_XMPP_JINGLE_TRANSFER_0 - = TransferPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for "XEP-298 :Delivering Conference - * Information to Jingle Participants (Coin)" support. - */ - public static final String URN_XMPP_JINGLE_COIN = "urn:xmpp:coin"; - - /** - * Jingle's Discovery Info URN for "XEP-0320: Use of DTLS-SRTP in - * Jingle Sessions". - */ - public static final String URN_XMPP_JINGLE_DTLS_SRTP - = "urn:xmpp:jingle:apps:dtls:0"; - - /** - * Discovery Info URN for classic RFC3264-style Offer/Answer negotiation - * with no support for Trickle ICE and low tolerance to transport/payload - * separation. Defined in XEP-0176 - */ - public static final String URN_IETF_RFC_3264 = "urn:ietf:rfc:3264"; - - /** - * Jingle's Discovery Info URN for "XEP-0294: Jingle RTP Header Extensions - * Negotiation" support. - */ - public static final String URN_XMPP_JINGLE_RTP_HDREXT = - "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; - - /** - * Capabilities name for audio call in Google Talk web version. - */ - public static final String CAPS_GTALK_WEB_VOICE = "voice-v1"; - - /** - * Capabilities name for video call (receive side) in Google Talk web - * version. - */ - public static final String CAPS_GTALK_WEB_VIDEO = "video-v1"; - - /** - * Capabilities name for video call (sender side) in Google Talk web - * version. - */ - public static final String CAPS_GTALK_WEB_CAMERA = "camera-v1"; - - /** - * URN for Google voice. - */ - public static final String URN_GOOGLE_VOICE = - "http://www.google.com/xmpp/protocol/voice/v1"; - - /** - * URN for Google camera. - */ - public static final String URN_GOOGLE_CAMERA = - "http://www.google.com/xmpp/protocol/camera/v1"; - - /** - * URN for Google video. - */ - public static final String URN_GOOGLE_VIDEO = - "http://www.google.com/xmpp/protocol/video/v1"; - - /** - * URN for XEP-0077 inband registration - */ - public static final String URN_REGISTER = "jabber:iq:register"; - - /** - * The name of the property under which the user may specify if the desktop - * streaming or sharing should be disabled. - */ - private static final String IS_DESKTOP_STREAMING_DISABLED - = "net.java.sip.communicator.impl.protocol.jabber." + - "DESKTOP_STREAMING_DISABLED"; - - /** - * The name of the property under which the user may specify if audio/video - * calls should be disabled. - */ - private static final String IS_CALLING_DISABLED - = "net.java.sip.communicator.impl.protocol.jabber.CALLING_DISABLED"; - - /** - * Smack packet reply timeout. - */ - public static final int SMACK_PACKET_REPLY_TIMEOUT = 45000; - - /** - * Property for vcard reply timeout. Time to wait before - * we think vcard retrieving has timeouted, default value - * of smack is 5000 (5 sec.). - */ - public static final String VCARD_REPLY_TIMEOUT_PROPERTY = - "net.java.sip.communicator.impl.protocol.jabber.VCARD_REPLY_TIMEOUT"; - - /** - * XMPP signaling DSCP configuration property name. - */ - private static final String XMPP_DSCP_PROPERTY = - "net.java.sip.communicator.impl.protocol.XMPP_DSCP"; - - /** - * Indicates if user search is disabled. - */ - private static final String IS_USER_SEARCH_ENABLED_PROPERTY - = "USER_SEARCH_ENABLED"; - - /** - * Google voice domain name. - */ - public static final String GOOGLE_VOICE_DOMAIN = "voice.google.com"; - - /** - * Used to connect to a XMPP server. - */ - private XMPPConnection connection; - - /** - * Indicates whether or not the provider is initialized and ready for use. - */ - private boolean isInitialized = false; - - /** - * We use this to lock access to initialization. - */ - private final Object initializationLock = new Object(); - - /** - * The identifier of the account that this provider represents. - */ - private AccountID accountID = null; - - /** - * Used when we need to re-register - */ - private SecurityAuthority authority = null; - - /** - * The resource we will use when connecting during this run. - */ - private String resource = null; - - /** - * The icon corresponding to the jabber protocol. - */ - private ProtocolIconJabberImpl jabberIcon; - - /** - * A set of features supported by our Jabber implementation. - * In general, we add new feature(s) when we add new operation sets. - * (see xep-0030 : http://www.xmpp.org/extensions/xep-0030.html#info). - * Example : to tell the world that we support jingle, we simply have - * to do : - * supportedFeatures.add("http://www.xmpp.org/extensions/xep-0166.html#ns"); - * Beware there is no canonical mapping between op set and jabber features - * (op set is a SC "concept"). This means that one op set in SC can - * correspond to many jabber features. It is also possible that there is no - * jabber feature corresponding to a SC op set or again, - * we can currently support some features wich do not have a specific - * op set in SC (the mandatory feature : - * http://jabber.org/protocol/disco#info is one example). - * We can find features corresponding to op set in the xep(s) related - * to implemented functionality. - */ - private final List supportedFeatures = new ArrayList(); - - /** - * The ServiceDiscoveryManager is responsible for advertising - * supportedFeatures when asked by a remote client. It can also - * be used to query remote clients for supported features. - */ - private ScServiceDiscoveryManager discoveryManager = null; - - /** - * The OperationSetContactCapabilities of this - * ProtocolProviderService which is the service-public counterpart - * of {@link #discoveryManager}. - */ - private OperationSetContactCapabilitiesJabberImpl opsetContactCapabilities; - - /** - * The statuses. - */ - private JabberStatusEnum jabberStatusEnum; - - /** - * The service we use to interact with user. - */ - private CertificateService guiVerification; - - /** - * Used with tls connecting when certificates are not trusted - * and we ask the user to confirm connection. When some timeout expires - * connect method returns, and we use abortConnecting to abort further - * execution cause after user chooses we make further processing from there. - */ - private boolean abortConnecting = false; - - /** - * Flag indicating are we currently executing connectAndLogin method. - */ - private boolean inConnectAndLogin = false; - - /** - * Object used to synchronize the flag inConnectAndLogin. - */ - private final Object connectAndLoginLock = new Object(); - - /** - * If an event occurs during login we fire it at the end of the login - * process (at the end of connectAndLogin method). - */ - private RegistrationStateChangeEvent eventDuringLogin; - - /** - * Listens for connection closes or errors. - */ - private JabberConnectionListener connectionListener; - - /** - * The details of the proxy we are using to connect to the server (if any) - */ - private org.jivesoftware.smack.proxy.ProxyInfo proxy; - - /** - * Our provider manager instances. - */ - private static ProviderManager providerManager = null; - - /** - * Lock for creating provider. - */ - private static Object providerCreationLock = new Object(); - - /** - * State for connect and login state. - */ - enum ConnectState - { - /** - * Abort any further connecting. - */ - ABORT_CONNECTING, - /** - * Continue trying with next address. - */ - CONTINUE_TRYING, - /** - * Stop trying we succeeded or just have a final state for - * the whole connecting procedure. - */ - STOP_TRYING - } - - /** - * The debugger who logs packets. - */ - private SmackPacketDebugger debugger = null; - - /** - * Jingle Nodes service. - */ - private SmackServiceNode jingleNodesServiceNode = null; - - /** - * Synchronization object to monitore jingle nodes auto discovery. - */ - private final Object jingleNodesSyncRoot = new Object(); - - /** - * Stores user credentials for local use if user hasn't stored - * its password. - */ - private UserCredentials userCredentials = null; - - /** - * The currently running keepAliveManager if enabled. - */ - private KeepAliveManager keepAliveManager = null; - - /** - * The version manager. - */ - private VersionManager versionManager = null; - - // load xmpp manager classes - static - { - if(OSUtils.IS_ANDROID) - loadJabberServiceClasses(); - } - - /** - * Returns the state of the registration of this protocol provider - * @return the RegistrationState that this provider is - * currently in or null in case it is in a unknown state. - */ - public RegistrationState getRegistrationState() - { - if(connection == null) - return RegistrationState.UNREGISTERED; - else if(connection.isConnected() && connection.isAuthenticated()) - return RegistrationState.REGISTERED; - else - return RegistrationState.UNREGISTERED; - } - - /** - * Return the certificate verification service impl. - * @return the CertificateVerification service. - */ - private CertificateService getCertificateVerificationService() - { - if(guiVerification == null) - { - ServiceReference guiVerifyReference - = JabberActivator.getBundleContext().getServiceReference( - CertificateService.class.getName()); - if(guiVerifyReference != null) - guiVerification = (CertificateService) - JabberActivator.getBundleContext().getService( - guiVerifyReference); - } - - return guiVerification; - } - - /** - * Starts the registration process. Connection details such as - * registration server, user name/number are provided through the - * configuration service through implementation specific properties. - * - * @param authority the security authority that will be used for resolving - * any security challenges that may be returned during the - * registration or at any moment while we're registered. - * @throws OperationFailedException with the corresponding code it the - * registration fails for some reason (e.g. a networking error or an - * implementation problem). - */ - public void register(final SecurityAuthority authority) - throws OperationFailedException - { - if(authority == null) - throw new IllegalArgumentException( - "The register method needs a valid non-null authority impl " - + " in order to be able and retrieve passwords."); - - this.authority = authority; - - try - { - // reset states - abortConnecting = false; - - // indicate we started connectAndLogin process - synchronized(connectAndLoginLock) - { - inConnectAndLogin = true; - } - - initializeConnectAndLogin(authority, - SecurityAuthority.AUTHENTICATION_REQUIRED); - } - catch (XMPPException ex) - { - logger.error("Error registering", ex); - - eventDuringLogin = null; - - fireRegistrationStateChanged(ex); - } - finally - { - synchronized(connectAndLoginLock) - { - // Checks if an error has occurred during login, if so we fire - // it here in order to avoid a deadlock which occurs in - // reconnect plugin. The deadlock is cause we fired an event - // during login process and have locked initializationLock and - // we cannot unregister from reconnect, cause unregister method - // also needs this lock. - if(eventDuringLogin != null) - { - if(eventDuringLogin.getNewState().equals( - RegistrationState.CONNECTION_FAILED) || - eventDuringLogin.getNewState().equals( - RegistrationState.UNREGISTERED)) - disconnectAndCleanConnection(); - - fireRegistrationStateChanged( - eventDuringLogin.getOldState(), - eventDuringLogin.getNewState(), - eventDuringLogin.getReasonCode(), - eventDuringLogin.getReason()); - - eventDuringLogin = null; - inConnectAndLogin = false; - return; - } - - inConnectAndLogin = false; - } - } - } - - /** - * Connects and logins again to the server. - * - * @param authReasonCode indicates the reason of the re-authentication. - */ - void reregister(int authReasonCode) - { - try - { - if (logger.isTraceEnabled()) - logger.trace("Trying to reregister us!"); - - // sets this if any is trying to use us through registration - // to know we are not registered - this.unregister(false); - - // reset states - this.abortConnecting = false; - - // indicate we started connectAndLogin process - synchronized(connectAndLoginLock) - { - inConnectAndLogin = true; - } - - initializeConnectAndLogin(authority, authReasonCode); - } - catch(OperationFailedException ex) - { - logger.error("Error ReRegistering", ex); - - eventDuringLogin = null; - - disconnectAndCleanConnection(); - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, null); - } - catch (XMPPException ex) - { - logger.error("Error ReRegistering", ex); - - eventDuringLogin = null; - - fireRegistrationStateChanged(ex); - } - finally - { - synchronized(connectAndLoginLock) - { - // Checks if an error has occurred during login, if so we fire - // it here in order to avoid a deadlock which occurs in - // reconnect plugin. The deadlock is cause we fired an event - // during login process and have locked initializationLock and - // we cannot unregister from reconnect, cause unregister method - // also needs this lock. - if(eventDuringLogin != null) - { - if(eventDuringLogin.getNewState().equals( - RegistrationState.CONNECTION_FAILED) || - eventDuringLogin.getNewState().equals( - RegistrationState.UNREGISTERED)) - disconnectAndCleanConnection(); - - fireRegistrationStateChanged( - eventDuringLogin.getOldState(), - eventDuringLogin.getNewState(), - eventDuringLogin.getReasonCode(), - eventDuringLogin.getReason()); - - eventDuringLogin = null; - inConnectAndLogin = false; - return; - } - - inConnectAndLogin = false; - } - } - } - - /** - * Indicates if the XMPP transport channel is using a TLS secured socket. - * - * @return True when TLS is used, false otherwise. - */ - public boolean isSignalingTransportSecure() - { - return connection != null && connection.isUsingTLS(); - } - - /** - * Returns the "transport" protocol of this instance used to carry the - * control channel for the current protocol service. - * - * @return The "transport" protocol of this instance: TCP, TLS or UNKNOWN. - */ - public TransportProtocol getTransportProtocol() - { - // Without a connection, there is no transport available. - if(connection != null && connection.isConnected()) - { - // Transport using a secure connection. - if(connection.isUsingTLS()) - { - return TransportProtocol.TLS; - } - // Transport using a unsecure connection. - return TransportProtocol.TCP; - } - return TransportProtocol.UNKNOWN; - } - - /** - * Connects and logins to the server - * @param authority SecurityAuthority - * @param reasonCode the authentication reason code. Indicates the reason of - * this authentication. - * @throws XMPPException if we cannot connect to the server - network problem - * @throws OperationFailedException if login parameters - * as server port are not correct - */ - private void initializeConnectAndLogin(SecurityAuthority authority, - int reasonCode) - throws XMPPException, OperationFailedException - { - synchronized(initializationLock) - { - // if a thread is waiting for initializationLock and enters - // lets check whether someone hasn't already tried login and - // have succeeded, - // should prevent "Trace possible duplicate connections" prints - if(isRegistered()) - return; - - JabberLoginStrategy loginStrategy = createLoginStrategy(); - userCredentials = loginStrategy.prepareLogin(authority, reasonCode); - if(!loginStrategy.loginPreparationSuccessful()) - return; - - String serviceName - = StringUtils.parseServer(getAccountID().getUserID()); - - loadResource(); - loadProxy(); - Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual); - - ConnectState state; - //[0] = hadDnsSecException - boolean[] hadDnsSecException = new boolean[]{false}; - - // try connecting with auto-detection if enabled - boolean isServerOverriden = - getAccountID().getAccountPropertyBoolean( - ProtocolProviderFactory.IS_SERVER_OVERRIDDEN, false); - - if(!isServerOverriden) - { - state = connectUsingSRVRecords(serviceName, - serviceName, hadDnsSecException, loginStrategy); - if(hadDnsSecException[0]) - { - setDnssecLoginFailure(); - return; - } - if(state == ConnectState.ABORT_CONNECTING - || state == ConnectState.STOP_TRYING) - return; - } - - // check for custom xmpp domain which we will check for - // SRV records for server addresses - String customXMPPDomain = getAccountID() - .getAccountPropertyString("CUSTOM_XMPP_DOMAIN"); - - if(customXMPPDomain != null && !hadDnsSecException[0]) - { - logger.info("Connect using custom xmpp domain: " + - customXMPPDomain); - - state = connectUsingSRVRecords( - customXMPPDomain, serviceName, - hadDnsSecException, loginStrategy); - - logger.info("state for connectUsingSRVRecords: " + state); - - if(hadDnsSecException[0]) - { - setDnssecLoginFailure(); - return; - } - - if(state == ConnectState.ABORT_CONNECTING - || state == ConnectState.STOP_TRYING) - return; - } - - // connect with specified server name - String serverAddressUserSetting - = getAccountID().getAccountPropertyString( - ProtocolProviderFactory.SERVER_ADDRESS); - - int serverPort = getAccountID().getAccountPropertyInt( - ProtocolProviderFactory.SERVER_PORT, 5222); - - InetSocketAddress[] addrs = null; - try - { - addrs = NetworkUtils.getAandAAAARecords( - serverAddressUserSetting, - serverPort - ); - } - catch (ParseException e) - { - logger.error("Domain not resolved", e); - } - catch (DnssecException e) - { - logger.error("DNSSEC failure for overridden server", e); - setDnssecLoginFailure(); - return; - } - - if (addrs == null || addrs.length == 0) - { - logger.error("No server addresses found"); - - eventDuringLogin = null; - - fireRegistrationStateChanged( - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, - "No server addresses found"); - } - else - { - for (InetSocketAddress isa : addrs) - { - try - { - state = connectAndLogin(isa, serviceName, - loginStrategy); - if(state == ConnectState.ABORT_CONNECTING - || state == ConnectState.STOP_TRYING) - return; - } - catch(XMPPException ex) - { - disconnectAndCleanConnection(); - if(isAuthenticationFailed(ex)) - throw ex; - } - } - } - } - } - - /** - * Creates the JabberLoginStrategy to use for the current account. - */ - private JabberLoginStrategy createLoginStrategy() - { - String clientCertId = getAccountID().getAccountPropertyString( - ProtocolProviderFactory.CLIENT_TLS_CERTIFICATE); - if(clientCertId != null) - { - return new LoginByClientCertificateStrategy(getAccountID()); - } - else - { - return new LoginByPasswordStrategy(this, getAccountID()); - } - } - - private void setDnssecLoginFailure() - { - eventDuringLogin = new RegistrationStateChangeEvent( - this, - getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_USER_REQUEST, - "No usable host found due to DNSSEC failures"); - } - - /** - * Connects using the domain specified and its SRV records. - * @param domain the domain to use - * @param serviceName the domain name of the user's login - * @param dnssecState state of possible received DNSSEC exceptions - * @param loginStrategy the login strategy to use - * @return whether to continue trying or stop. - */ - private ConnectState connectUsingSRVRecords( - String domain, - String serviceName, - boolean[] dnssecState, - JabberLoginStrategy loginStrategy) - throws XMPPException - { - // check to see is there SRV records for this server domain - SRVRecord srvRecords[] = null; - try - { - srvRecords = NetworkUtils - .getSRVRecords("xmpp-client", "tcp", domain); - } - catch (ParseException e) - { - logger.error("SRV record not resolved", e); - } - catch (DnssecException e) - { - logger.error("DNSSEC failure for SRV lookup", e); - dnssecState[0] = true; - } - - if(srvRecords != null) - { - for(SRVRecord srv : srvRecords) - { - InetSocketAddress[] addrs = null; - try - { - addrs = - NetworkUtils.getAandAAAARecords( - srv.getTarget(), - srv.getPort() - ); - } - catch (ParseException e) - { - logger.error("Invalid SRV record target", e); - } - catch (DnssecException e) - { - logger.error("DNSSEC failure for A/AAAA lookup of SRV", e); - dnssecState[0] = true; - } - - if (addrs == null || addrs.length == 0) - { - logger.error("No A/AAAA addresses found for " + - srv.getTarget()); - continue; - } - - for (InetSocketAddress isa : addrs) - { - try - { - // if failover mechanism is enabled, use it, - // default is not enabled. - if(JabberActivator.getConfigurationService() - .getBoolean(FailoverConnectionMonitor - .REVERSE_FAILOVER_ENABLED_PROP, - false - )) - { - FailoverConnectionMonitor.getInstance(this) - .setCurrent(domain, srv.getTarget()); - } - - ConnectState state = connectAndLogin( - isa, serviceName, loginStrategy); - return state; - } - catch(XMPPException ex) - { - logger.error("Error connecting to " + isa - + " for domain:" + domain - + " serviceName:" + serviceName, ex); - - disconnectAndCleanConnection(); - - if(isAuthenticationFailed(ex)) - throw ex; - } - } - } - } - else - logger.error("No SRV addresses found for _xmpp-client._tcp." - + domain); - - return ConnectState.CONTINUE_TRYING; - } - - /** - * Tries to login to the XMPP server with the supplied user ID. If the - * protocol is Google Talk, the user ID including the service name is used. - * For other protocols, if the login with the user ID without the service - * name fails, a second attempt including the service name is made. - * - * @param currentAddress the IP address to connect to - * @param serviceName the domain name of the user's login - * @param loginStrategy the login strategy to use - * @throws XMPPException when a failure occurs - */ - private ConnectState connectAndLogin(InetSocketAddress currentAddress, - String serviceName, - JabberLoginStrategy loginStrategy) - throws XMPPException - { - String userID = null; - boolean qualifiedUserID; - - /* with a google account (either gmail or google apps - * related ones), the userID MUST be the full e-mail address - * not just the ID - */ - if(getAccountID().getProtocolDisplayName().equals("Google Talk")) - { - userID = getAccountID().getUserID(); - qualifiedUserID = true; - } - else - { - userID = StringUtils.parseName(getAccountID().getUserID()); - qualifiedUserID = false; - } - - try - { - return connectAndLogin( - currentAddress, serviceName, - userID, resource, loginStrategy); - } - catch(XMPPException ex) - { - // server disconnect us after such an error, do cleanup - disconnectAndCleanConnection(); - - //no need to check with a different username if the - //socket could not be opened - if (ex.getWrappedThrowable() instanceof ConnectException - || ex.getWrappedThrowable() instanceof NoRouteToHostException) - { - //as we got an exception not handled in connectAndLogin - //no state was set, so fire it here so we can continue - //with the re-register process - //2013-08-07 do not fire event, if we have several - // addresses and we fire event will activate reconnect - // but we will continue connecting with other addresses - // and can register with address, then unregister and try again - // that is from reconnect plugin. - // Storing event for fire after all have failed and we have - // tried every address. - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, - null); - - throw ex; - } - - // don't attempt to append the service name if it's already there - if (!qualifiedUserID) - { - try - { - // logging in might need the service name - return connectAndLogin( - currentAddress, serviceName, - userID + "@" + serviceName, - resource, - loginStrategy); - } - catch(XMPPException ex2) - { - disconnectAndCleanConnection(); - throw ex; //throw the original exception - } - } - else - throw ex; - } - } - - /** - * Initializes the Jabber Resource identifier. - */ - private void loadResource() - { - if(resource == null) - { - String defaultResource = "jitsi"; - String autoGenenerateResource = - getAccountID().getAccountPropertyString( - ProtocolProviderFactory.AUTO_GENERATE_RESOURCE); - if(autoGenenerateResource == null || - Boolean.parseBoolean(autoGenenerateResource)) - { - SecureRandom random = new SecureRandom(); - - resource = defaultResource + "-" + - new BigInteger(32, random).toString(32); - } - else - { - resource = getAccountID().getAccountPropertyString( - ProtocolProviderFactory.RESOURCE); - - if(resource == null || resource.length() == 0) - resource = defaultResource; - } - } - } - - /** - * Sets the global proxy information based on the configuration - * - * @throws OperationFailedException - */ - private void loadProxy() throws OperationFailedException - { - String globalProxyType = - JabberActivator.getConfigurationService() - .getString(ProxyInfo.CONNECTION_PROXY_TYPE_PROPERTY_NAME); - if(globalProxyType == null || - globalProxyType.equals(ProxyInfo.ProxyType.NONE.name())) - { - proxy = org.jivesoftware.smack.proxy.ProxyInfo.forNoProxy(); - } - else - { - String globalProxyAddress = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_ADDRESS_PROPERTY_NAME); - String globalProxyPortStr = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_PORT_PROPERTY_NAME); - int globalProxyPort; - try - { - globalProxyPort = Integer.parseInt( - globalProxyPortStr); - } - catch(NumberFormatException ex) - { - throw new OperationFailedException("Wrong proxy port, " - + globalProxyPortStr - + " does not represent an integer", - OperationFailedException.INVALID_ACCOUNT_PROPERTIES, - ex); - } - String globalProxyUsername = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_USERNAME_PROPERTY_NAME); - String globalProxyPassword = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_PASSWORD_PROPERTY_NAME); - if(globalProxyAddress == null || - globalProxyAddress.length() <= 0) - { - throw new OperationFailedException( - "Missing Proxy Address", - OperationFailedException.INVALID_ACCOUNT_PROPERTIES); - } - try - { - proxy = new org.jivesoftware.smack.proxy.ProxyInfo( - Enum.valueOf(org.jivesoftware.smack.proxy.ProxyInfo. - ProxyType.class, globalProxyType), - globalProxyAddress, globalProxyPort, - globalProxyUsername, globalProxyPassword); - } - catch(IllegalArgumentException e) - { - logger.error("Invalid value for smack proxy enum", e); - proxy = null; - } - } - } - - /** - * Connects xmpp connection and login. Returning the state whether is it - * final - Abort due to certificate cancel or keep trying cause only current - * address has failed or stop trying cause we succeeded. - * @param address the address to connect to - * @param serviceName the service name to use - * @param userName the username to use - * @param resource and the resource. - * @param loginStrategy the login strategy to use - * @return return the state how to continue the connect process. - * @throws XMPPException if we cannot connect for some reason - */ - private ConnectState connectAndLogin( - InetSocketAddress address, String serviceName, - String userName, String resource, - JabberLoginStrategy loginStrategy) - throws XMPPException - { - ConnectionConfiguration confConn = new ConnectionConfiguration( - address.getAddress().getHostAddress(), - address.getPort(), - serviceName, proxy - ); - - confConn.setReconnectionAllowed(false); - boolean tlsRequired = loginStrategy.isTlsRequired(); - - // user have the possibility to disable TLS but in this case, it will - // not be able to connect to a server which requires TLS - confConn.setSecurityMode( - tlsRequired ? ConnectionConfiguration.SecurityMode.required : - ConnectionConfiguration.SecurityMode.enabled); - - if(connection != null) - { - logger.error("Connection is not null and isConnected:" - + connection.isConnected(), - new Exception("Trace possible duplicate connections: " + - getAccountID().getAccountAddress())); - disconnectAndCleanConnection(); - } - - connection = new XMPPConnection(confConn); - - try - { - CertificateService cvs = - getCertificateVerificationService(); - if(cvs != null) - { - SSLContext sslContext = loginStrategy.createSslContext(cvs, - getTrustManager(cvs, serviceName)); - - // log SSL/TLS algorithms and protocols - if (logger.isDebugEnabled()) - { - final StringBuilder buff = new StringBuilder(); - buff.append("Available TLS protocols and algorithms:\n"); - buff.append("Default protocols: "); - buff.append(Arrays.toString( - sslContext.getDefaultSSLParameters().getProtocols())); - buff.append("\n"); - buff.append("Supported protocols: "); - buff.append(Arrays.toString( - sslContext.getSupportedSSLParameters().getProtocols())); - buff.append("\n"); - buff.append("Default cipher suites: "); - buff.append(Arrays.toString( - sslContext.getDefaultSSLParameters() - .getCipherSuites())); - buff.append("\n"); - buff.append("Supported cipher suites: "); - buff.append(Arrays.toString( - sslContext.getSupportedSSLParameters() - .getCipherSuites())); - logger.debug(buff.toString()); - } - - connection.setCustomSslContext(sslContext); - } - else if (tlsRequired) - throw new XMPPException( - "Certificate verification service is " - + "unavailable and TLS is required"); - } - catch(GeneralSecurityException e) - { - logger.error("Error creating custom trust manager", e); - throw new XMPPException("Error creating custom trust manager", e); - } - - if(debugger == null) - debugger = new SmackPacketDebugger(); - - // sets the debugger - debugger.setConnection(connection); - connection.addPacketListener(debugger, null); - connection.addPacketInterceptor(debugger, null); - - connection.connect(); - - setTrafficClass(); - - if(abortConnecting) - { - abortConnecting = false; - disconnectAndCleanConnection(); - - return ConnectState.ABORT_CONNECTING; - } - - registerServiceDiscoveryManager(); - - if(connectionListener == null) - { - connectionListener = new JabberConnectionListener(); - } - - if(!connection.isSecureConnection() && tlsRequired) - { - throw new XMPPException("TLS is required by client"); - } - - if(!connection.isConnected()) - { - // connection is not connected, lets set state to our connection - // as failed seems there is some lag/problem with network - // and this way we will inform for it and later reconnect if needed - // as IllegalStateException that is thrown within - // addConnectionListener is not handled properly - disconnectAndCleanConnection(); - - logger.error("Connection not established, server not found!"); - - eventDuringLogin = null; - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, null); - - return ConnectState.ABORT_CONNECTING; - } - else - { - if (connection.getSocket() instanceof SSLSocket) - { - final SSLSocket sslSocket = (SSLSocket) connection.getSocket(); - StringBuilder buff = new StringBuilder(); - buff.append("Chosen TLS protocol and algorithm:\n") - .append("Protocol: ").append(sslSocket.getSession() - .getProtocol()).append("\n") - .append("Cipher suite: ").append(sslSocket.getSession() - .getCipherSuite()); - logger.info(buff.toString()); - - if (logger.isDebugEnabled()) - { - buff = new StringBuilder(); - buff.append("Server TLS certificate chain:\n"); - try - { - buff.append(Arrays.toString( - sslSocket.getSession().getPeerCertificates())); - } - catch (SSLPeerUnverifiedException ex) - { - buff.append(""); - } - logger.debug(buff.toString()); - } - } - - connection.addConnectionListener(connectionListener); - } - - if(abortConnecting) - { - abortConnecting = false; - disconnectAndCleanConnection(); - - return ConnectState.ABORT_CONNECTING; - } - - fireRegistrationStateChanged( - getRegistrationState() - , RegistrationState.REGISTERING - , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED - , null); - - if (!loginStrategy.login(connection, userName, resource)) - { - disconnectAndCleanConnection(); - eventDuringLogin = null; - fireRegistrationStateChanged( - getRegistrationState(), - // not auth failed, or there would be no info-popup - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED, - loginStrategy.getClass().getName() + " requests abort"); - - return ConnectState.ABORT_CONNECTING; - } - - if(connection.isAuthenticated()) - { - eventDuringLogin = null; - - fireRegistrationStateChanged( - getRegistrationState(), - RegistrationState.REGISTERED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null); - - return ConnectState.STOP_TRYING; - } - else - { - disconnectAndCleanConnection(); - - eventDuringLogin = null; - - fireRegistrationStateChanged( - getRegistrationState() - , RegistrationState.UNREGISTERED - , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED - , null); - - return ConnectState.CONTINUE_TRYING; - } - } - - /** - * Gets the TrustManager that should be used for the specified service - * - * @param serviceName the service name - * @param cvs The CertificateVerificationService to retrieve the - * trust manager - * @return the trust manager - */ - private X509TrustManager getTrustManager(CertificateService cvs, - String serviceName) - throws GeneralSecurityException - { - return new HostTrustManager( - cvs.getTrustManager( - Arrays.asList(new String[]{ - serviceName, - "_xmpp-client." + serviceName - }) - ) - ); - } - - /** - * Registers our ServiceDiscoveryManager - */ - private void registerServiceDiscoveryManager() - { - // we setup supported features no packets are actually sent - //during feature registration so we'd better do it here so that - //our first presence update would contain a caps with the right - //features. - String name - = System.getProperty( - "sip-communicator.application.name", - "SIP Communicator ") - + System.getProperty("sip-communicator.version","SVN"); - - ServiceDiscoveryManager.setIdentityName(name); - ServiceDiscoveryManager.setIdentityType("pc"); - - discoveryManager - = new ScServiceDiscoveryManager( - this, - new String[] { "http://jabber.org/protocol/commands"}, - // Add features Jitsi supports in addition to smack. - supportedFeatures.toArray( - new String[supportedFeatures.size()])); - - boolean isCallingDisabled - = JabberActivator.getConfigurationService() - .getBoolean(IS_CALLING_DISABLED, false); - - boolean isCallingDisabledForAccount = false; - if (accountID != null && accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT, - false)) - isCallingDisabled = true; - - if(isGTalkTesting() - && !isCallingDisabled - && !isCallingDisabledForAccount) - { - // Add Google Talk "ext" capabilities - discoveryManager.addExtFeature(CAPS_GTALK_WEB_VOICE); - discoveryManager.addExtFeature(CAPS_GTALK_WEB_VIDEO); - discoveryManager.addExtFeature(CAPS_GTALK_WEB_CAMERA); - discoveryManager.addFeature(URN_GOOGLE_VOICE); - discoveryManager.addFeature(URN_GOOGLE_VIDEO); - discoveryManager.addFeature(URN_GOOGLE_CAMERA); - } - - /* - * Expose the discoveryManager as service-public through the - * OperationSetContactCapabilities of this ProtocolProviderService. - */ - if (opsetContactCapabilities != null) - opsetContactCapabilities.setDiscoveryManager(discoveryManager); - } - - /** - * Used to disconnect current connection and clean it. - */ - public void disconnectAndCleanConnection() - { - if(connection != null) - { - connection.removeConnectionListener(connectionListener); - - // disconnect anyway cause it will clear any listeners - // that maybe added even if its not connected - try - { - OperationSetPersistentPresenceJabberImpl opSet = - (OperationSetPersistentPresenceJabberImpl) - this.getOperationSet(OperationSetPersistentPresence.class); - - Presence unavailablePresence = - new Presence(Presence.Type.unavailable); - - if(opSet != null - && !org.jitsi.util.StringUtils - .isNullOrEmpty(opSet.getCurrentStatusMessage())) - { - unavailablePresence.setStatus( - opSet.getCurrentStatusMessage()); - } - - connection.disconnect(unavailablePresence); - } catch (Exception e) - {} - - connectionListener = null; - connection = null; - // make it null as it also holds a reference to the old connection - // will be created again on new connection - try - { - /* - * The discoveryManager is exposed as service-public by the - * OperationSetContactCapabilities of this - * ProtocolProviderService. No longer expose it because it's - * going away. - */ - if (opsetContactCapabilities != null) - opsetContactCapabilities.setDiscoveryManager(null); - } - finally - { - if(discoveryManager != null) - { - discoveryManager.stop(); - discoveryManager = null; - } - } - } - } - - /** - * Ends the registration of this protocol provider with the service. - */ - public void unregister() - { - unregister(true); - } - - /** - * Unregister and fire the event if requested - * @param fireEvent boolean - */ - public void unregister(boolean fireEvent) - { - synchronized(initializationLock) - { - if(fireEvent) - { - eventDuringLogin = null; - fireRegistrationStateChanged( - getRegistrationState() - , RegistrationState.UNREGISTERING - , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED - , null); - } - - disconnectAndCleanConnection(); - - RegistrationState currRegState = getRegistrationState(); - - if(fireEvent) - { - eventDuringLogin = null; - fireRegistrationStateChanged( - currRegState, - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_USER_REQUEST, null); - } - } - } - - /** - * Returns the short name of the protocol that the implementation of this - * provider is based upon (like SIP, Jabber, ICQ/AIM, or others for - * example). - * - * @return a String containing the short name of the protocol this - * service is taking care of. - */ - public String getProtocolName() - { - return ProtocolNames.JABBER; - } - - /** - * Initialized the service implementation, and puts it in a sate where it - * could interoperate with other services. It is strongly recommended that - * properties in this Map be mapped to property names as specified by - * AccountProperties. - * - * @param screenname the account id/uin/screenname of the account that - * we're about to create - * @param accountID the identifier of the account that this protocol - * provider represents. - * - * @see net.java.sip.communicator.service.protocol.AccountID - */ - protected void initialize(String screenname, - AccountID accountID) - { - synchronized(initializationLock) - { - this.accountID = accountID; - - // in case of modified account, we clear list of supported features - // and every state change listeners, otherwise we can have two - // OperationSet for same feature and it can causes problem (i.e. - // two OperationSetBasicTelephony can launch two ICE negotiations - // (with different ufrag/pwd) and peer will failed call. And - // by the way user will see two dialog for answering/refusing the - // call - supportedFeatures.clear(); - this.clearRegistrationStateChangeListener(); - this.clearSupportedOperationSet(); - - synchronized(providerCreationLock) - { - if(providerManager == null) - { - try - { - ProviderManager.setInstance(new ProviderManagerExt()); - } - catch(Throwable t) - { - // once loaded if we try to set instance second time - // IllegalStateException is thrown - } - finally - { - providerManager = ProviderManager.getInstance(); - } - } - } - - String protocolIconPath - = accountID.getAccountPropertyString( - ProtocolProviderFactory.PROTOCOL_ICON_PATH); - - if (protocolIconPath == null) - protocolIconPath = "resources/images/protocol/jabber"; - - jabberIcon = new ProtocolIconJabberImpl(protocolIconPath); - - jabberStatusEnum - = JabberStatusEnum.getJabberStatusEnum(protocolIconPath); - - //this feature is mandatory to be compliant with Service Discovery - supportedFeatures.add("http://jabber.org/protocol/disco#info"); - - String keepAliveStrValue - = accountID.getAccountPropertyString( - ProtocolProviderFactory.KEEP_ALIVE_METHOD); - - InfoRetreiver infoRetreiver = new InfoRetreiver(this, screenname); - - //initialize the presence OperationSet - OperationSetPersistentPresenceJabberImpl persistentPresence = - new OperationSetPersistentPresenceJabberImpl(this, infoRetreiver); - - addSupportedOperationSet( - OperationSetPersistentPresence.class, - persistentPresence); - // TODO: add the feature, if any, corresponding to persistent - // presence, if someone knows - // supportedFeatures.add(_PRESENCE_); - - //register it once again for those that simply need presence - addSupportedOperationSet( - OperationSetPresence.class, - persistentPresence); - - if(accountID.getAccountPropertyString( - ProtocolProviderFactory.ACCOUNT_READ_ONLY_GROUPS) != null) - { - addSupportedOperationSet( - OperationSetPersistentPresencePermissions.class, - new OperationSetPersistentPresencePermissionsJabberImpl( - this)); - } - - //initialize the IM operation set - OperationSetBasicInstantMessagingJabberImpl basicInstantMessaging = - new OperationSetBasicInstantMessagingJabberImpl(this); - - if (keepAliveStrValue == null - || keepAliveStrValue.equalsIgnoreCase("XEP-0199")) - { - if(keepAliveManager == null) - keepAliveManager = new KeepAliveManager(this); - } - - addSupportedOperationSet( - OperationSetBasicInstantMessaging.class, - basicInstantMessaging); - - // The http://jabber.org/protocol/xhtml-im feature is included - // already in smack. - - addSupportedOperationSet( - OperationSetExtendedAuthorizations.class, - new OperationSetExtendedAuthorizationsJabberImpl( - this, - persistentPresence)); - - //initialize the Whiteboard operation set - addSupportedOperationSet( - OperationSetWhiteboarding.class, - new OperationSetWhiteboardingJabberImpl(this)); - - //initialize the typing notifications operation set - addSupportedOperationSet( - OperationSetTypingNotifications.class, - new OperationSetTypingNotificationsJabberImpl(this)); - - // The http://jabber.org/protocol/chatstates feature implemented in - // OperationSetTypingNotifications is included already in smack. - - //initialize the multi user chat operation set - addSupportedOperationSet( - OperationSetMultiUserChat.class, - new OperationSetMultiUserChatJabberImpl(this)); - - addSupportedOperationSet( - OperationSetServerStoredContactInfo.class, - new OperationSetServerStoredContactInfoJabberImpl( - infoRetreiver)); - - OperationSetServerStoredAccountInfo accountInfo = - new OperationSetServerStoredAccountInfoJabberImpl(this, - infoRetreiver, - screenname); - - addSupportedOperationSet( - OperationSetServerStoredAccountInfo.class, - accountInfo); - - // Initialize avatar operation set - addSupportedOperationSet( - OperationSetAvatar.class, - new OperationSetAvatarJabberImpl(this, accountInfo)); - - // initialize the file transfer operation set - addSupportedOperationSet( - OperationSetFileTransfer.class, - new OperationSetFileTransferJabberImpl(this)); - - addSupportedOperationSet( - OperationSetInstantMessageTransform.class, - new OperationSetInstantMessageTransformImpl()); - - // Include features we're supporting in addition to the four - // included by smack itself: - // http://jabber.org/protocol/si/profile/file-transfer - // http://jabber.org/protocol/si - // http://jabber.org/protocol/bytestreams - // http://jabber.org/protocol/ibb - supportedFeatures.add("urn:xmpp:thumbs:0"); - supportedFeatures.add("urn:xmpp:bob"); - - // initialize the thumbnailed file factory operation set - addSupportedOperationSet( - OperationSetThumbnailedFileFactory.class, - new OperationSetThumbnailedFileFactoryImpl()); - - // TODO: this is the "main" feature to advertise when a client - // support muc. We have to add some features for - // specific functionality we support in muc. - // see http://www.xmpp.org/extensions/xep-0045.html - - // The http://jabber.org/protocol/muc feature is already included in - // smack. - supportedFeatures.add("http://jabber.org/protocol/muc#rooms"); - supportedFeatures.add("http://jabber.org/protocol/muc#traffic"); - - // RTP HDR extension - supportedFeatures.add(URN_XMPP_JINGLE_RTP_HDREXT); - - //register our jingle provider - providerManager.addIQProvider( JingleIQ.ELEMENT_NAME, - JingleIQ.NAMESPACE, - new JingleIQProvider()); - - // register our input event provider - providerManager.addIQProvider(InputEvtIQ.ELEMENT_NAME, - InputEvtIQ.NAMESPACE, - new InputEvtIQProvider()); - - // register our coin provider - providerManager.addIQProvider(CoinIQ.ELEMENT_NAME, - CoinIQ.NAMESPACE, - new CoinIQProvider()); - supportedFeatures.add(URN_XMPP_JINGLE_COIN); - - // register our JingleInfo provider - providerManager.addIQProvider(JingleInfoQueryIQ.ELEMENT_NAME, - JingleInfoQueryIQ.NAMESPACE, - new JingleInfoQueryIQProvider()); - - // Jitsi Videobridge IQProvider and PacketExtensionProvider - providerManager.addIQProvider( - ColibriConferenceIQ.ELEMENT_NAME, - ColibriConferenceIQ.NAMESPACE, - new ColibriIQProvider()); - - providerManager.addExtensionProvider( - ConferenceDescriptionPacketExtension.ELEMENT_NAME, - ConferenceDescriptionPacketExtension.NAMESPACE, - new ConferenceDescriptionPacketExtension.Provider()); - - providerManager.addExtensionProvider( - CarbonPacketExtension.RECEIVED_ELEMENT_NAME, - CarbonPacketExtension.NAMESPACE, - new CarbonPacketExtension.Provider( - CarbonPacketExtension.RECEIVED_ELEMENT_NAME)); - - providerManager.addExtensionProvider( - CarbonPacketExtension.SENT_ELEMENT_NAME, - CarbonPacketExtension.NAMESPACE, - new CarbonPacketExtension.Provider( - CarbonPacketExtension.SENT_ELEMENT_NAME)); - - //initialize the telephony operation set - boolean isCallingDisabled - = JabberActivator.getConfigurationService() - .getBoolean(IS_CALLING_DISABLED, false); - - boolean isCallingDisabledForAccount - = accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT, - false); - - // Check if calling is enabled. - if (!isCallingDisabled && !isCallingDisabledForAccount) - { - OperationSetBasicTelephonyJabberImpl basicTelephony - = new OperationSetBasicTelephonyJabberImpl(this); - - addSupportedOperationSet( - OperationSetAdvancedTelephony.class, - basicTelephony); - addSupportedOperationSet( - OperationSetBasicTelephony.class, - basicTelephony); - addSupportedOperationSet( - OperationSetSecureZrtpTelephony.class, - basicTelephony); - addSupportedOperationSet( - OperationSetSecureSDesTelephony.class, - basicTelephony); - - // initialize video telephony OperationSet - addSupportedOperationSet( - OperationSetVideoTelephony.class, - new OperationSetVideoTelephonyJabberImpl(basicTelephony)); - - addSupportedOperationSet( - OperationSetTelephonyConferencing.class, - new OperationSetTelephonyConferencingJabberImpl(this)); - - addSupportedOperationSet( - OperationSetBasicAutoAnswer.class, - new OperationSetAutoAnswerJabberImpl(this)); - - addSupportedOperationSet( - OperationSetResourceAwareTelephony.class, - new OperationSetResAwareTelephonyJabberImpl(basicTelephony)); - - // Only init video bridge if enabled - boolean isVideobridgeDisabled - = JabberActivator.getConfigurationService() - .getBoolean(OperationSetVideoBridge. - IS_VIDEO_BRIDGE_DISABLED, false); - - if (!isVideobridgeDisabled) - { - // init video bridge - addSupportedOperationSet( - OperationSetVideoBridge.class, - new OperationSetVideoBridgeImpl(this)); - } - - // init DTMF - OperationSetDTMFJabberImpl operationSetDTMFSip - = new OperationSetDTMFJabberImpl(this); - addSupportedOperationSet( - OperationSetDTMF.class, operationSetDTMFSip); - - addJingleFeatures(); - - // Check if desktop streaming is enabled. - boolean isDesktopStreamingDisabled - = JabberActivator.getConfigurationService() - .getBoolean(IS_DESKTOP_STREAMING_DISABLED, false); - - boolean isAccountDesktopStreamingDisabled - = accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_DESKTOP_STREAMING_DISABLED, - false); - - if (!isDesktopStreamingDisabled - && !isAccountDesktopStreamingDisabled) - { - // initialize desktop streaming OperationSet - addSupportedOperationSet( - OperationSetDesktopStreaming.class, - new OperationSetDesktopStreamingJabberImpl( - basicTelephony)); - - if(!accountID.getAccountPropertyBoolean( - ProtocolProviderFactory - .IS_DESKTOP_REMOTE_CONTROL_DISABLED, - false)) - { - // initialize desktop sharing OperationSets - addSupportedOperationSet( - OperationSetDesktopSharingServer.class, - new OperationSetDesktopSharingServerJabberImpl( - basicTelephony)); - - // Adds extension to support remote control as a sharing - // server (sharer). - supportedFeatures.add(InputEvtIQ.NAMESPACE_SERVER); - - addSupportedOperationSet( - OperationSetDesktopSharingClient.class, - new OperationSetDesktopSharingClientJabberImpl(this) - ); - // Adds extension to support remote control as a sharing - // client (sharer). - supportedFeatures.add(InputEvtIQ.NAMESPACE_CLIENT); - } - } - } - - // OperationSetContactCapabilities - opsetContactCapabilities - = new OperationSetContactCapabilitiesJabberImpl(this); - if (discoveryManager != null) - opsetContactCapabilities.setDiscoveryManager(discoveryManager); - addSupportedOperationSet( - OperationSetContactCapabilities.class, - opsetContactCapabilities); - - addSupportedOperationSet( - OperationSetGenericNotifications.class, - new OperationSetGenericNotificationsJabberImpl(this)); - - supportedFeatures.add("jabber:iq:version"); - if(versionManager == null) - versionManager = new VersionManager(this); - - supportedFeatures.add(MessageCorrectionExtension.NAMESPACE); - addSupportedOperationSet(OperationSetMessageCorrection.class, - basicInstantMessaging); - - OperationSetChangePassword opsetChangePassword - = new OperationSetChangePasswordJabberImpl(this); - addSupportedOperationSet(OperationSetChangePassword.class, - opsetChangePassword); - - OperationSetCusaxUtils opsetCusaxCusaxUtils - = new OperationSetCusaxUtilsJabberImpl(this); - addSupportedOperationSet(OperationSetCusaxUtils.class, - opsetCusaxCusaxUtils); - - boolean isUserSearchEnabled = accountID.getAccountPropertyBoolean( - IS_USER_SEARCH_ENABLED_PROPERTY, false); - if(isUserSearchEnabled) - { - addSupportedOperationSet(OperationSetUserSearch.class, - new OperationSetUserSearchJabberImpl(this)); - } - - OperationSetTLS opsetTLS - = new OperationSetTLSJabberImpl(this); - addSupportedOperationSet(OperationSetTLS.class, - opsetTLS); - - isInitialized = true; - } - } - - /** - * Adds Jingle related features to the supported features. - */ - private void addJingleFeatures() - { - // Add Jingle features to supported features. - supportedFeatures.add(URN_XMPP_JINGLE); - supportedFeatures.add(URN_XMPP_JINGLE_RTP); - supportedFeatures.add(URN_XMPP_JINGLE_RAW_UDP_0); - - /* - * Reflect the preference of the user with respect to the use of - * ICE. - */ - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_USE_ICE, - true)) - { - supportedFeatures.add(URN_XMPP_JINGLE_ICE_UDP_1); - } - - supportedFeatures.add(URN_XMPP_JINGLE_RTP_AUDIO); - supportedFeatures.add(URN_XMPP_JINGLE_RTP_VIDEO); - supportedFeatures.add(URN_XMPP_JINGLE_RTP_ZRTP); - - /* - * Reflect the preference of the user with respect to the use of - * Jingle Nodes. - */ - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactoryJabberImpl.IS_USE_JINGLE_NODES, - true)) - { - supportedFeatures.add(URN_XMPP_JINGLE_NODES); - } - - // XEP-0251: Jingle Session Transfer - supportedFeatures.add(URN_XMPP_JINGLE_TRANSFER_0); - - // XEP-0320: Use of DTLS-SRTP in Jingle Sessions - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME)) - { - supportedFeatures.add(URN_XMPP_JINGLE_DTLS_SRTP); - } - } - - /** - * Makes the service implementation close all open sockets and release - * any resources that it might have taken and prepare for - * shutdown/garbage collection. - */ - public void shutdown() - { - synchronized(initializationLock) - { - if (logger.isTraceEnabled()) - logger.trace("Killing the Jabber Protocol Provider."); - - //kill all active calls - OperationSetBasicTelephonyJabberImpl telephony - = (OperationSetBasicTelephonyJabberImpl)getOperationSet( - OperationSetBasicTelephony.class); - if (telephony != null) - { - telephony.shutdown(); - } - - disconnectAndCleanConnection(); - - isInitialized = false; - } - } - - /** - * Returns true if the provider service implementation is initialized and - * ready for use by other services, and false otherwise. - * - * @return true if the provider is initialized and ready for use and false - * otherwise - */ - public boolean isInitialized() - { - return isInitialized; - } - - /** - * Returns the AccountID that uniquely identifies the account represented - * by this instance of the ProtocolProviderService. - * @return the id of the account represented by this provider. - */ - public AccountID getAccountID() - { - return accountID; - } - - /** - * Returns the XMPPConnectionopened by this provider - * @return a reference to the XMPPConnection last opened by this - * provider. - */ - public XMPPConnection getConnection() - { - return connection; - } - - /** - * Determines whether a specific XMPPException signals that - * attempted authentication has failed. - * - * @param ex the XMPPException which is to be determined whether it - * signals that attempted authentication has failed - * @return true if the specified ex signals that attempted - * authentication has failed; otherwise, false - */ - private boolean isAuthenticationFailed(XMPPException ex) - { - String exMsg = ex.getMessage().toLowerCase(); - - // as there are no types or reasons for XMPPException - // we try determine the reason according to their message - // all messages that were found in smack 3.1.0 were took in count - return - ((exMsg.indexOf("sasl authentication") != -1) - && (exMsg.indexOf("failed") != -1)) - || (exMsg.indexOf( - "does not support compatible authentication mechanism") - != -1) - || (exMsg.indexOf("unable to determine password") != -1); - } - - /** - * Tries to determine the appropriate message and status to fire, - * according the exception. - * - * @param ex the {@link XMPPException} that caused the state change. - */ - private void fireRegistrationStateChanged(XMPPException ex) - { - int reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED; - RegistrationState regState = RegistrationState.UNREGISTERED; - String reasonStr = null; - - Throwable wrappedEx = ex.getWrappedThrowable(); - if(wrappedEx != null - && (wrappedEx instanceof UnknownHostException - || wrappedEx instanceof ConnectException - || wrappedEx instanceof SocketException)) - { - reason = RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND; - regState = RegistrationState.CONNECTION_FAILED; - } - else - { - String exMsg = ex.getMessage().toLowerCase(); - - // as there are no types or reasons for XMPPException - // we try determine the reason according to their message - // all messages that were found in smack 3.1.0 were took in count - if(isAuthenticationFailed(ex)) - { - JabberActivator.getProtocolProviderFactory(). - storePassword(getAccountID(), null); - - reason = RegistrationStateChangeEvent - .REASON_AUTHENTICATION_FAILED; - - regState = RegistrationState.AUTHENTICATION_FAILED; - - fireRegistrationStateChanged( - getRegistrationState(), regState, reason, null); - - // Try to reregister and to ask user for a new password. - reregister(SecurityAuthority.WRONG_PASSWORD); - - return; - } - else if(exMsg.indexOf("no response from the server") != -1 - || exMsg.indexOf("connection failed") != -1) - { - reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED; - regState = RegistrationState.CONNECTION_FAILED; - } - else if(exMsg.indexOf("tls is required") != -1) - { - regState = RegistrationState.AUTHENTICATION_FAILED; - reason = RegistrationStateChangeEvent.REASON_TLS_REQUIRED; - } - } - - if(regState == RegistrationState.UNREGISTERED - || regState == RegistrationState.CONNECTION_FAILED) - { - // we fired that for some reason we are going offline - // lets clean the connection state for any future connections - disconnectAndCleanConnection(); - } - - fireRegistrationStateChanged( - getRegistrationState(), regState, reason, reasonStr); - } - - /** - * Enable to listen for jabber connection events - */ - private class JabberConnectionListener - implements ConnectionListener - { - /** - * Implements connectionClosed from ConnectionListener - */ - public void connectionClosed() - { - // if we are in the middle of connecting process - // do not fire events, will do it later when the method - // connectAndLogin finishes its work - synchronized(connectAndLoginLock) - { - if(inConnectAndLogin) - { - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - null); - return; - } - } - // fire that a connection failed, the reconnection mechanism - // will look after us and will clean us, other wise we can do - // a dead lock (connection closed is called - // within xmppConneciton and calling disconnect again can lock it) - fireRegistrationStateChanged( - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - null); - } - - /** - * Implements connectionClosedOnError from - * ConnectionListener. - * - * @param exception contains information on the error. - */ - public void connectionClosedOnError(Exception exception) - { - logger.error("connectionClosedOnError " + - exception.getLocalizedMessage()); - - if(exception instanceof XMPPException) - { - StreamError err = ((XMPPException)exception).getStreamError(); - - if(err != null && err.getCode().equals( - XMPPError.Condition.conflict.toString())) - { - // if we are in the middle of connecting process - // do not fire events, will do it later when the method - // connectAndLogin finishes its work - synchronized(connectAndLoginLock) - { - if(inConnectAndLogin) - { - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS, - "Connecting multiple times with the same resource"); - return; - } - } - - disconnectAndCleanConnection(); - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS, - "Connecting multiple times with the same resource"); - - return; - } - } // Ignore certificate exceptions as we handle them elsewhere - else if(exception instanceof SSLHandshakeException && - exception.getCause() instanceof CertificateException) - { - return; - } - - // if we are in the middle of connecting process - // do not fire events, will do it later when the method - // connectAndLogin finishes its work - synchronized(connectAndLoginLock) - { - if(inConnectAndLogin) - { - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - exception.getMessage()); - return; - } - } - - disconnectAndCleanConnection(); - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - exception.getMessage()); - } - - /** - * Implements reconnectingIn from ConnectionListener - * - * @param i delay in seconds for reconnection. - */ - public void reconnectingIn(int i) - { - if (logger.isInfoEnabled()) - logger.info("reconnectingIn " + i); - } - - /** - * Implements reconnectingIn from ConnectionListener - */ - public void reconnectionSuccessful() - { - if (logger.isInfoEnabled()) - logger.info("reconnectionSuccessful"); - } - - /** - * Implements reconnectionFailed from - * ConnectionListener. - * - * @param exception description of the failure - */ - public void reconnectionFailed(Exception exception) - { - if (logger.isInfoEnabled()) - logger.info("reconnectionFailed ", exception); - } - } - - /** - * Returns the jabber protocol icon. - * @return the jabber protocol icon - */ - public ProtocolIcon getProtocolIcon() - { - return jabberIcon; - } - - /** - * Returns the current instance of JabberStatusEnum. - * - * @return the current instance of JabberStatusEnum. - */ - JabberStatusEnum getJabberStatusEnum() - { - return jabberStatusEnum; - } - - /** - * Determines if the given list of ext features is supported by the - * specified jabber id. - * - * @param jid the jabber id for which to check - * @param extFeatures the list of ext features to check for - * - * @return true if the list of ext features is supported; - * otherwise, false - */ - public boolean isExtFeatureListSupported(String jid, String... extFeatures) - { - EntityCapsManager capsManager = discoveryManager.getCapsManager(); - EntityCapsManager.Caps caps = capsManager.getCapsByUser(jid); - - String bypassDomain = accountID.getAccountPropertyString( - "TELEPHONY_BYPASS_GTALK_CAPS"); - String domain = StringUtils.parseServer(jid); - boolean domainEquals = domain.equals(bypassDomain); - - if(caps != null && caps.ext != null) - { - String exts[] = caps.ext.split(" "); - boolean found = false; - - for(String extFeature : extFeatures) - { - // in case we have a domain that have to bypass GTalk caps - if(extFeature.equals(CAPS_GTALK_WEB_VOICE) && domainEquals) - { - return true; - } - - found = false; - for(String ext : exts) - { - if(ext.equals(extFeature)) - { - found = true; - break; - } - } - - if(!found) - { - break; - } - } - - return found; - } - - return false; - } - - /** - * Determines if the given list of features is supported by the - * specified jabber id. - * - * @param jid the jabber id for which to check - * @param features the list of features to check for - * - * @return true if the list of features is supported; otherwise, - * false - */ - public boolean isFeatureListSupported(String jid, String... features) - { - boolean isFeatureListSupported = true; - - try - { - if(discoveryManager == null) - return isFeatureListSupported; - - DiscoverInfo featureInfo = - discoveryManager.discoverInfoNonBlocking(jid); - - if(featureInfo == null) - return isFeatureListSupported; - - for (String feature : features) - { - if (!featureInfo.containsFeature(feature)) - { - // If one is not supported we return false and don't check - // the others. - isFeatureListSupported = false; - break; - } - } - } - catch (XMPPException e) - { - if (logger.isDebugEnabled()) - logger.debug("Failed to retrive discovery info.", e); - } - return isFeatureListSupported; - } - - /** - * Determines if the given list of features is supported by the - * specified jabber id. - * - * @param jid the jabber id that we'd like to get information about - * @param feature the feature to check for - * - * @return true if the list of features is supported, otherwise - * returns false - */ - public boolean isFeatureSupported(String jid, String feature) - { - return isFeatureListSupported(jid, feature); - } - - /** - * Returns the full jabber id (jid) corresponding to the given contact. If - * the provider is not connected returns null. - * - * @param contact the contact, for which we're looking for a jid - * @return the jid of the specified contact or null if the provider is not - * yet connected; - */ - public String getFullJid(Contact contact) - { - return getFullJid(contact.getAddress()); - } - - /** - * Returns the full jabber id (jid) corresponding to the given bare jid. If - * the provider is not connected returns null. - * - * @param bareJid the bare contact address (i.e. no resource) whose full - * jid we are looking for. - * @return the jid of the specified contact or null if the provider is not - * yet connected; - */ - public String getFullJid(String bareJid) - { - XMPPConnection connection = getConnection(); - - // when we are not connected there is no full jid - if (connection != null && connection.isConnected()) - { - Roster roster = connection.getRoster(); - - if (roster != null) - return roster.getPresence(bareJid).getFrom(); - } - return null; - } - - /** - * The trust manager which asks the client whether to trust particular - * certificate which is not globally trusted. - */ - private class HostTrustManager - implements X509TrustManager - { - /** - * The default trust manager. - */ - private final X509TrustManager tm; - - /** - * Creates the custom trust manager. - * @param tm the default trust manager. - */ - HostTrustManager(X509TrustManager tm) - { - this.tm = tm; - } - - /** - * Not used. - * - * @return nothing. - */ - public X509Certificate[] getAcceptedIssuers() - { - return new X509Certificate[0]; - } - - /** - * Not used. - * @param chain the cert chain. - * @param authType authentication type like: RSA. - * @throws CertificateException never - * @throws UnsupportedOperationException always - */ - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException, UnsupportedOperationException - { - throw new UnsupportedOperationException(); - } - - /** - * Check whether a certificate is trusted, if not as user whether he - * trust it. - * @param chain the certificate chain. - * @param authType authentication type like: RSA. - * @throws CertificateException not trusted. - */ - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException - { - abortConnecting = true; - try - { - tm.checkServerTrusted(chain, authType); - } - catch(CertificateException e) - { - // notify in a separate thread to avoid a deadlock when a - // reg state listener accesses a synchronized XMPPConnection - // method (like getRoster) - new Thread(new Runnable() - { - public void run() - { - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_USER_REQUEST, - "Not trusted certificate"); - } - }).start(); - throw e; - } - - if(abortConnecting) - { - // connect hasn't finished we will continue normally - abortConnecting = false; - return; - } - else - { - // in this situation connect method has finished - // and it was disconnected so we wont to connect. - - // register.connect in new thread so we can release the - // current connecting thread, otherwise this blocks - // jabber - new Thread(new Runnable() - { - public void run() - { - reregister(SecurityAuthority.CONNECTION_FAILED); - } - }).start(); - return; - } - } - } - - /** - * Returns the currently valid {@link ScServiceDiscoveryManager}. - * - * @return the currently valid {@link ScServiceDiscoveryManager}. - */ - public ScServiceDiscoveryManager getDiscoveryManager() - { - return discoveryManager; - } - - /** - * Returns our own Jabber ID. - * - * @return our own Jabber ID. - */ - public String getOurJID() - { - String jid = null; - - if (connection != null) - jid = connection.getUser(); - - if (jid == null) - { - // seems like the connection is not yet initialized so lets try to - // construct our jid ourselves. - String accountIDUserID = getAccountID().getUserID(); - String userID = StringUtils.parseName(accountIDUserID); - String serviceName = StringUtils.parseServer(accountIDUserID); - - jid = userID + "@" + serviceName; - } - - return jid; - } - - /** - * Returns the InetAddress that is most likely to be to be used - * as a next hop when contacting our XMPP server. This is an utility method - * that is used whenever we have to choose one of our local addresses (e.g. - * when trying to pick a best candidate for raw udp). It is based on the - * assumption that, in absence of any more specific details, chances are - * that we will be accessing remote destinations via the same interface - * that we are using to access our jabber server. - * - * @return the InetAddress that is most likely to be to be used - * as a next hop when contacting our server. - * - * @throws IllegalArgumentException if we don't have a valid server. - */ - public InetAddress getNextHop() - throws IllegalArgumentException - { - InetAddress nextHop = null; - String nextHopStr = null; - - if ( proxy != null - && proxy.getProxyType() - != org.jivesoftware.smack.proxy.ProxyInfo.ProxyType.NONE) - { - nextHopStr = proxy.getProxyAddress(); - } - else - { - nextHopStr = getConnection().getHost(); - } - - try - { - nextHop = NetworkUtils.getInetAddress(nextHopStr); - } - catch (UnknownHostException ex) - { - throw new IllegalArgumentException( - "seems we don't have a valid next hop.", ex); - } - - if(logger.isDebugEnabled()) - logger.debug("Returning address " + nextHop + " as next hop."); - - return nextHop; - } - - /** - * Start auto-discovery of JingleNodes tracker/relays. - */ - public void startJingleNodesDiscovery() - { - // Jingle Nodes Service Initialization - final JabberAccountIDImpl accID = (JabberAccountIDImpl)getAccountID(); - final SmackServiceNode service = new SmackServiceNode(connection, - 60000); - // make sure SmackServiceNode will clean up when connection is closed - connection.addConnectionListener(service); - - for(JingleNodeDescriptor desc : accID.getJingleNodes()) - { - TrackerEntry entry = new TrackerEntry( - desc.isRelaySupported() ? TrackerEntry.Type.relay : - TrackerEntry.Type.tracker, - TrackerEntry.Policy._public, - desc.getJID(), - JingleChannelIQ.UDP); - - service.addTrackerEntry(entry); - } - - new Thread(new JingleNodesServiceDiscovery( - service, - connection, - accID, - jingleNodesSyncRoot)) - .start(); - - jingleNodesServiceNode = service; - } - - /** - * Get the Jingle Nodes service. Note that this method will block until - * Jingle Nodes auto discovery (if enabled) finished. - * - * @return Jingle Nodes service - */ - public SmackServiceNode getJingleNodesServiceNode() - { - synchronized(jingleNodesSyncRoot) - { - return jingleNodesServiceNode; - } - } - - /** - * Logs a specific message and associated Throwable cause as an - * error using the current Logger and then throws a new - * OperationFailedException with the message, a specific error code - * and the cause. - * - * @param message the message to be logged and then wrapped in a new - * OperationFailedException - * @param errorCode the error code to be assigned to the new - * OperationFailedException - * @param cause the Throwable that has caused the necessity to log - * an error and have a new OperationFailedException thrown - * @param logger the logger that we'd like to log the error message - * and cause. - * - * @throws OperationFailedException the exception that we wanted this method - * to throw. - */ - public static void throwOperationFailedException( String message, - int errorCode, - Throwable cause, - Logger logger) - throws OperationFailedException - { - logger.error(message, cause); - - if(cause == null) - throw new OperationFailedException(message, errorCode); - else - throw new OperationFailedException(message, errorCode, cause); - } - - /** - * Used when we need to re-register or someone needs to obtain credentials. - * @return the SecurityAuthority. - */ - public SecurityAuthority getAuthority() - { - return authority; - } - - /** - * Returns true if gtalktesting is enabled, false otherwise. - * - * @return true if gtalktesting is enabled, false otherwise. - */ - public boolean isGTalkTesting() - { - return - Boolean.getBoolean("gtalktesting") - || JabberActivator.getConfigurationService().getBoolean( - "net.java.sip.communicator.impl.protocol.jabber" - + ".gtalktesting", - false) - || accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_USE_GOOGLE_ICE, - true); - } - - UserCredentials getUserCredentials() - { - return userCredentials; - } - - /** - * Returns true if our account is a Gmail or a Google Apps ones. - * - * @return true if our account is a Gmail or a Google Apps ones. - */ - public boolean isGmailOrGoogleAppsAccount() - { - String domain = StringUtils.parseServer( - getAccountID().getUserID()); - return isGmailOrGoogleAppsAccount(domain); - } - - /** - * Returns true if our account is a Gmail or a Google Apps ones. - * - * @param domain domain to check - * @return true if our account is a Gmail or a Google Apps ones. - */ - public static boolean isGmailOrGoogleAppsAccount(String domain) - { - SRVRecord srvRecords[] = null; - - try - { - srvRecords = NetworkUtils.getSRVRecords("xmpp-client", "tcp", - domain); - } - catch (ParseException e) - { - logger.info("Failed to get SRV records for XMPP domain"); - return false; - } - catch (DnssecException e) - { - logger.error("DNSSEC failure while checking for google domains", e); - return false; - } - - if(srvRecords == null) - { - return false; - } - - for(SRVRecord srv : srvRecords) - { - if(srv.getTarget().endsWith("google.com") || - srv.getTarget().endsWith("google.com.")) - { - return true; - } - } - - return false; - } - - /** - * Sets the traffic class for the XMPP signalling socket. - */ - private void setTrafficClass() - { - Socket s = connection.getSocket(); - - if(s != null) - { - ConfigurationService configService = - JabberActivator.getConfigurationService(); - String dscp = configService.getString(XMPP_DSCP_PROPERTY); - - if(dscp != null) - { - try - { - int dscpInt = Integer.parseInt(dscp) << 2; - - if(dscpInt > 0) - s.setTrafficClass(dscpInt); - } - catch (Exception e) - { - logger.info("Failed to set trafficClass", e); - } - } - } - } - - /** - * Gets the entity ID of the first Jitsi Videobridge associated with - * {@link #connection} i.e. provided by the serviceName of - * connection. - * - * @return the entity ID of the first Jitsi Videobridge associated with - * connection - */ - public String getJitsiVideobridge() - { - XMPPConnection connection = getConnection(); - - if (connection != null) - { - ScServiceDiscoveryManager discoveryManager = getDiscoveryManager(); - String serviceName = connection.getServiceName(); - DiscoverItems discoverItems = null; - - try - { - discoverItems = discoveryManager.discoverItems(serviceName); - } - catch (XMPPException xmppe) - { - if (logger.isDebugEnabled()) - { - logger.debug( - "Failed to discover the items associated with" - + " Jabber entity: " + serviceName, - xmppe); - } - } - if (discoverItems != null) - { - Iterator discoverItemIter - = discoverItems.getItems(); - - while (discoverItemIter.hasNext()) - { - DiscoverItems.Item discoverItem = discoverItemIter.next(); - String entityID = discoverItem.getEntityID(); - DiscoverInfo discoverInfo = null; - - try - { - discoverInfo = discoveryManager.discoverInfo(entityID); - } - catch (XMPPException xmppe) - { - logger.warn( - "Failed to discover information about Jabber" - + " entity: " + entityID, - xmppe); - } - if ((discoverInfo != null) - && discoverInfo.containsFeature( - ColibriConferenceIQ.NAMESPACE)) - { - return entityID; - } - } - } - } - - return null; - } - - /** - * Load jabber service class, their static context will register - * what is needed. Used in android as when using the other jars - * these services are loaded from the jar manifest. - */ - private static void loadJabberServiceClasses() - { - if(!OSUtils.IS_ANDROID) - return; - - try - { - // pre-configure smack in android - // just to load class to init their static blocks - SmackConfiguration.getVersion(); - Class.forName(ServiceDiscoveryManager.class.getName()); - - Class.forName(DelayInformation.class.getName()); - Class.forName(org.jivesoftware.smackx - .provider.DelayInformationProvider.class.getName()); - Class.forName(org.jivesoftware.smackx - .bytestreams.socks5.Socks5BytestreamManager.class.getName()); - Class.forName(XHTMLManager.class.getName()); - Class.forName(org.jivesoftware.smackx - .bytestreams.ibb.InBandBytestreamManager.class.getName()); - - } - catch(ClassNotFoundException e) - { - logger.error("Error loading classes in smack", e); - } - } - - /** - * Return the SSL socket (if TLS used). - * @return The SSL socket or null if not used - */ - public SSLSocket getSSLSocket() - { - final SSLSocket result; - final Socket socket = connection.getSocket(); - if (socket instanceof SSLSocket) - { - result = (SSLSocket) socket; - } - else - { - result = null; - } - return result; - } - -} +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.jabber; + +import java.math.*; +import java.net.*; +import java.security.*; +import java.security.cert.*; +import java.text.*; +import java.util.*; + +import javax.net.ssl.*; + +import net.java.sip.communicator.impl.protocol.jabber.debugger.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.caps.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.carbon.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.inputevt.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingleinfo.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.keepalive.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.messagecorrection.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.version.*; +import net.java.sip.communicator.service.certificate.*; +import net.java.sip.communicator.service.dns.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.service.protocol.jabberconstants.*; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.Logger; + +import org.jitsi.service.configuration.*; +import org.jitsi.service.neomedia.*; +import org.jitsi.util.*; +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.*; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.*; +import org.jivesoftware.smackx.packet.*; +import org.osgi.framework.*; +import org.xmpp.jnodes.smack.*; + +/** + * An implementation of the protocol provider service over the Jabber protocol + * + * @author Damian Minkov + * @author Symphorien Wanko + * @author Lyubomir Marinov + * @author Yana Stamcheva + * @author Emil Ivov + */ +public class ProtocolProviderServiceJabberImpl + extends AbstractProtocolProviderService +{ + /** + * Logger of this class + */ + private static final Logger logger = + Logger.getLogger(ProtocolProviderServiceJabberImpl.class); + + /** + * Jingle's Discovery Info common URN. + */ + public static final String URN_XMPP_JINGLE = JingleIQ.NAMESPACE; + + /** + * Jingle's Discovery Info URN for RTP support. + */ + public static final String URN_XMPP_JINGLE_RTP + = RtpDescriptionPacketExtension.NAMESPACE; + + /** + * Jingle's Discovery Info URN for RTP support with audio. + */ + public static final String URN_XMPP_JINGLE_RTP_AUDIO + = "urn:xmpp:jingle:apps:rtp:audio"; + + /** + * Jingle's Discovery Info URN for RTP support with video. + */ + public static final String URN_XMPP_JINGLE_RTP_VIDEO + = "urn:xmpp:jingle:apps:rtp:video"; + + /** + * Jingle's Discovery Info URN for ZRTP support with RTP. + */ + public static final String URN_XMPP_JINGLE_RTP_ZRTP + = ZrtpHashPacketExtension.NAMESPACE; + + /** + * Jingle's Discovery Info URN for ICE_UDP transport support. + */ + public static final String URN_XMPP_JINGLE_RAW_UDP_0 + = RawUdpTransportPacketExtension.NAMESPACE; + + /** + * Jingle's Discovery Info URN for ICE_UDP transport support. + */ + public static final String URN_XMPP_JINGLE_ICE_UDP_1 + = IceUdpTransportPacketExtension.NAMESPACE; + + /** + * Jingle's Discovery Info URN for Jingle Nodes support. + */ + public static final String URN_XMPP_JINGLE_NODES + = "http://jabber.org/protocol/jinglenodes"; + + /** + * Jingle's Discovery Info URN for "XEP-0251: Jingle Session Transfer" + * support. + */ + public static final String URN_XMPP_JINGLE_TRANSFER_0 + = TransferPacketExtension.NAMESPACE; + + /** + * Jingle's Discovery Info URN for "XEP-298 :Delivering Conference + * Information to Jingle Participants (Coin)" support. + */ + public static final String URN_XMPP_JINGLE_COIN = "urn:xmpp:coin"; + + /** + * Jingle's Discovery Info URN for "XEP-0320: Use of DTLS-SRTP in + * Jingle Sessions". + */ + public static final String URN_XMPP_JINGLE_DTLS_SRTP + = "urn:xmpp:jingle:apps:dtls:0"; + + /** + * Discovery Info URN for classic RFC3264-style Offer/Answer negotiation + * with no support for Trickle ICE and low tolerance to transport/payload + * separation. Defined in XEP-0176 + */ + public static final String URN_IETF_RFC_3264 = "urn:ietf:rfc:3264"; + + /** + * Jingle's Discovery Info URN for "XEP-0294: Jingle RTP Header Extensions + * Negotiation" support. + */ + public static final String URN_XMPP_JINGLE_RTP_HDREXT = + "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + + /** + * Capabilities name for audio call in Google Talk web version. + */ + public static final String CAPS_GTALK_WEB_VOICE = "voice-v1"; + + /** + * Capabilities name for video call (receive side) in Google Talk web + * version. + */ + public static final String CAPS_GTALK_WEB_VIDEO = "video-v1"; + + /** + * Capabilities name for video call (sender side) in Google Talk web + * version. + */ + public static final String CAPS_GTALK_WEB_CAMERA = "camera-v1"; + + /** + * URN for Google voice. + */ + public static final String URN_GOOGLE_VOICE = + "http://www.google.com/xmpp/protocol/voice/v1"; + + /** + * URN for Google camera. + */ + public static final String URN_GOOGLE_CAMERA = + "http://www.google.com/xmpp/protocol/camera/v1"; + + /** + * URN for Google video. + */ + public static final String URN_GOOGLE_VIDEO = + "http://www.google.com/xmpp/protocol/video/v1"; + + /** + * URN for XEP-0077 inband registration + */ + public static final String URN_REGISTER = "jabber:iq:register"; + + /** + * The name of the property under which the user may specify if the desktop + * streaming or sharing should be disabled. + */ + private static final String IS_DESKTOP_STREAMING_DISABLED + = "net.java.sip.communicator.impl.protocol.jabber." + + "DESKTOP_STREAMING_DISABLED"; + + /** + * The name of the property under which the user may specify if audio/video + * calls should be disabled. + */ + private static final String IS_CALLING_DISABLED + = "net.java.sip.communicator.impl.protocol.jabber.CALLING_DISABLED"; + + /** + * Smack packet reply timeout. + */ + public static final int SMACK_PACKET_REPLY_TIMEOUT = 45000; + + /** + * Property for vcard reply timeout. Time to wait before + * we think vcard retrieving has timeouted, default value + * of smack is 5000 (5 sec.). + */ + public static final String VCARD_REPLY_TIMEOUT_PROPERTY = + "net.java.sip.communicator.impl.protocol.jabber.VCARD_REPLY_TIMEOUT"; + + /** + * XMPP signaling DSCP configuration property name. + */ + private static final String XMPP_DSCP_PROPERTY = + "net.java.sip.communicator.impl.protocol.XMPP_DSCP"; + + /** + * Indicates if user search is disabled. + */ + private static final String IS_USER_SEARCH_ENABLED_PROPERTY + = "USER_SEARCH_ENABLED"; + + /** + * Google voice domain name. + */ + public static final String GOOGLE_VOICE_DOMAIN = "voice.google.com"; + + /** + * Used to connect to a XMPP server. + */ + private XMPPConnection connection; + + /** + * Indicates whether or not the provider is initialized and ready for use. + */ + private boolean isInitialized = false; + + /** + * We use this to lock access to initialization. + */ + private final Object initializationLock = new Object(); + + /** + * The identifier of the account that this provider represents. + */ + private AccountID accountID = null; + + /** + * Used when we need to re-register + */ + private SecurityAuthority authority = null; + + /** + * The resource we will use when connecting during this run. + */ + private String resource = null; + + /** + * The icon corresponding to the jabber protocol. + */ + private ProtocolIconJabberImpl jabberIcon; + + /** + * A set of features supported by our Jabber implementation. + * In general, we add new feature(s) when we add new operation sets. + * (see xep-0030 : http://www.xmpp.org/extensions/xep-0030.html#info). + * Example : to tell the world that we support jingle, we simply have + * to do : + * supportedFeatures.add("http://www.xmpp.org/extensions/xep-0166.html#ns"); + * Beware there is no canonical mapping between op set and jabber features + * (op set is a SC "concept"). This means that one op set in SC can + * correspond to many jabber features. It is also possible that there is no + * jabber feature corresponding to a SC op set or again, + * we can currently support some features wich do not have a specific + * op set in SC (the mandatory feature : + * http://jabber.org/protocol/disco#info is one example). + * We can find features corresponding to op set in the xep(s) related + * to implemented functionality. + */ + private final List supportedFeatures = new ArrayList(); + + /** + * The ServiceDiscoveryManager is responsible for advertising + * supportedFeatures when asked by a remote client. It can also + * be used to query remote clients for supported features. + */ + private ScServiceDiscoveryManager discoveryManager = null; + + /** + * The OperationSetContactCapabilities of this + * ProtocolProviderService which is the service-public counterpart + * of {@link #discoveryManager}. + */ + private OperationSetContactCapabilitiesJabberImpl opsetContactCapabilities; + + /** + * The statuses. + */ + private JabberStatusEnum jabberStatusEnum; + + /** + * The service we use to interact with user. + */ + private CertificateService guiVerification; + + /** + * Used with tls connecting when certificates are not trusted + * and we ask the user to confirm connection. When some timeout expires + * connect method returns, and we use abortConnecting to abort further + * execution cause after user chooses we make further processing from there. + */ + private boolean abortConnecting = false; + + /** + * Flag indicating are we currently executing connectAndLogin method. + */ + private boolean inConnectAndLogin = false; + + /** + * Object used to synchronize the flag inConnectAndLogin. + */ + private final Object connectAndLoginLock = new Object(); + + /** + * If an event occurs during login we fire it at the end of the login + * process (at the end of connectAndLogin method). + */ + private RegistrationStateChangeEvent eventDuringLogin; + + /** + * Listens for connection closes or errors. + */ + private JabberConnectionListener connectionListener; + + /** + * The details of the proxy we are using to connect to the server (if any) + */ + private org.jivesoftware.smack.proxy.ProxyInfo proxy; + + /** + * Our provider manager instances. + */ + private static ProviderManager providerManager = null; + + /** + * Lock for creating provider. + */ + private static Object providerCreationLock = new Object(); + + /** + * State for connect and login state. + */ + enum ConnectState + { + /** + * Abort any further connecting. + */ + ABORT_CONNECTING, + /** + * Continue trying with next address. + */ + CONTINUE_TRYING, + /** + * Stop trying we succeeded or just have a final state for + * the whole connecting procedure. + */ + STOP_TRYING + } + + /** + * The debugger who logs packets. + */ + private SmackPacketDebugger debugger = null; + + /** + * Jingle Nodes service. + */ + private SmackServiceNode jingleNodesServiceNode = null; + + /** + * Synchronization object to monitore jingle nodes auto discovery. + */ + private final Object jingleNodesSyncRoot = new Object(); + + /** + * Stores user credentials for local use if user hasn't stored + * its password. + */ + private UserCredentials userCredentials = null; + + /** + * The currently running keepAliveManager if enabled. + */ + private KeepAliveManager keepAliveManager = null; + + /** + * The version manager. + */ + private VersionManager versionManager = null; + + // load xmpp manager classes + static + { + if(OSUtils.IS_ANDROID) + loadJabberServiceClasses(); + } + + /** + * Returns the state of the registration of this protocol provider + * @return the RegistrationState that this provider is + * currently in or null in case it is in a unknown state. + */ + public RegistrationState getRegistrationState() + { + if(connection == null) + return RegistrationState.UNREGISTERED; + else if(connection.isConnected() && connection.isAuthenticated()) + return RegistrationState.REGISTERED; + else + return RegistrationState.UNREGISTERED; + } + + /** + * Return the certificate verification service impl. + * @return the CertificateVerification service. + */ + private CertificateService getCertificateVerificationService() + { + if(guiVerification == null) + { + ServiceReference guiVerifyReference + = JabberActivator.getBundleContext().getServiceReference( + CertificateService.class.getName()); + if(guiVerifyReference != null) + guiVerification = (CertificateService) + JabberActivator.getBundleContext().getService( + guiVerifyReference); + } + + return guiVerification; + } + + /** + * Starts the registration process. Connection details such as + * registration server, user name/number are provided through the + * configuration service through implementation specific properties. + * + * @param authority the security authority that will be used for resolving + * any security challenges that may be returned during the + * registration or at any moment while we're registered. + * @throws OperationFailedException with the corresponding code it the + * registration fails for some reason (e.g. a networking error or an + * implementation problem). + */ + public void register(final SecurityAuthority authority) + throws OperationFailedException + { + if(authority == null) + throw new IllegalArgumentException( + "The register method needs a valid non-null authority impl " + + " in order to be able and retrieve passwords."); + + this.authority = authority; + + try + { + // reset states + abortConnecting = false; + + // indicate we started connectAndLogin process + synchronized(connectAndLoginLock) + { + inConnectAndLogin = true; + } + + initializeConnectAndLogin(authority, + SecurityAuthority.AUTHENTICATION_REQUIRED); + } + catch (XMPPException ex) + { + logger.error("Error registering", ex); + + eventDuringLogin = null; + + fireRegistrationStateChanged(ex); + } + finally + { + synchronized(connectAndLoginLock) + { + // Checks if an error has occurred during login, if so we fire + // it here in order to avoid a deadlock which occurs in + // reconnect plugin. The deadlock is cause we fired an event + // during login process and have locked initializationLock and + // we cannot unregister from reconnect, cause unregister method + // also needs this lock. + if(eventDuringLogin != null) + { + if(eventDuringLogin.getNewState().equals( + RegistrationState.CONNECTION_FAILED) || + eventDuringLogin.getNewState().equals( + RegistrationState.UNREGISTERED)) + disconnectAndCleanConnection(); + + fireRegistrationStateChanged( + eventDuringLogin.getOldState(), + eventDuringLogin.getNewState(), + eventDuringLogin.getReasonCode(), + eventDuringLogin.getReason()); + + eventDuringLogin = null; + inConnectAndLogin = false; + return; + } + + inConnectAndLogin = false; + } + } + } + + /** + * Connects and logins again to the server. + * + * @param authReasonCode indicates the reason of the re-authentication. + */ + void reregister(int authReasonCode) + { + try + { + if (logger.isTraceEnabled()) + logger.trace("Trying to reregister us!"); + + // sets this if any is trying to use us through registration + // to know we are not registered + this.unregister(false); + + // reset states + this.abortConnecting = false; + + // indicate we started connectAndLogin process + synchronized(connectAndLoginLock) + { + inConnectAndLogin = true; + } + + initializeConnectAndLogin(authority, authReasonCode); + } + catch(OperationFailedException ex) + { + logger.error("Error ReRegistering", ex); + + eventDuringLogin = null; + + disconnectAndCleanConnection(); + + fireRegistrationStateChanged(getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, null); + } + catch (XMPPException ex) + { + logger.error("Error ReRegistering", ex); + + eventDuringLogin = null; + + fireRegistrationStateChanged(ex); + } + finally + { + synchronized(connectAndLoginLock) + { + // Checks if an error has occurred during login, if so we fire + // it here in order to avoid a deadlock which occurs in + // reconnect plugin. The deadlock is cause we fired an event + // during login process and have locked initializationLock and + // we cannot unregister from reconnect, cause unregister method + // also needs this lock. + if(eventDuringLogin != null) + { + if(eventDuringLogin.getNewState().equals( + RegistrationState.CONNECTION_FAILED) || + eventDuringLogin.getNewState().equals( + RegistrationState.UNREGISTERED)) + disconnectAndCleanConnection(); + + fireRegistrationStateChanged( + eventDuringLogin.getOldState(), + eventDuringLogin.getNewState(), + eventDuringLogin.getReasonCode(), + eventDuringLogin.getReason()); + + eventDuringLogin = null; + inConnectAndLogin = false; + return; + } + + inConnectAndLogin = false; + } + } + } + + /** + * Indicates if the XMPP transport channel is using a TLS secured socket. + * + * @return True when TLS is used, false otherwise. + */ + public boolean isSignalingTransportSecure() + { + return connection != null && connection.isUsingTLS(); + } + + /** + * Returns the "transport" protocol of this instance used to carry the + * control channel for the current protocol service. + * + * @return The "transport" protocol of this instance: TCP, TLS or UNKNOWN. + */ + public TransportProtocol getTransportProtocol() + { + // Without a connection, there is no transport available. + if(connection != null && connection.isConnected()) + { + // Transport using a secure connection. + if(connection.isUsingTLS()) + { + return TransportProtocol.TLS; + } + // Transport using a unsecure connection. + return TransportProtocol.TCP; + } + return TransportProtocol.UNKNOWN; + } + + /** + * Connects and logins to the server + * @param authority SecurityAuthority + * @param reasonCode the authentication reason code. Indicates the reason of + * this authentication. + * @throws XMPPException if we cannot connect to the server - network problem + * @throws OperationFailedException if login parameters + * as server port are not correct + */ + private void initializeConnectAndLogin(SecurityAuthority authority, + int reasonCode) + throws XMPPException, OperationFailedException + { + synchronized(initializationLock) + { + // if a thread is waiting for initializationLock and enters + // lets check whether someone hasn't already tried login and + // have succeeded, + // should prevent "Trace possible duplicate connections" prints + if(isRegistered()) + return; + + JabberLoginStrategy loginStrategy = createLoginStrategy(); + userCredentials = loginStrategy.prepareLogin(authority, reasonCode); + if(!loginStrategy.loginPreparationSuccessful()) + return; + + String serviceName + = StringUtils.parseServer(getAccountID().getUserID()); + + loadResource(); + loadProxy(); + Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual); + + ConnectState state; + //[0] = hadDnsSecException + boolean[] hadDnsSecException = new boolean[]{false}; + + // try connecting with auto-detection if enabled + boolean isServerOverriden = + getAccountID().getAccountPropertyBoolean( + ProtocolProviderFactory.IS_SERVER_OVERRIDDEN, false); + + if(!isServerOverriden) + { + state = connectUsingSRVRecords(serviceName, + serviceName, hadDnsSecException, loginStrategy); + if(hadDnsSecException[0]) + { + setDnssecLoginFailure(); + return; + } + if(state == ConnectState.ABORT_CONNECTING + || state == ConnectState.STOP_TRYING) + return; + } + + // check for custom xmpp domain which we will check for + // SRV records for server addresses + String customXMPPDomain = getAccountID() + .getAccountPropertyString("CUSTOM_XMPP_DOMAIN"); + + if(customXMPPDomain != null && !hadDnsSecException[0]) + { + logger.info("Connect using custom xmpp domain: " + + customXMPPDomain); + + state = connectUsingSRVRecords( + customXMPPDomain, serviceName, + hadDnsSecException, loginStrategy); + + logger.info("state for connectUsingSRVRecords: " + state); + + if(hadDnsSecException[0]) + { + setDnssecLoginFailure(); + return; + } + + if(state == ConnectState.ABORT_CONNECTING + || state == ConnectState.STOP_TRYING) + return; + } + + // connect with specified server name + String serverAddressUserSetting + = getAccountID().getAccountPropertyString( + ProtocolProviderFactory.SERVER_ADDRESS); + + int serverPort = getAccountID().getAccountPropertyInt( + ProtocolProviderFactory.SERVER_PORT, 5222); + + InetSocketAddress[] addrs = null; + try + { + addrs = NetworkUtils.getAandAAAARecords( + serverAddressUserSetting, + serverPort + ); + } + catch (ParseException e) + { + logger.error("Domain not resolved", e); + } + catch (DnssecException e) + { + logger.error("DNSSEC failure for overridden server", e); + setDnssecLoginFailure(); + return; + } + + if (addrs == null || addrs.length == 0) + { + logger.error("No server addresses found"); + + eventDuringLogin = null; + + fireRegistrationStateChanged( + getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, + "No server addresses found"); + } + else + { + for (InetSocketAddress isa : addrs) + { + try + { + state = connectAndLogin(isa, serviceName, + loginStrategy); + if(state == ConnectState.ABORT_CONNECTING + || state == ConnectState.STOP_TRYING) + return; + } + catch(XMPPException ex) + { + disconnectAndCleanConnection(); + if(isAuthenticationFailed(ex)) + throw ex; + } + } + } + } + } + + /** + * Creates the JabberLoginStrategy to use for the current account. + */ + private JabberLoginStrategy createLoginStrategy() + { + String clientCertId = getAccountID().getAccountPropertyString( + ProtocolProviderFactory.CLIENT_TLS_CERTIFICATE); + if(clientCertId != null) + { + return new LoginByClientCertificateStrategy(getAccountID()); + } + else + { + return new LoginByPasswordStrategy(this, getAccountID()); + } + } + + private void setDnssecLoginFailure() + { + eventDuringLogin = new RegistrationStateChangeEvent( + this, + getRegistrationState(), + RegistrationState.UNREGISTERED, + RegistrationStateChangeEvent.REASON_USER_REQUEST, + "No usable host found due to DNSSEC failures"); + } + + /** + * Connects using the domain specified and its SRV records. + * @param domain the domain to use + * @param serviceName the domain name of the user's login + * @param dnssecState state of possible received DNSSEC exceptions + * @param loginStrategy the login strategy to use + * @return whether to continue trying or stop. + */ + private ConnectState connectUsingSRVRecords( + String domain, + String serviceName, + boolean[] dnssecState, + JabberLoginStrategy loginStrategy) + throws XMPPException + { + // check to see is there SRV records for this server domain + SRVRecord srvRecords[] = null; + try + { + srvRecords = NetworkUtils + .getSRVRecords("xmpp-client", "tcp", domain); + } + catch (ParseException e) + { + logger.error("SRV record not resolved", e); + } + catch (DnssecException e) + { + logger.error("DNSSEC failure for SRV lookup", e); + dnssecState[0] = true; + } + + if(srvRecords != null) + { + for(SRVRecord srv : srvRecords) + { + InetSocketAddress[] addrs = null; + try + { + addrs = + NetworkUtils.getAandAAAARecords( + srv.getTarget(), + srv.getPort() + ); + } + catch (ParseException e) + { + logger.error("Invalid SRV record target", e); + } + catch (DnssecException e) + { + logger.error("DNSSEC failure for A/AAAA lookup of SRV", e); + dnssecState[0] = true; + } + + if (addrs == null || addrs.length == 0) + { + logger.error("No A/AAAA addresses found for " + + srv.getTarget()); + continue; + } + + for (InetSocketAddress isa : addrs) + { + try + { + // if failover mechanism is enabled, use it, + // default is not enabled. + if(JabberActivator.getConfigurationService() + .getBoolean(FailoverConnectionMonitor + .REVERSE_FAILOVER_ENABLED_PROP, + false + )) + { + FailoverConnectionMonitor.getInstance(this) + .setCurrent(domain, srv.getTarget()); + } + + ConnectState state = connectAndLogin( + isa, serviceName, loginStrategy); + return state; + } + catch(XMPPException ex) + { + logger.error("Error connecting to " + isa + + " for domain:" + domain + + " serviceName:" + serviceName, ex); + + disconnectAndCleanConnection(); + + if(isAuthenticationFailed(ex)) + throw ex; + } + } + } + } + else + logger.error("No SRV addresses found for _xmpp-client._tcp." + + domain); + + return ConnectState.CONTINUE_TRYING; + } + + /** + * Tries to login to the XMPP server with the supplied user ID. If the + * protocol is Google Talk, the user ID including the service name is used. + * For other protocols, if the login with the user ID without the service + * name fails, a second attempt including the service name is made. + * + * @param currentAddress the IP address to connect to + * @param serviceName the domain name of the user's login + * @param loginStrategy the login strategy to use + * @throws XMPPException when a failure occurs + */ + private ConnectState connectAndLogin(InetSocketAddress currentAddress, + String serviceName, + JabberLoginStrategy loginStrategy) + throws XMPPException + { + String userID = null; + boolean qualifiedUserID; + + /* with a google account (either gmail or google apps + * related ones), the userID MUST be the full e-mail address + * not just the ID + */ + if(getAccountID().getProtocolDisplayName().equals("Google Talk")) + { + userID = getAccountID().getUserID(); + qualifiedUserID = true; + } + else + { + userID = StringUtils.parseName(getAccountID().getUserID()); + qualifiedUserID = false; + } + + try + { + return connectAndLogin( + currentAddress, serviceName, + userID, resource, loginStrategy); + } + catch(XMPPException ex) + { + // server disconnect us after such an error, do cleanup + disconnectAndCleanConnection(); + + //no need to check with a different username if the + //socket could not be opened + if (ex.getWrappedThrowable() instanceof ConnectException + || ex.getWrappedThrowable() instanceof NoRouteToHostException) + { + //as we got an exception not handled in connectAndLogin + //no state was set, so fire it here so we can continue + //with the re-register process + //2013-08-07 do not fire event, if we have several + // addresses and we fire event will activate reconnect + // but we will continue connecting with other addresses + // and can register with address, then unregister and try again + // that is from reconnect plugin. + // Storing event for fire after all have failed and we have + // tried every address. + eventDuringLogin = new RegistrationStateChangeEvent( + ProtocolProviderServiceJabberImpl.this, + getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, + null); + + throw ex; + } + + // don't attempt to append the service name if it's already there + if (!qualifiedUserID) + { + try + { + // logging in might need the service name + return connectAndLogin( + currentAddress, serviceName, + userID + "@" + serviceName, + resource, + loginStrategy); + } + catch(XMPPException ex2) + { + disconnectAndCleanConnection(); + throw ex; //throw the original exception + } + } + else + throw ex; + } + } + + /** + * Initializes the Jabber Resource identifier. + */ + private void loadResource() + { + if(resource == null) + { + String defaultResource = "jitsi"; + String autoGenenerateResource = + getAccountID().getAccountPropertyString( + ProtocolProviderFactory.AUTO_GENERATE_RESOURCE); + if(autoGenenerateResource == null || + Boolean.parseBoolean(autoGenenerateResource)) + { + SecureRandom random = new SecureRandom(); + + resource = defaultResource + "-" + + new BigInteger(32, random).toString(32); + } + else + { + resource = getAccountID().getAccountPropertyString( + ProtocolProviderFactory.RESOURCE); + + if(resource == null || resource.length() == 0) + resource = defaultResource; + } + } + } + + /** + * Sets the global proxy information based on the configuration + * + * @throws OperationFailedException + */ + private void loadProxy() throws OperationFailedException + { + String globalProxyType = + JabberActivator.getConfigurationService() + .getString(ProxyInfo.CONNECTION_PROXY_TYPE_PROPERTY_NAME); + if(globalProxyType == null || + globalProxyType.equals(ProxyInfo.ProxyType.NONE.name())) + { + proxy = org.jivesoftware.smack.proxy.ProxyInfo.forNoProxy(); + } + else + { + String globalProxyAddress = + JabberActivator.getConfigurationService().getString( + ProxyInfo.CONNECTION_PROXY_ADDRESS_PROPERTY_NAME); + String globalProxyPortStr = + JabberActivator.getConfigurationService().getString( + ProxyInfo.CONNECTION_PROXY_PORT_PROPERTY_NAME); + int globalProxyPort; + try + { + globalProxyPort = Integer.parseInt( + globalProxyPortStr); + } + catch(NumberFormatException ex) + { + throw new OperationFailedException("Wrong proxy port, " + + globalProxyPortStr + + " does not represent an integer", + OperationFailedException.INVALID_ACCOUNT_PROPERTIES, + ex); + } + String globalProxyUsername = + JabberActivator.getConfigurationService().getString( + ProxyInfo.CONNECTION_PROXY_USERNAME_PROPERTY_NAME); + String globalProxyPassword = + JabberActivator.getConfigurationService().getString( + ProxyInfo.CONNECTION_PROXY_PASSWORD_PROPERTY_NAME); + if(globalProxyAddress == null || + globalProxyAddress.length() <= 0) + { + throw new OperationFailedException( + "Missing Proxy Address", + OperationFailedException.INVALID_ACCOUNT_PROPERTIES); + } + try + { + proxy = new org.jivesoftware.smack.proxy.ProxyInfo( + Enum.valueOf(org.jivesoftware.smack.proxy.ProxyInfo. + ProxyType.class, globalProxyType), + globalProxyAddress, globalProxyPort, + globalProxyUsername, globalProxyPassword); + } + catch(IllegalArgumentException e) + { + logger.error("Invalid value for smack proxy enum", e); + proxy = null; + } + } + } + + /** + * Connects xmpp connection and login. Returning the state whether is it + * final - Abort due to certificate cancel or keep trying cause only current + * address has failed or stop trying cause we succeeded. + * @param address the address to connect to + * @param serviceName the service name to use + * @param userName the username to use + * @param resource and the resource. + * @param loginStrategy the login strategy to use + * @return return the state how to continue the connect process. + * @throws XMPPException if we cannot connect for some reason + */ + private ConnectState connectAndLogin( + InetSocketAddress address, String serviceName, + String userName, String resource, + JabberLoginStrategy loginStrategy) + throws XMPPException + { + ConnectionConfiguration confConn = new ConnectionConfiguration( + address.getAddress().getHostAddress(), + address.getPort(), + serviceName, proxy + ); + + confConn.setReconnectionAllowed(false); + boolean tlsRequired = loginStrategy.isTlsRequired(); + + // user have the possibility to disable TLS but in this case, it will + // not be able to connect to a server which requires TLS + confConn.setSecurityMode( + tlsRequired ? ConnectionConfiguration.SecurityMode.required : + ConnectionConfiguration.SecurityMode.enabled); + + if(connection != null) + { + logger.error("Connection is not null and isConnected:" + + connection.isConnected(), + new Exception("Trace possible duplicate connections: " + + getAccountID().getAccountAddress())); + disconnectAndCleanConnection(); + } + + connection = new XMPPConnection(confConn); + + try + { + CertificateService cvs = + getCertificateVerificationService(); + if(cvs != null) + { + SSLContext sslContext = loginStrategy.createSslContext(cvs, + getTrustManager(cvs, serviceName)); + + // log SSL/TLS algorithms and protocols + if (logger.isDebugEnabled()) + { + final StringBuilder buff = new StringBuilder(); + buff.append("Available TLS protocols and algorithms:\n"); + buff.append("Default protocols: "); + buff.append(Arrays.toString( + sslContext.getDefaultSSLParameters().getProtocols())); + buff.append("\n"); + buff.append("Supported protocols: "); + buff.append(Arrays.toString( + sslContext.getSupportedSSLParameters().getProtocols())); + buff.append("\n"); + buff.append("Default cipher suites: "); + buff.append(Arrays.toString( + sslContext.getDefaultSSLParameters() + .getCipherSuites())); + buff.append("\n"); + buff.append("Supported cipher suites: "); + buff.append(Arrays.toString( + sslContext.getSupportedSSLParameters() + .getCipherSuites())); + logger.debug(buff.toString()); + } + + connection.setCustomSslContext(sslContext); + } + else if (tlsRequired) + throw new XMPPException( + "Certificate verification service is " + + "unavailable and TLS is required"); + } + catch(GeneralSecurityException e) + { + logger.error("Error creating custom trust manager", e); + throw new XMPPException("Error creating custom trust manager", e); + } + + if(debugger == null) + debugger = new SmackPacketDebugger(); + + // sets the debugger + debugger.setConnection(connection); + connection.addPacketListener(debugger, null); + connection.addPacketInterceptor(debugger, null); + + connection.connect(); + + setTrafficClass(); + + if(abortConnecting) + { + abortConnecting = false; + disconnectAndCleanConnection(); + + return ConnectState.ABORT_CONNECTING; + } + + registerServiceDiscoveryManager(); + + if(connectionListener == null) + { + connectionListener = new JabberConnectionListener(); + } + + if(!connection.isSecureConnection() && tlsRequired) + { + throw new XMPPException("TLS is required by client"); + } + + if(!connection.isConnected()) + { + // connection is not connected, lets set state to our connection + // as failed seems there is some lag/problem with network + // and this way we will inform for it and later reconnect if needed + // as IllegalStateException that is thrown within + // addConnectionListener is not handled properly + disconnectAndCleanConnection(); + + logger.error("Connection not established, server not found!"); + + eventDuringLogin = null; + + fireRegistrationStateChanged(getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, null); + + return ConnectState.ABORT_CONNECTING; + } + else + { + if (connection.getSocket() instanceof SSLSocket) + { + final SSLSocket sslSocket = (SSLSocket) connection.getSocket(); + StringBuilder buff = new StringBuilder(); + buff.append("Chosen TLS protocol and algorithm:\n") + .append("Protocol: ").append(sslSocket.getSession() + .getProtocol()).append("\n") + .append("Cipher suite: ").append(sslSocket.getSession() + .getCipherSuite()); + logger.info(buff.toString()); + + if (logger.isDebugEnabled()) + { + buff = new StringBuilder(); + buff.append("Server TLS certificate chain:\n"); + try + { + buff.append(Arrays.toString( + sslSocket.getSession().getPeerCertificates())); + } + catch (SSLPeerUnverifiedException ex) + { + buff.append(""); + } + logger.debug(buff.toString()); + } + } + + connection.addConnectionListener(connectionListener); + } + + if(abortConnecting) + { + abortConnecting = false; + disconnectAndCleanConnection(); + + return ConnectState.ABORT_CONNECTING; + } + + fireRegistrationStateChanged( + getRegistrationState() + , RegistrationState.REGISTERING + , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED + , null); + + if (!loginStrategy.login(connection, userName, resource)) + { + disconnectAndCleanConnection(); + eventDuringLogin = null; + fireRegistrationStateChanged( + getRegistrationState(), + // not auth failed, or there would be no info-popup + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED, + loginStrategy.getClass().getName() + " requests abort"); + + return ConnectState.ABORT_CONNECTING; + } + + if(connection.isAuthenticated()) + { + eventDuringLogin = null; + + fireRegistrationStateChanged( + getRegistrationState(), + RegistrationState.REGISTERED, + RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null); + + return ConnectState.STOP_TRYING; + } + else + { + disconnectAndCleanConnection(); + + eventDuringLogin = null; + + fireRegistrationStateChanged( + getRegistrationState() + , RegistrationState.UNREGISTERED + , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED + , null); + + return ConnectState.CONTINUE_TRYING; + } + } + + /** + * Gets the TrustManager that should be used for the specified service + * + * @param serviceName the service name + * @param cvs The CertificateVerificationService to retrieve the + * trust manager + * @return the trust manager + */ + private X509TrustManager getTrustManager(CertificateService cvs, + String serviceName) + throws GeneralSecurityException + { + return new HostTrustManager( + cvs.getTrustManager( + Arrays.asList(new String[]{ + serviceName, + "_xmpp-client." + serviceName + }) + ) + ); + } + + /** + * Registers our ServiceDiscoveryManager + */ + private void registerServiceDiscoveryManager() + { + // we setup supported features no packets are actually sent + //during feature registration so we'd better do it here so that + //our first presence update would contain a caps with the right + //features. + String name + = System.getProperty( + "sip-communicator.application.name", + "SIP Communicator ") + + System.getProperty("sip-communicator.version","SVN"); + + ServiceDiscoveryManager.setIdentityName(name); + ServiceDiscoveryManager.setIdentityType("pc"); + + discoveryManager + = new ScServiceDiscoveryManager( + this, + new String[] { "http://jabber.org/protocol/commands"}, + // Add features Jitsi supports in addition to smack. + supportedFeatures.toArray( + new String[supportedFeatures.size()])); + + boolean isCallingDisabled + = JabberActivator.getConfigurationService() + .getBoolean(IS_CALLING_DISABLED, false); + + boolean isCallingDisabledForAccount = false; + if (accountID != null && accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT, + false)) + isCallingDisabled = true; + + if(isGTalkTesting() + && !isCallingDisabled + && !isCallingDisabledForAccount) + { + // Add Google Talk "ext" capabilities + discoveryManager.addExtFeature(CAPS_GTALK_WEB_VOICE); + discoveryManager.addExtFeature(CAPS_GTALK_WEB_VIDEO); + discoveryManager.addExtFeature(CAPS_GTALK_WEB_CAMERA); + discoveryManager.addFeature(URN_GOOGLE_VOICE); + discoveryManager.addFeature(URN_GOOGLE_VIDEO); + discoveryManager.addFeature(URN_GOOGLE_CAMERA); + } + + /* + * Expose the discoveryManager as service-public through the + * OperationSetContactCapabilities of this ProtocolProviderService. + */ + if (opsetContactCapabilities != null) + opsetContactCapabilities.setDiscoveryManager(discoveryManager); + } + + /** + * Used to disconnect current connection and clean it. + */ + public void disconnectAndCleanConnection() + { + if(connection != null) + { + connection.removeConnectionListener(connectionListener); + + // disconnect anyway cause it will clear any listeners + // that maybe added even if its not connected + try + { + OperationSetPersistentPresenceJabberImpl opSet = + (OperationSetPersistentPresenceJabberImpl) + this.getOperationSet(OperationSetPersistentPresence.class); + + Presence unavailablePresence = + new Presence(Presence.Type.unavailable); + + if(opSet != null + && !org.jitsi.util.StringUtils + .isNullOrEmpty(opSet.getCurrentStatusMessage())) + { + unavailablePresence.setStatus( + opSet.getCurrentStatusMessage()); + } + + connection.disconnect(unavailablePresence); + } catch (Exception e) + {} + + connectionListener = null; + connection = null; + // make it null as it also holds a reference to the old connection + // will be created again on new connection + try + { + /* + * The discoveryManager is exposed as service-public by the + * OperationSetContactCapabilities of this + * ProtocolProviderService. No longer expose it because it's + * going away. + */ + if (opsetContactCapabilities != null) + opsetContactCapabilities.setDiscoveryManager(null); + } + finally + { + if(discoveryManager != null) + { + discoveryManager.stop(); + discoveryManager = null; + } + } + } + } + + /** + * Ends the registration of this protocol provider with the service. + */ + public void unregister() + { + unregister(true); + } + + /** + * Unregister and fire the event if requested + * @param fireEvent boolean + */ + public void unregister(boolean fireEvent) + { + synchronized(initializationLock) + { + if(fireEvent) + { + eventDuringLogin = null; + fireRegistrationStateChanged( + getRegistrationState() + , RegistrationState.UNREGISTERING + , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED + , null); + } + + disconnectAndCleanConnection(); + + RegistrationState currRegState = getRegistrationState(); + + if(fireEvent) + { + eventDuringLogin = null; + fireRegistrationStateChanged( + currRegState, + RegistrationState.UNREGISTERED, + RegistrationStateChangeEvent.REASON_USER_REQUEST, null); + } + } + } + + /** + * Returns the short name of the protocol that the implementation of this + * provider is based upon (like SIP, Jabber, ICQ/AIM, or others for + * example). + * + * @return a String containing the short name of the protocol this + * service is taking care of. + */ + public String getProtocolName() + { + return ProtocolNames.JABBER; + } + + /** + * Initialized the service implementation, and puts it in a sate where it + * could interoperate with other services. It is strongly recommended that + * properties in this Map be mapped to property names as specified by + * AccountProperties. + * + * @param screenname the account id/uin/screenname of the account that + * we're about to create + * @param accountID the identifier of the account that this protocol + * provider represents. + * + * @see net.java.sip.communicator.service.protocol.AccountID + */ + protected void initialize(String screenname, + AccountID accountID) + { + synchronized(initializationLock) + { + this.accountID = accountID; + + // in case of modified account, we clear list of supported features + // and every state change listeners, otherwise we can have two + // OperationSet for same feature and it can causes problem (i.e. + // two OperationSetBasicTelephony can launch two ICE negotiations + // (with different ufrag/pwd) and peer will failed call. And + // by the way user will see two dialog for answering/refusing the + // call + supportedFeatures.clear(); + this.clearRegistrationStateChangeListener(); + this.clearSupportedOperationSet(); + + synchronized(providerCreationLock) + { + if(providerManager == null) + { + try + { + ProviderManager.setInstance(new ProviderManagerExt()); + } + catch(Throwable t) + { + // once loaded if we try to set instance second time + // IllegalStateException is thrown + } + finally + { + providerManager = ProviderManager.getInstance(); + } + } + } + + String protocolIconPath + = accountID.getAccountPropertyString( + ProtocolProviderFactory.PROTOCOL_ICON_PATH); + + if (protocolIconPath == null) + protocolIconPath = "resources/images/protocol/jabber"; + + jabberIcon = new ProtocolIconJabberImpl(protocolIconPath); + + jabberStatusEnum + = JabberStatusEnum.getJabberStatusEnum(protocolIconPath); + + //this feature is mandatory to be compliant with Service Discovery + supportedFeatures.add("http://jabber.org/protocol/disco#info"); + + String keepAliveStrValue + = accountID.getAccountPropertyString( + ProtocolProviderFactory.KEEP_ALIVE_METHOD); + + InfoRetreiver infoRetreiver = new InfoRetreiver(this, screenname); + + //initialize the presence OperationSet + OperationSetPersistentPresenceJabberImpl persistentPresence = + new OperationSetPersistentPresenceJabberImpl(this, infoRetreiver); + + addSupportedOperationSet( + OperationSetPersistentPresence.class, + persistentPresence); + // TODO: add the feature, if any, corresponding to persistent + // presence, if someone knows + // supportedFeatures.add(_PRESENCE_); + + //register it once again for those that simply need presence + addSupportedOperationSet( + OperationSetPresence.class, + persistentPresence); + + if(accountID.getAccountPropertyString( + ProtocolProviderFactory.ACCOUNT_READ_ONLY_GROUPS) != null) + { + addSupportedOperationSet( + OperationSetPersistentPresencePermissions.class, + new OperationSetPersistentPresencePermissionsJabberImpl( + this)); + } + + //initialize the IM operation set + OperationSetBasicInstantMessagingJabberImpl basicInstantMessaging = + new OperationSetBasicInstantMessagingJabberImpl(this); + + if (keepAliveStrValue == null + || keepAliveStrValue.equalsIgnoreCase("XEP-0199")) + { + if(keepAliveManager == null) + keepAliveManager = new KeepAliveManager(this); + } + + addSupportedOperationSet( + OperationSetBasicInstantMessaging.class, + basicInstantMessaging); + + // The http://jabber.org/protocol/xhtml-im feature is included + // already in smack. + + addSupportedOperationSet( + OperationSetExtendedAuthorizations.class, + new OperationSetExtendedAuthorizationsJabberImpl( + this, + persistentPresence)); + + //initialize the Whiteboard operation set + addSupportedOperationSet( + OperationSetWhiteboarding.class, + new OperationSetWhiteboardingJabberImpl(this)); + + //initialize the typing notifications operation set + addSupportedOperationSet( + OperationSetTypingNotifications.class, + new OperationSetTypingNotificationsJabberImpl(this)); + + // The http://jabber.org/protocol/chatstates feature implemented in + // OperationSetTypingNotifications is included already in smack. + + //initialize the multi user chat operation set + addSupportedOperationSet( + OperationSetMultiUserChat.class, + new OperationSetMultiUserChatJabberImpl(this)); + + addSupportedOperationSet( + OperationSetServerStoredContactInfo.class, + new OperationSetServerStoredContactInfoJabberImpl( + infoRetreiver)); + + OperationSetServerStoredAccountInfo accountInfo = + new OperationSetServerStoredAccountInfoJabberImpl(this, + infoRetreiver, + screenname); + + addSupportedOperationSet( + OperationSetServerStoredAccountInfo.class, + accountInfo); + + // Initialize avatar operation set + addSupportedOperationSet( + OperationSetAvatar.class, + new OperationSetAvatarJabberImpl(this, accountInfo)); + + // initialize the file transfer operation set + addSupportedOperationSet( + OperationSetFileTransfer.class, + new OperationSetFileTransferJabberImpl(this)); + + addSupportedOperationSet( + OperationSetInstantMessageTransform.class, + new OperationSetInstantMessageTransformImpl()); + + // Include features we're supporting in addition to the four + // included by smack itself: + // http://jabber.org/protocol/si/profile/file-transfer + // http://jabber.org/protocol/si + // http://jabber.org/protocol/bytestreams + // http://jabber.org/protocol/ibb + supportedFeatures.add("urn:xmpp:thumbs:0"); + supportedFeatures.add("urn:xmpp:bob"); + + // initialize the thumbnailed file factory operation set + addSupportedOperationSet( + OperationSetThumbnailedFileFactory.class, + new OperationSetThumbnailedFileFactoryImpl()); + + // TODO: this is the "main" feature to advertise when a client + // support muc. We have to add some features for + // specific functionality we support in muc. + // see http://www.xmpp.org/extensions/xep-0045.html + + // The http://jabber.org/protocol/muc feature is already included in + // smack. + supportedFeatures.add("http://jabber.org/protocol/muc#rooms"); + supportedFeatures.add("http://jabber.org/protocol/muc#traffic"); + + // RTP HDR extension + supportedFeatures.add(URN_XMPP_JINGLE_RTP_HDREXT); + + //register our jingle provider + providerManager.addIQProvider( JingleIQ.ELEMENT_NAME, + JingleIQ.NAMESPACE, + new JingleIQProvider()); + + // register our input event provider + providerManager.addIQProvider(InputEvtIQ.ELEMENT_NAME, + InputEvtIQ.NAMESPACE, + new InputEvtIQProvider()); + + // register our coin provider + providerManager.addIQProvider(CoinIQ.ELEMENT_NAME, + CoinIQ.NAMESPACE, + new CoinIQProvider()); + supportedFeatures.add(URN_XMPP_JINGLE_COIN); + + // register our JingleInfo provider + providerManager.addIQProvider(JingleInfoQueryIQ.ELEMENT_NAME, + JingleInfoQueryIQ.NAMESPACE, + new JingleInfoQueryIQProvider()); + + // Jitsi Videobridge IQProvider and PacketExtensionProvider + providerManager.addIQProvider( + ColibriConferenceIQ.ELEMENT_NAME, + ColibriConferenceIQ.NAMESPACE, + new ColibriIQProvider()); + + providerManager.addExtensionProvider( + ConferenceDescriptionPacketExtension.ELEMENT_NAME, + ConferenceDescriptionPacketExtension.NAMESPACE, + new ConferenceDescriptionPacketExtension.Provider()); + + providerManager.addExtensionProvider( + CarbonPacketExtension.RECEIVED_ELEMENT_NAME, + CarbonPacketExtension.NAMESPACE, + new CarbonPacketExtension.Provider( + CarbonPacketExtension.RECEIVED_ELEMENT_NAME)); + + providerManager.addExtensionProvider( + CarbonPacketExtension.SENT_ELEMENT_NAME, + CarbonPacketExtension.NAMESPACE, + new CarbonPacketExtension.Provider( + CarbonPacketExtension.SENT_ELEMENT_NAME)); + + //initialize the telephony operation set + boolean isCallingDisabled + = JabberActivator.getConfigurationService() + .getBoolean(IS_CALLING_DISABLED, false); + + boolean isCallingDisabledForAccount + = accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT, + false); + + // Check if calling is enabled. + if (!isCallingDisabled && !isCallingDisabledForAccount) + { + OperationSetBasicTelephonyJabberImpl basicTelephony + = new OperationSetBasicTelephonyJabberImpl(this); + + addSupportedOperationSet( + OperationSetAdvancedTelephony.class, + basicTelephony); + addSupportedOperationSet( + OperationSetBasicTelephony.class, + basicTelephony); + addSupportedOperationSet( + OperationSetSecureZrtpTelephony.class, + basicTelephony); + addSupportedOperationSet( + OperationSetSecureSDesTelephony.class, + basicTelephony); + + // initialize video telephony OperationSet + addSupportedOperationSet( + OperationSetVideoTelephony.class, + new OperationSetVideoTelephonyJabberImpl(basicTelephony)); + + addSupportedOperationSet( + OperationSetTelephonyConferencing.class, + new OperationSetTelephonyConferencingJabberImpl(this)); + + addSupportedOperationSet( + OperationSetBasicAutoAnswer.class, + new OperationSetAutoAnswerJabberImpl(this)); + + addSupportedOperationSet( + OperationSetResourceAwareTelephony.class, + new OperationSetResAwareTelephonyJabberImpl(basicTelephony)); + + // Only init video bridge if enabled + boolean isVideobridgeDisabled + = JabberActivator.getConfigurationService() + .getBoolean(OperationSetVideoBridge. + IS_VIDEO_BRIDGE_DISABLED, false); + + if (!isVideobridgeDisabled) + { + // init video bridge + addSupportedOperationSet( + OperationSetVideoBridge.class, + new OperationSetVideoBridgeImpl(this)); + } + + // init DTMF + OperationSetDTMFJabberImpl operationSetDTMFSip + = new OperationSetDTMFJabberImpl(this); + addSupportedOperationSet( + OperationSetDTMF.class, operationSetDTMFSip); + + addJingleFeatures(); + + // Check if desktop streaming is enabled. + boolean isDesktopStreamingDisabled + = JabberActivator.getConfigurationService() + .getBoolean(IS_DESKTOP_STREAMING_DISABLED, false); + + boolean isAccountDesktopStreamingDisabled + = accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.IS_DESKTOP_STREAMING_DISABLED, + false); + + if (!isDesktopStreamingDisabled + && !isAccountDesktopStreamingDisabled) + { + // initialize desktop streaming OperationSet + addSupportedOperationSet( + OperationSetDesktopStreaming.class, + new OperationSetDesktopStreamingJabberImpl( + basicTelephony)); + + if(!accountID.getAccountPropertyBoolean( + ProtocolProviderFactory + .IS_DESKTOP_REMOTE_CONTROL_DISABLED, + false)) + { + // initialize desktop sharing OperationSets + addSupportedOperationSet( + OperationSetDesktopSharingServer.class, + new OperationSetDesktopSharingServerJabberImpl( + basicTelephony)); + + // Adds extension to support remote control as a sharing + // server (sharer). + supportedFeatures.add(InputEvtIQ.NAMESPACE_SERVER); + + addSupportedOperationSet( + OperationSetDesktopSharingClient.class, + new OperationSetDesktopSharingClientJabberImpl(this) + ); + // Adds extension to support remote control as a sharing + // client (sharer). + supportedFeatures.add(InputEvtIQ.NAMESPACE_CLIENT); + } + } + } + + // OperationSetContactCapabilities + opsetContactCapabilities + = new OperationSetContactCapabilitiesJabberImpl(this); + if (discoveryManager != null) + opsetContactCapabilities.setDiscoveryManager(discoveryManager); + addSupportedOperationSet( + OperationSetContactCapabilities.class, + opsetContactCapabilities); + + addSupportedOperationSet( + OperationSetGenericNotifications.class, + new OperationSetGenericNotificationsJabberImpl(this)); + + supportedFeatures.add("jabber:iq:version"); + if(versionManager == null) + versionManager = new VersionManager(this); + + supportedFeatures.add(MessageCorrectionExtension.NAMESPACE); + addSupportedOperationSet(OperationSetMessageCorrection.class, + basicInstantMessaging); + + OperationSetChangePassword opsetChangePassword + = new OperationSetChangePasswordJabberImpl(this); + addSupportedOperationSet(OperationSetChangePassword.class, + opsetChangePassword); + + OperationSetCusaxUtils opsetCusaxCusaxUtils + = new OperationSetCusaxUtilsJabberImpl(this); + addSupportedOperationSet(OperationSetCusaxUtils.class, + opsetCusaxCusaxUtils); + + boolean isUserSearchEnabled = accountID.getAccountPropertyBoolean( + IS_USER_SEARCH_ENABLED_PROPERTY, false); + if(isUserSearchEnabled) + { + addSupportedOperationSet(OperationSetUserSearch.class, + new OperationSetUserSearchJabberImpl(this)); + } + + OperationSetTLS opsetTLS + = new OperationSetTLSJabberImpl(this); + addSupportedOperationSet(OperationSetTLS.class, + opsetTLS); + + isInitialized = true; + } + } + + /** + * Adds Jingle related features to the supported features. + */ + private void addJingleFeatures() + { + // Add Jingle features to supported features. + supportedFeatures.add(URN_XMPP_JINGLE); + supportedFeatures.add(URN_XMPP_JINGLE_RTP); + supportedFeatures.add(URN_XMPP_JINGLE_RAW_UDP_0); + + /* + * Reflect the preference of the user with respect to the use of + * ICE. + */ + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.IS_USE_ICE, + true)) + { + supportedFeatures.add(URN_XMPP_JINGLE_ICE_UDP_1); + } + + supportedFeatures.add(URN_XMPP_JINGLE_RTP_AUDIO); + supportedFeatures.add(URN_XMPP_JINGLE_RTP_VIDEO); + supportedFeatures.add(URN_XMPP_JINGLE_RTP_ZRTP); + + /* + * Reflect the preference of the user with respect to the use of + * Jingle Nodes. + */ + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactoryJabberImpl.IS_USE_JINGLE_NODES, + true)) + { + supportedFeatures.add(URN_XMPP_JINGLE_NODES); + } + + // XEP-0251: Jingle Session Transfer + supportedFeatures.add(URN_XMPP_JINGLE_TRANSFER_0); + + // XEP-0320: Use of DTLS-SRTP in Jingle Sessions + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + SrtpControlType.DTLS_SRTP)) + { + supportedFeatures.add(URN_XMPP_JINGLE_DTLS_SRTP); + } + } + + /** + * Makes the service implementation close all open sockets and release + * any resources that it might have taken and prepare for + * shutdown/garbage collection. + */ + public void shutdown() + { + synchronized(initializationLock) + { + if (logger.isTraceEnabled()) + logger.trace("Killing the Jabber Protocol Provider."); + + //kill all active calls + OperationSetBasicTelephonyJabberImpl telephony + = (OperationSetBasicTelephonyJabberImpl)getOperationSet( + OperationSetBasicTelephony.class); + if (telephony != null) + { + telephony.shutdown(); + } + + disconnectAndCleanConnection(); + + isInitialized = false; + } + } + + /** + * Returns true if the provider service implementation is initialized and + * ready for use by other services, and false otherwise. + * + * @return true if the provider is initialized and ready for use and false + * otherwise + */ + public boolean isInitialized() + { + return isInitialized; + } + + /** + * Returns the AccountID that uniquely identifies the account represented + * by this instance of the ProtocolProviderService. + * @return the id of the account represented by this provider. + */ + public AccountID getAccountID() + { + return accountID; + } + + /** + * Returns the XMPPConnectionopened by this provider + * @return a reference to the XMPPConnection last opened by this + * provider. + */ + public XMPPConnection getConnection() + { + return connection; + } + + /** + * Determines whether a specific XMPPException signals that + * attempted authentication has failed. + * + * @param ex the XMPPException which is to be determined whether it + * signals that attempted authentication has failed + * @return true if the specified ex signals that attempted + * authentication has failed; otherwise, false + */ + private boolean isAuthenticationFailed(XMPPException ex) + { + String exMsg = ex.getMessage().toLowerCase(); + + // as there are no types or reasons for XMPPException + // we try determine the reason according to their message + // all messages that were found in smack 3.1.0 were took in count + return + ((exMsg.indexOf("sasl authentication") != -1) + && (exMsg.indexOf("failed") != -1)) + || (exMsg.indexOf( + "does not support compatible authentication mechanism") + != -1) + || (exMsg.indexOf("unable to determine password") != -1); + } + + /** + * Tries to determine the appropriate message and status to fire, + * according the exception. + * + * @param ex the {@link XMPPException} that caused the state change. + */ + private void fireRegistrationStateChanged(XMPPException ex) + { + int reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED; + RegistrationState regState = RegistrationState.UNREGISTERED; + String reasonStr = null; + + Throwable wrappedEx = ex.getWrappedThrowable(); + if(wrappedEx != null + && (wrappedEx instanceof UnknownHostException + || wrappedEx instanceof ConnectException + || wrappedEx instanceof SocketException)) + { + reason = RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND; + regState = RegistrationState.CONNECTION_FAILED; + } + else + { + String exMsg = ex.getMessage().toLowerCase(); + + // as there are no types or reasons for XMPPException + // we try determine the reason according to their message + // all messages that were found in smack 3.1.0 were took in count + if(isAuthenticationFailed(ex)) + { + JabberActivator.getProtocolProviderFactory(). + storePassword(getAccountID(), null); + + reason = RegistrationStateChangeEvent + .REASON_AUTHENTICATION_FAILED; + + regState = RegistrationState.AUTHENTICATION_FAILED; + + fireRegistrationStateChanged( + getRegistrationState(), regState, reason, null); + + // Try to reregister and to ask user for a new password. + reregister(SecurityAuthority.WRONG_PASSWORD); + + return; + } + else if(exMsg.indexOf("no response from the server") != -1 + || exMsg.indexOf("connection failed") != -1) + { + reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED; + regState = RegistrationState.CONNECTION_FAILED; + } + else if(exMsg.indexOf("tls is required") != -1) + { + regState = RegistrationState.AUTHENTICATION_FAILED; + reason = RegistrationStateChangeEvent.REASON_TLS_REQUIRED; + } + } + + if(regState == RegistrationState.UNREGISTERED + || regState == RegistrationState.CONNECTION_FAILED) + { + // we fired that for some reason we are going offline + // lets clean the connection state for any future connections + disconnectAndCleanConnection(); + } + + fireRegistrationStateChanged( + getRegistrationState(), regState, reason, reasonStr); + } + + /** + * Enable to listen for jabber connection events + */ + private class JabberConnectionListener + implements ConnectionListener + { + /** + * Implements connectionClosed from ConnectionListener + */ + public void connectionClosed() + { + // if we are in the middle of connecting process + // do not fire events, will do it later when the method + // connectAndLogin finishes its work + synchronized(connectAndLoginLock) + { + if(inConnectAndLogin) + { + eventDuringLogin = new RegistrationStateChangeEvent( + ProtocolProviderServiceJabberImpl.this, + getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, + null); + return; + } + } + // fire that a connection failed, the reconnection mechanism + // will look after us and will clean us, other wise we can do + // a dead lock (connection closed is called + // within xmppConneciton and calling disconnect again can lock it) + fireRegistrationStateChanged( + getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, + null); + } + + /** + * Implements connectionClosedOnError from + * ConnectionListener. + * + * @param exception contains information on the error. + */ + public void connectionClosedOnError(Exception exception) + { + logger.error("connectionClosedOnError " + + exception.getLocalizedMessage()); + + if(exception instanceof XMPPException) + { + StreamError err = ((XMPPException)exception).getStreamError(); + + if(err != null && err.getCode().equals( + XMPPError.Condition.conflict.toString())) + { + // if we are in the middle of connecting process + // do not fire events, will do it later when the method + // connectAndLogin finishes its work + synchronized(connectAndLoginLock) + { + if(inConnectAndLogin) + { + eventDuringLogin = new RegistrationStateChangeEvent( + ProtocolProviderServiceJabberImpl.this, + getRegistrationState(), + RegistrationState.UNREGISTERED, + RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS, + "Connecting multiple times with the same resource"); + return; + } + } + + disconnectAndCleanConnection(); + + fireRegistrationStateChanged(getRegistrationState(), + RegistrationState.UNREGISTERED, + RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS, + "Connecting multiple times with the same resource"); + + return; + } + } // Ignore certificate exceptions as we handle them elsewhere + else if(exception instanceof SSLHandshakeException && + exception.getCause() instanceof CertificateException) + { + return; + } + + // if we are in the middle of connecting process + // do not fire events, will do it later when the method + // connectAndLogin finishes its work + synchronized(connectAndLoginLock) + { + if(inConnectAndLogin) + { + eventDuringLogin = new RegistrationStateChangeEvent( + ProtocolProviderServiceJabberImpl.this, + getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, + exception.getMessage()); + return; + } + } + + disconnectAndCleanConnection(); + + fireRegistrationStateChanged(getRegistrationState(), + RegistrationState.CONNECTION_FAILED, + RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, + exception.getMessage()); + } + + /** + * Implements reconnectingIn from ConnectionListener + * + * @param i delay in seconds for reconnection. + */ + public void reconnectingIn(int i) + { + if (logger.isInfoEnabled()) + logger.info("reconnectingIn " + i); + } + + /** + * Implements reconnectingIn from ConnectionListener + */ + public void reconnectionSuccessful() + { + if (logger.isInfoEnabled()) + logger.info("reconnectionSuccessful"); + } + + /** + * Implements reconnectionFailed from + * ConnectionListener. + * + * @param exception description of the failure + */ + public void reconnectionFailed(Exception exception) + { + if (logger.isInfoEnabled()) + logger.info("reconnectionFailed ", exception); + } + } + + /** + * Returns the jabber protocol icon. + * @return the jabber protocol icon + */ + public ProtocolIcon getProtocolIcon() + { + return jabberIcon; + } + + /** + * Returns the current instance of JabberStatusEnum. + * + * @return the current instance of JabberStatusEnum. + */ + JabberStatusEnum getJabberStatusEnum() + { + return jabberStatusEnum; + } + + /** + * Determines if the given list of ext features is supported by the + * specified jabber id. + * + * @param jid the jabber id for which to check + * @param extFeatures the list of ext features to check for + * + * @return true if the list of ext features is supported; + * otherwise, false + */ + public boolean isExtFeatureListSupported(String jid, String... extFeatures) + { + EntityCapsManager capsManager = discoveryManager.getCapsManager(); + EntityCapsManager.Caps caps = capsManager.getCapsByUser(jid); + + String bypassDomain = accountID.getAccountPropertyString( + "TELEPHONY_BYPASS_GTALK_CAPS"); + String domain = StringUtils.parseServer(jid); + boolean domainEquals = domain.equals(bypassDomain); + + if(caps != null && caps.ext != null) + { + String exts[] = caps.ext.split(" "); + boolean found = false; + + for(String extFeature : extFeatures) + { + // in case we have a domain that have to bypass GTalk caps + if(extFeature.equals(CAPS_GTALK_WEB_VOICE) && domainEquals) + { + return true; + } + + found = false; + for(String ext : exts) + { + if(ext.equals(extFeature)) + { + found = true; + break; + } + } + + if(!found) + { + break; + } + } + + return found; + } + + return false; + } + + /** + * Determines if the given list of features is supported by the + * specified jabber id. + * + * @param jid the jabber id for which to check + * @param features the list of features to check for + * + * @return true if the list of features is supported; otherwise, + * false + */ + public boolean isFeatureListSupported(String jid, String... features) + { + boolean isFeatureListSupported = true; + + try + { + if(discoveryManager == null) + return isFeatureListSupported; + + DiscoverInfo featureInfo = + discoveryManager.discoverInfoNonBlocking(jid); + + if(featureInfo == null) + return isFeatureListSupported; + + for (String feature : features) + { + if (!featureInfo.containsFeature(feature)) + { + // If one is not supported we return false and don't check + // the others. + isFeatureListSupported = false; + break; + } + } + } + catch (XMPPException e) + { + if (logger.isDebugEnabled()) + logger.debug("Failed to retrive discovery info.", e); + } + return isFeatureListSupported; + } + + /** + * Determines if the given list of features is supported by the + * specified jabber id. + * + * @param jid the jabber id that we'd like to get information about + * @param feature the feature to check for + * + * @return true if the list of features is supported, otherwise + * returns false + */ + public boolean isFeatureSupported(String jid, String feature) + { + return isFeatureListSupported(jid, feature); + } + + /** + * Returns the full jabber id (jid) corresponding to the given contact. If + * the provider is not connected returns null. + * + * @param contact the contact, for which we're looking for a jid + * @return the jid of the specified contact or null if the provider is not + * yet connected; + */ + public String getFullJid(Contact contact) + { + return getFullJid(contact.getAddress()); + } + + /** + * Returns the full jabber id (jid) corresponding to the given bare jid. If + * the provider is not connected returns null. + * + * @param bareJid the bare contact address (i.e. no resource) whose full + * jid we are looking for. + * @return the jid of the specified contact or null if the provider is not + * yet connected; + */ + public String getFullJid(String bareJid) + { + XMPPConnection connection = getConnection(); + + // when we are not connected there is no full jid + if (connection != null && connection.isConnected()) + { + Roster roster = connection.getRoster(); + + if (roster != null) + return roster.getPresence(bareJid).getFrom(); + } + return null; + } + + /** + * The trust manager which asks the client whether to trust particular + * certificate which is not globally trusted. + */ + private class HostTrustManager + implements X509TrustManager + { + /** + * The default trust manager. + */ + private final X509TrustManager tm; + + /** + * Creates the custom trust manager. + * @param tm the default trust manager. + */ + HostTrustManager(X509TrustManager tm) + { + this.tm = tm; + } + + /** + * Not used. + * + * @return nothing. + */ + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[0]; + } + + /** + * Not used. + * @param chain the cert chain. + * @param authType authentication type like: RSA. + * @throws CertificateException never + * @throws UnsupportedOperationException always + */ + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException, UnsupportedOperationException + { + throw new UnsupportedOperationException(); + } + + /** + * Check whether a certificate is trusted, if not as user whether he + * trust it. + * @param chain the certificate chain. + * @param authType authentication type like: RSA. + * @throws CertificateException not trusted. + */ + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException + { + abortConnecting = true; + try + { + tm.checkServerTrusted(chain, authType); + } + catch(CertificateException e) + { + // notify in a separate thread to avoid a deadlock when a + // reg state listener accesses a synchronized XMPPConnection + // method (like getRoster) + new Thread(new Runnable() + { + public void run() + { + fireRegistrationStateChanged(getRegistrationState(), + RegistrationState.UNREGISTERED, + RegistrationStateChangeEvent.REASON_USER_REQUEST, + "Not trusted certificate"); + } + }).start(); + throw e; + } + + if(abortConnecting) + { + // connect hasn't finished we will continue normally + abortConnecting = false; + return; + } + else + { + // in this situation connect method has finished + // and it was disconnected so we wont to connect. + + // register.connect in new thread so we can release the + // current connecting thread, otherwise this blocks + // jabber + new Thread(new Runnable() + { + public void run() + { + reregister(SecurityAuthority.CONNECTION_FAILED); + } + }).start(); + return; + } + } + } + + /** + * Returns the currently valid {@link ScServiceDiscoveryManager}. + * + * @return the currently valid {@link ScServiceDiscoveryManager}. + */ + public ScServiceDiscoveryManager getDiscoveryManager() + { + return discoveryManager; + } + + /** + * Returns our own Jabber ID. + * + * @return our own Jabber ID. + */ + public String getOurJID() + { + String jid = null; + + if (connection != null) + jid = connection.getUser(); + + if (jid == null) + { + // seems like the connection is not yet initialized so lets try to + // construct our jid ourselves. + String accountIDUserID = getAccountID().getUserID(); + String userID = StringUtils.parseName(accountIDUserID); + String serviceName = StringUtils.parseServer(accountIDUserID); + + jid = userID + "@" + serviceName; + } + + return jid; + } + + /** + * Returns the InetAddress that is most likely to be to be used + * as a next hop when contacting our XMPP server. This is an utility method + * that is used whenever we have to choose one of our local addresses (e.g. + * when trying to pick a best candidate for raw udp). It is based on the + * assumption that, in absence of any more specific details, chances are + * that we will be accessing remote destinations via the same interface + * that we are using to access our jabber server. + * + * @return the InetAddress that is most likely to be to be used + * as a next hop when contacting our server. + * + * @throws IllegalArgumentException if we don't have a valid server. + */ + public InetAddress getNextHop() + throws IllegalArgumentException + { + InetAddress nextHop = null; + String nextHopStr = null; + + if ( proxy != null + && proxy.getProxyType() + != org.jivesoftware.smack.proxy.ProxyInfo.ProxyType.NONE) + { + nextHopStr = proxy.getProxyAddress(); + } + else + { + nextHopStr = getConnection().getHost(); + } + + try + { + nextHop = NetworkUtils.getInetAddress(nextHopStr); + } + catch (UnknownHostException ex) + { + throw new IllegalArgumentException( + "seems we don't have a valid next hop.", ex); + } + + if(logger.isDebugEnabled()) + logger.debug("Returning address " + nextHop + " as next hop."); + + return nextHop; + } + + /** + * Start auto-discovery of JingleNodes tracker/relays. + */ + public void startJingleNodesDiscovery() + { + // Jingle Nodes Service Initialization + final JabberAccountIDImpl accID = (JabberAccountIDImpl)getAccountID(); + final SmackServiceNode service = new SmackServiceNode(connection, + 60000); + // make sure SmackServiceNode will clean up when connection is closed + connection.addConnectionListener(service); + + for(JingleNodeDescriptor desc : accID.getJingleNodes()) + { + TrackerEntry entry = new TrackerEntry( + desc.isRelaySupported() ? TrackerEntry.Type.relay : + TrackerEntry.Type.tracker, + TrackerEntry.Policy._public, + desc.getJID(), + JingleChannelIQ.UDP); + + service.addTrackerEntry(entry); + } + + new Thread(new JingleNodesServiceDiscovery( + service, + connection, + accID, + jingleNodesSyncRoot)) + .start(); + + jingleNodesServiceNode = service; + } + + /** + * Get the Jingle Nodes service. Note that this method will block until + * Jingle Nodes auto discovery (if enabled) finished. + * + * @return Jingle Nodes service + */ + public SmackServiceNode getJingleNodesServiceNode() + { + synchronized(jingleNodesSyncRoot) + { + return jingleNodesServiceNode; + } + } + + /** + * Logs a specific message and associated Throwable cause as an + * error using the current Logger and then throws a new + * OperationFailedException with the message, a specific error code + * and the cause. + * + * @param message the message to be logged and then wrapped in a new + * OperationFailedException + * @param errorCode the error code to be assigned to the new + * OperationFailedException + * @param cause the Throwable that has caused the necessity to log + * an error and have a new OperationFailedException thrown + * @param logger the logger that we'd like to log the error message + * and cause. + * + * @throws OperationFailedException the exception that we wanted this method + * to throw. + */ + public static void throwOperationFailedException( String message, + int errorCode, + Throwable cause, + Logger logger) + throws OperationFailedException + { + logger.error(message, cause); + + if(cause == null) + throw new OperationFailedException(message, errorCode); + else + throw new OperationFailedException(message, errorCode, cause); + } + + /** + * Used when we need to re-register or someone needs to obtain credentials. + * @return the SecurityAuthority. + */ + public SecurityAuthority getAuthority() + { + return authority; + } + + /** + * Returns true if gtalktesting is enabled, false otherwise. + * + * @return true if gtalktesting is enabled, false otherwise. + */ + public boolean isGTalkTesting() + { + return + Boolean.getBoolean("gtalktesting") + || JabberActivator.getConfigurationService().getBoolean( + "net.java.sip.communicator.impl.protocol.jabber" + + ".gtalktesting", + false) + || accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.IS_USE_GOOGLE_ICE, + true); + } + + UserCredentials getUserCredentials() + { + return userCredentials; + } + + /** + * Returns true if our account is a Gmail or a Google Apps ones. + * + * @return true if our account is a Gmail or a Google Apps ones. + */ + public boolean isGmailOrGoogleAppsAccount() + { + String domain = StringUtils.parseServer( + getAccountID().getUserID()); + return isGmailOrGoogleAppsAccount(domain); + } + + /** + * Returns true if our account is a Gmail or a Google Apps ones. + * + * @param domain domain to check + * @return true if our account is a Gmail or a Google Apps ones. + */ + public static boolean isGmailOrGoogleAppsAccount(String domain) + { + SRVRecord srvRecords[] = null; + + try + { + srvRecords = NetworkUtils.getSRVRecords("xmpp-client", "tcp", + domain); + } + catch (ParseException e) + { + logger.info("Failed to get SRV records for XMPP domain"); + return false; + } + catch (DnssecException e) + { + logger.error("DNSSEC failure while checking for google domains", e); + return false; + } + + if(srvRecords == null) + { + return false; + } + + for(SRVRecord srv : srvRecords) + { + if(srv.getTarget().endsWith("google.com") || + srv.getTarget().endsWith("google.com.")) + { + return true; + } + } + + return false; + } + + /** + * Sets the traffic class for the XMPP signalling socket. + */ + private void setTrafficClass() + { + Socket s = connection.getSocket(); + + if(s != null) + { + ConfigurationService configService = + JabberActivator.getConfigurationService(); + String dscp = configService.getString(XMPP_DSCP_PROPERTY); + + if(dscp != null) + { + try + { + int dscpInt = Integer.parseInt(dscp) << 2; + + if(dscpInt > 0) + s.setTrafficClass(dscpInt); + } + catch (Exception e) + { + logger.info("Failed to set trafficClass", e); + } + } + } + } + + /** + * Gets the entity ID of the first Jitsi Videobridge associated with + * {@link #connection} i.e. provided by the serviceName of + * connection. + * + * @return the entity ID of the first Jitsi Videobridge associated with + * connection + */ + public String getJitsiVideobridge() + { + XMPPConnection connection = getConnection(); + + if (connection != null) + { + ScServiceDiscoveryManager discoveryManager = getDiscoveryManager(); + String serviceName = connection.getServiceName(); + DiscoverItems discoverItems = null; + + try + { + discoverItems = discoveryManager.discoverItems(serviceName); + } + catch (XMPPException xmppe) + { + if (logger.isDebugEnabled()) + { + logger.debug( + "Failed to discover the items associated with" + + " Jabber entity: " + serviceName, + xmppe); + } + } + if (discoverItems != null) + { + Iterator discoverItemIter + = discoverItems.getItems(); + + while (discoverItemIter.hasNext()) + { + DiscoverItems.Item discoverItem = discoverItemIter.next(); + String entityID = discoverItem.getEntityID(); + DiscoverInfo discoverInfo = null; + + try + { + discoverInfo = discoveryManager.discoverInfo(entityID); + } + catch (XMPPException xmppe) + { + logger.warn( + "Failed to discover information about Jabber" + + " entity: " + entityID, + xmppe); + } + if ((discoverInfo != null) + && discoverInfo.containsFeature( + ColibriConferenceIQ.NAMESPACE)) + { + return entityID; + } + } + } + } + + return null; + } + + /** + * Load jabber service class, their static context will register + * what is needed. Used in android as when using the other jars + * these services are loaded from the jar manifest. + */ + private static void loadJabberServiceClasses() + { + if(!OSUtils.IS_ANDROID) + return; + + try + { + // pre-configure smack in android + // just to load class to init their static blocks + SmackConfiguration.getVersion(); + Class.forName(ServiceDiscoveryManager.class.getName()); + + Class.forName(DelayInformation.class.getName()); + Class.forName(org.jivesoftware.smackx + .provider.DelayInformationProvider.class.getName()); + Class.forName(org.jivesoftware.smackx + .bytestreams.socks5.Socks5BytestreamManager.class.getName()); + Class.forName(XHTMLManager.class.getName()); + Class.forName(org.jivesoftware.smackx + .bytestreams.ibb.InBandBytestreamManager.class.getName()); + + } + catch(ClassNotFoundException e) + { + logger.error("Error loading classes in smack", e); + } + } + + /** + * Return the SSL socket (if TLS used). + * @return The SSL socket or null if not used + */ + public SSLSocket getSSLSocket() + { + final SSLSocket result; + final Socket socket = connection.getSocket(); + if (socket instanceof SSLSocket) + { + result = (SSLSocket) socket; + } + else + { + result = null; + } + return result; + } + +} diff --git a/src/net/java/sip/communicator/impl/protocol/sip/CallPeerMediaHandlerSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/CallPeerMediaHandlerSipImpl.java index 32e726a71..2ca7d83e6 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/CallPeerMediaHandlerSipImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/CallPeerMediaHandlerSipImpl.java @@ -789,7 +789,7 @@ private boolean updateMediaDescriptionForDtls( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME)) + SrtpControlType.DTLS_SRTP)) { /* * The transport protocol of the media described by localMd should @@ -1019,7 +1019,7 @@ private boolean updateMediaDescriptionForSDes( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) || !accountID.isEncryptionProtocolEnabled( - SDesControl.PROTO_NAME)) + SrtpControlType.SDES)) { return false; } @@ -1102,7 +1102,7 @@ private boolean updateMediaDescriptionForZrtp( if(accountID.getAccountPropertyBoolean( ProtocolProviderFactory.DEFAULT_ENCRYPTION, true) - && accountID.isEncryptionProtocolEnabled(ZrtpControl.PROTO_NAME) + && accountID.isEncryptionProtocolEnabled(SrtpControlType.ZRTP) && peer.getCall().isSipZrtpAttribute()) { ZrtpControl zrtpControl diff --git a/src/net/java/sip/communicator/service/protocol/AccountID.java b/src/net/java/sip/communicator/service/protocol/AccountID.java index 959bd0a0c..412ce3f1f 100644 --- a/src/net/java/sip/communicator/service/protocol/AccountID.java +++ b/src/net/java/sip/communicator/service/protocol/AccountID.java @@ -795,16 +795,16 @@ public void setAccountProperties(Map accountProperties) * @param encryptionProtocolName The name of the encryption protocol * ("ZRTP", "SDES" or "MIKEY"). */ - public boolean isEncryptionProtocolEnabled(String encryptionProtocolName) + public boolean isEncryptionProtocolEnabled(SrtpControlType type) { // The default value is false, except for ZRTP. - boolean defaultValue = "ZRTP".equals(encryptionProtocolName); + boolean defaultValue = type == SrtpControlType.ZRTP; return getAccountPropertyBoolean( ProtocolProviderFactory.ENCRYPTION_PROTOCOL_STATUS + "." - + encryptionProtocolName, + + type.toString(), defaultValue); } diff --git a/src/net/java/sip/communicator/service/protocol/SecurityAccountRegistration.java b/src/net/java/sip/communicator/service/protocol/SecurityAccountRegistration.java index e4a62ac67..4a1de01a7 100644 --- a/src/net/java/sip/communicator/service/protocol/SecurityAccountRegistration.java +++ b/src/net/java/sip/communicator/service/protocol/SecurityAccountRegistration.java @@ -29,9 +29,9 @@ public abstract class SecurityAccountRegistration public static final List ENCRYPTION_PROTOCOLS = Collections.unmodifiableList( Arrays.asList( - ZrtpControl.PROTO_NAME, - SDesControl.PROTO_NAME, - DtlsControl.PROTO_NAME)); + SrtpControlType.ZRTP.toString(), + SrtpControlType.SDES.toString(), + SrtpControlType.DTLS_SRTP.toString())); /** * Enables support to encrypt calls.