Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorpperret2006-10-17 19:12:42 +0000
committerpperret2006-10-17 19:12:42 +0000
commit8deb3e7166aa8524fa2b53da623a558f7683d2bb (patch)
treea20b7b41c61e2ea63ce4cc83292665888a13204c /protocols/bundles/org.jivesoftware.smack
parent69287910ce05d48ac3cb72cb648e55ff868381d6 (diff)
downloadorg.eclipse.ecf-8deb3e7166aa8524fa2b53da623a558f7683d2bb.tar.gz
org.eclipse.ecf-8deb3e7166aa8524fa2b53da623a558f7683d2bb.tar.xz
org.eclipse.ecf-8deb3e7166aa8524fa2b53da623a558f7683d2bb.zip
xmpp jingle provider
Diffstat (limited to 'protocols/bundles/org.jivesoftware.smack')
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java58
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java365
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java164
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java363
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java662
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java341
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java1020
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java91
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java43
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java553
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java405
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java289
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java171
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java797
14 files changed, 5322 insertions, 0 deletions
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java
new file mode 100644
index 000000000..d40c772f3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/ContentInfo.java
@@ -0,0 +1,58 @@
+package org.jivesoftware.smackx.jingle;
+
+/**
+ * Content info. Content info messages are complementary messages that can be
+ * transmitted for informing of events like "busy", "ringtone", etc.
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+public abstract class ContentInfo {
+
+ /**
+ * Audio conten info messages.
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+ public static class Audio extends ContentInfo {
+
+ public static final ContentInfo.Audio BUSY = new ContentInfo.Audio("busy");
+
+ public static final ContentInfo.Audio HOLD = new ContentInfo.Audio("hold");
+
+ public static final ContentInfo.Audio MUTE = new ContentInfo.Audio("mute");
+
+ public static final ContentInfo.Audio QUEUED = new ContentInfo.Audio("queued");
+
+ public static final ContentInfo.Audio RINGING = new ContentInfo.Audio("ringing");
+
+ private String value;
+
+ public Audio(final String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ /**
+ * Returns the MediaInfo constant associated with the String value.
+ */
+ public static ContentInfo fromString(String value) {
+ value = value.toLowerCase();
+ if (value.equals("busy")) {
+ return BUSY;
+ } else if (value.equals("hold")) {
+ return HOLD;
+ } else if (value.equals("mute")) {
+ return MUTE;
+ } else if (value.equals("queued")) {
+ return QUEUED;
+ } else if (value.equals("ringing")) {
+ return RINGING;
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java
new file mode 100644
index 000000000..30a400c26
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/IncomingJingleSession.java
@@ -0,0 +1,365 @@
+/**
+ * $RCSfile: IncomingJingleSession.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2006/10/17 19:12:42 $
+ *
+ * Copyright (C) 2002-2006 Jive Software. All rights reserved.
+ * ====================================================================
+ * The Jive Software License (based on Apache Software License, Version 1.1)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution,
+ * if any, must include the following acknowledgment:
+ * "This product includes software developed by
+ * Jive Software (http://www.jivesoftware.com)."
+ * Alternately, this acknowledgment may appear in the software itself,
+ * if and wherever such third-party acknowledgments normally appear.
+ *
+ * 4. The names "Smack" and "Jive Software" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For written permission, please
+ * contact webmaster@jivesoftware.com.
+ *
+ * 5. Products derived from this software may not be called "Smack",
+ * nor may "Smack" appear in their name, without prior written
+ * permission of Jive Software.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ */
+
+package org.jivesoftware.smackx.jingle;
+
+import java.util.List;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.nat.TransportCandidate;
+import org.jivesoftware.smackx.nat.TransportResolver;
+import org.jivesoftware.smackx.packet.Jingle;
+import org.jivesoftware.smackx.packet.JingleContentDescription;
+import org.jivesoftware.smackx.packet.JingleError;
+import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType;
+
+/**
+ * An incoming Jingle session.
+ *
+ * </p>
+ *
+ * This class is not directly used by users. Instead, users should refer to the
+ * JingleManager class, that will create the appropiate instance...
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin
+ */
+public class IncomingJingleSession extends JingleSession {
+
+ // states
+ private final Accepting accepting;
+
+ private final Pending pending;
+
+ private final Active active;
+
+ /**
+ * Constructor with the request
+ *
+ * @param conn the XMPP connection
+ * @param responder the responder
+ * @param resolver The transport resolver
+ */
+ public IncomingJingleSession(final XMPPConnection conn, final String responder,
+ final List payloadTypes, final TransportResolver resolver) {
+
+ super(conn, responder, conn.getUser());
+
+ // Create the states...
+
+ accepting = new Accepting(this);
+ pending = new Pending(this);
+ active = new Active(this);
+
+ setMediaNeg(new MediaNegotiator(this, payloadTypes));
+ setTransportNeg(new TransportNegotiator.RawUdp(this, resolver));
+ }
+
+ /**
+ * Start the session.
+ *
+ * @throws XMPPException
+ */
+ public void start(final JingleSessionRequest request) throws XMPPException {
+ if (invalidState()) {
+ Jingle jin = request.getJingle();
+ if (jin != null) {
+
+ // Initialize the session information
+ setSid(jin.getSid());
+
+ // Establish the default state
+ setState(accepting);
+
+ updatePacketListener();
+ respond(jin);
+ } else {
+ throw new IllegalStateException(
+ "Session request with null Jingle packet.");
+ }
+ } else {
+ throw new IllegalStateException("Starting session without null state.");
+ }
+ }
+
+ // States
+
+ /**
+ * First stage when we have received a session request, and we accept the
+ * request. We start in this stage, as the instance is created when the user
+ * accepts the connection...
+ */
+ public class Accepting extends JingleNegotiator.State {
+
+ public Accepting(final JingleNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * Initiate the incoming session. We have already sent the ACK partially
+ * accepting the session...
+ *
+ * @throws XMPPException
+ */
+ public Jingle eventInitiate(final Jingle inJingle) throws XMPPException {
+ // Set the new session state
+ setState(pending);
+ return super.eventInitiate(inJingle);
+ }
+
+ /**
+ * An error has occurred.
+ *
+ * @throws XMPPException
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerSessionClosedOnError(new JingleException(iq.getError().getMessage()));
+ super.eventError(iq);
+ }
+ }
+
+ /**
+ * "Pending" state: we are waiting for the transport and content
+ * negotiators.
+ */
+ private class Pending extends JingleNegotiator.State {
+
+ JingleListener.Media mediaListener;
+
+ JingleListener.Transport transportListener;
+
+ public Pending(final JingleNegotiator neg) {
+ super(neg);
+
+ // Create the listeners that will send a "session-accept" when the
+ // sub-negotiators are done.
+ mediaListener = new JingleListener.Media() {
+ public void mediaClosed(final PayloadType cand) {
+ }
+
+ public void mediaEstablished(final PayloadType pt) {
+ checkFullyEstablished();
+ }
+ };
+
+ transportListener = new JingleListener.Transport() {
+ public void transportEstablished(final TransportCandidate local,
+ final TransportCandidate remote) {
+ checkFullyEstablished();
+ }
+
+ public void transportClosed(final TransportCandidate cand) {
+ }
+
+ public void transportClosedOnError(final XMPPException e) {
+ }
+ };
+ }
+
+ /**
+ * Enter in the pending state: wait for the sub-negotiators.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventEnter() {
+ // Add the listeners to the sub-negotiators...
+ addMediaListener(mediaListener);
+ addTransportListener(transportListener);
+ super.eventEnter();
+ }
+
+ /**
+ * Exit of the state
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit()
+ */
+ public void eventExit() {
+ removeMediaListener(mediaListener);
+ removeTransportListener(transportListener);
+ super.eventExit();
+ }
+
+ /**
+ * Check if the session has been fully accepted by all the
+ * sub-negotiators and, in that case, send an "accept" message...
+ */
+ private void checkFullyEstablished() {
+ if (isFullyEstablished()) {
+
+ PayloadType.Audio bestCommonAudioPt = getMediaNeg()
+ .getBestCommonAudioPt();
+ TransportCandidate bestRemoteCandidate = getTransportNeg()
+ .getBestRemoteCandidate();
+ TransportCandidate acceptedLocalCandidate = getTransportNeg()
+ .getAcceptedLocalCandidate();
+
+ if (bestCommonAudioPt != null && bestRemoteCandidate != null
+ && acceptedLocalCandidate != null) {
+ // Ok, send a packet saying that we accept this session
+ Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT);
+
+ // ... with the audio payload type and the transport
+ // candidate
+ jout.addDescription(new JingleContentDescription.Audio(
+ new JinglePayloadType(bestCommonAudioPt)));
+ jout.addTransport(getTransportNeg().getJingleTransport(
+ bestRemoteCandidate));
+
+ addExpectedId(jout.getPacketID());
+ sendFormattedJingle(jout);
+ }
+ }
+ }
+
+ /**
+ * The other endpoint has accepted the session.
+ */
+ public Jingle eventAccept(final Jingle jin) throws XMPPException {
+
+ PayloadType acceptedPayloadType = null;
+ TransportCandidate acceptedLocalCandidate = null;
+
+ // We process the "accepted" if we have finished the
+ // sub-negotiators. Maybe this is not needed (ie, the other endpoint
+ // can take the first valid transport candidate), but otherwise we
+ // must cancel the negotiators...
+ //
+ if (isFullyEstablished()) {
+ acceptedPayloadType = getAcceptedAudioPayloadType(jin);
+ acceptedLocalCandidate = getAcceptedLocalCandidate(jin);
+
+ if (acceptedPayloadType != null && acceptedLocalCandidate != null) {
+ if (acceptedPayloadType.equals(getMediaNeg().getBestCommonAudioPt())
+ && acceptedLocalCandidate.equals(getTransportNeg()
+ .getAcceptedLocalCandidate())) {
+ setState(active);
+ }
+ } else {
+ throw new JingleException(JingleError.MALFORMED_STANZA);
+ }
+ }
+
+ return super.eventAccept(jin);
+ }
+
+ /**
+ * We have received a confirmation.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
+ */
+ public Jingle eventAck(final IQ iq) throws XMPPException {
+ setState(active);
+ return super.eventAck(iq);
+ }
+
+ /**
+ * An error has occurred.
+ *
+ * @throws XMPPException
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage()));
+ super.eventError(iq);
+ }
+ }
+
+ /**
+ * "Active" state: we have an agreement about the session.
+ */
+ private class Active extends JingleNegotiator.State {
+ public Active(final JingleNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * We have a established session: notify the listeners
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventEnter() {
+ PayloadType.Audio bestCommonAudioPt = getMediaNeg().getBestCommonAudioPt();
+ TransportCandidate bestRemoteCandidate = getTransportNeg()
+ .getBestRemoteCandidate();
+ TransportCandidate acceptedLocalCandidate = getTransportNeg()
+ .getAcceptedLocalCandidate();
+
+ // Trigger the session established flag
+ triggerSessionEstablished(bestCommonAudioPt, bestRemoteCandidate,
+ acceptedLocalCandidate);
+
+ super.eventEnter();
+ }
+
+ /**
+ * Terminate the connection.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventTerminate(org.jivesoftware.smackx.packet.Jingle)
+ */
+ public Jingle eventTerminate(final Jingle jin) throws XMPPException {
+ triggerSessionClosed(null);
+ return super.eventTerminate(jin);
+ }
+
+ /**
+ * An error has occurred.
+ *
+ * @throws XMPPException
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage()));
+ super.eventError(iq);
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java
new file mode 100644
index 000000000..621435124
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleListener.java
@@ -0,0 +1,164 @@
+package org.jivesoftware.smackx.jingle;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.nat.TransportCandidate;
+
+/**
+ * Jingle listeners.
+ *
+ * </p>
+ *
+ * This is the list of events that can be observed from a JingleSession and some
+ * sub negotiators. This listeners can be added to different elements of the
+ * Jingle model.
+ *
+ * </p>
+ *
+ * For example, a JingleManager can notify any JingleListener.SessionRequest
+ * listener when a new session request is received. In this case, the
+ * <i>sessionRequested()</i> of the listener will be executed, and the listener
+ * will be able to <i>accept()</i> or <i>decline()</i> the invitation.
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin
+ */
+public interface JingleListener {
+
+ /**
+ * Jingle session request listener.
+ *
+ * @author Alvaro Saurin
+ */
+ public static interface SessionRequest extends JingleListener {
+ /**
+ * A request to start a session has been recieved from another user.
+ *
+ * @param request The request from the other user.
+ */
+ public void sessionRequested(final JingleSessionRequest request);
+ }
+
+ /**
+ * Interface for listening for session events.
+ */
+ public static interface Session extends JingleListener {
+ /**
+ * Notification that the session has been established. Arguments specify
+ * the payload type and transport to use.
+ *
+ * @param pt The Payload tyep to use
+ * @param rc The remote candidate to use for connecting to the remote
+ * service.
+ * @param lc The local candidate where we must listen for connections
+ */
+ public void sessionEstablished(final PayloadType pt, final TransportCandidate rc,
+ final TransportCandidate lc);
+
+ /**
+ * Notification that the session was declined.
+ *
+ * @param reason The reason (if any).
+ */
+ public void sessionDeclined(final String reason);
+
+ /**
+ * Notification that the session was redirected.
+ */
+ public void sessionRedirected(final String redirection);
+
+ /**
+ * Notification that the session was closed normally.
+ *
+ * @param reason The reason (if any).
+ */
+ public void sessionClosed(final String reason);
+
+ /**
+ * Notification that the session was closed due to an exception.
+ *
+ * @param e the exception.
+ */
+ public void sessionClosedOnError(final XMPPException e);
+ }
+
+ /**
+ * Interface for listening to transport events.
+ */
+ public static interface Transport extends JingleListener {
+ /**
+ * Notification that the transport has been established.
+ *
+ * @param local The transport candidate that has been used for listening
+ * in the local machine
+ * @param remote The transport candidate that has been used for
+ * transmitting to the remote machine
+ */
+ public void transportEstablished(final TransportCandidate local,
+ final TransportCandidate remote);
+
+ /**
+ * Notification that a transport must be cancelled.
+ *
+ * @param cand The transport candidate that must be cancelled. A value
+ * of "null" means all the transports for this session.
+ */
+ public void transportClosed(final TransportCandidate cand);
+
+ /**
+ * Notification that the transport was closed due to an exception.
+ *
+ * @param e the exception.
+ */
+ public void transportClosedOnError(final XMPPException e);
+ }
+
+ /**
+ * Interface for listening to media events.
+ */
+ public static interface Media extends JingleListener {
+ /**
+ * Notification that the media has been negotiated and established.
+ *
+ * @param pt The payload type agreed.
+ */
+ public void mediaEstablished(final PayloadType pt);
+
+ /**
+ * Notification that a payload type must be cancelled
+ *
+ * @param cand The payload type that must be closed
+ */
+ public void mediaClosed(final PayloadType cand);
+ }
+
+ /**
+ * Interface for listening to media info events.
+ */
+ public static interface MediaInfo extends JingleListener {
+ /**
+ * The other end is busy.
+ */
+ public void mediaInfoBusy();
+
+ /**
+ * We are on hold.
+ */
+ public void mediaInfoHold();
+
+ /**
+ * The media is muted.
+ */
+ public void mediaInfoMute();
+
+ /**
+ * We are queued.
+ */
+ public void mediaInfoQueued();
+
+ /**
+ * We are ringing.
+ */
+ public void mediaInfoRinging();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java
new file mode 100644
index 000000000..49bf6fa41
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManager.java
@@ -0,0 +1,363 @@
+/**
+ * $RCSfile: JingleManager.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2006/10/17 19:12:42 $
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jivesoftware.smack.ConnectionEstablishedListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.nat.TransportResolver;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.Jingle;
+
+/**
+ * The JingleManager is a facade built upon Jabber Jingle (JEP-166) to allow the
+ * use of the Jingle extension. This implementation allows the user to simply
+ * use this class for setting the Jingle parameters.
+ *
+ * </p>
+ *
+ * This is an example of how to use the Jingle code:
+ *
+ * <pre>
+ * XMPPConnection con = new XMPPConnection(&quot;jabber.org&quot;);
+ *
+ * TransportResolver tm = new STUNResolver();
+ *
+ * JingleManager jmanager = new JingleManager(conn, tm);
+ *
+ * // Insert the payloads in the &quot;mypayloads&quot; list...
+ *
+ * OutgoingJingleSession jsession = jmanager.createOutgoingJingleSession(mypayloads);
+ *
+ * // Install some session listeners...
+ *
+ * </pre>
+ *
+ * In order to use the Jingle extension, the user must provide a
+ * TransportResolver that will handle the resolution of the external address of
+ * the machine. This resolver can be initialized with several default resolvers,
+ * including a fixed solver that can be used when the address and port are know
+ * in advance. See TransportResolver or TransportManager for a complete list of
+ * resolution services.
+ *
+ * </p>
+ *
+ * Before creating an outgoing connection, the user must create session
+ * listeners that will be called when different events happen. The most
+ * important event is <i>sessionEstablished()</i>, that will be called when all
+ * the negotiations are finished, providing the payload type for the
+ * transmission as well as the remote and local addresses and ports for the
+ * communication. See JingleListener for a complete list of events that can be
+ * observed.
+ *
+ * </p>
+ *
+ * @see JingleListener
+ * @see TransportResolver
+ * @see TransportManager
+ * @see OutgoingJingleSession
+ * @see IncomingJingleSession
+ *
+ * </p>
+ * @author Alvaro Saurin
+ */
+public class JingleManager {
+
+ // non-static
+
+ // Listeners for manager events (ie, session requests...)
+ private List listeners;
+
+ // The XMPP connection
+ private XMPPConnection connection;
+
+ // The Jingle transport manager
+ private final TransportResolver resolver;
+
+ static {
+ ProviderManager.addIQProvider("jingle", "http://jabber.org/protocol/jingle",
+ new org.jivesoftware.smackx.provider.JingleProvider());
+
+ ProviderManager.addExtensionProvider("description", "http://jabber.org/protocol/jingle/description/audio",
+ new org.jivesoftware.smackx.provider.JingleContentDescriptionProvider.Audio());
+
+ ProviderManager.addExtensionProvider("transport", "http://jabber.org/protocol/jingle/transport/ice",
+ new org.jivesoftware.smackx.provider.JingleTransportProvider.Ice());
+ ProviderManager.addExtensionProvider("transport", "http://jabber.org/protocol/jingle/transport/raw-udp",
+ new org.jivesoftware.smackx.provider.JingleTransportProvider.RawUdp());
+
+ ProviderManager.addExtensionProvider("busy", "http://jabber.org/protocol/jingle/info/audio",
+ new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Busy());
+ ProviderManager.addExtensionProvider("hold", "http://jabber.org/protocol/jingle/info/audio",
+ new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Hold());
+ ProviderManager.addExtensionProvider("mute", "http://jabber.org/protocol/jingle/info/audio",
+ new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Mute());
+ ProviderManager.addExtensionProvider("queued", "http://jabber.org/protocol/jingle/info/audio",
+ new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Queued());
+ ProviderManager.addExtensionProvider("ringing", "http://jabber.org/protocol/jingle/info/audio",
+ new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Ringing());
+
+
+ // Enable the Jingle support on every established connection
+ // The ServiceDiscoveryManager class should have been already
+ // initialized
+ XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() {
+ public void connectionEstablished(final XMPPConnection connection) {
+ JingleManager.setServiceEnabled(connection, true);
+ }
+ });
+ }
+
+ /**
+ * Private constructor
+ */
+ private JingleManager() {
+ resolver = null;
+ }
+
+ /**
+ * Default constructor, with a connection.
+ *
+ * @param conn
+ */
+ public JingleManager(final XMPPConnection conn, final TransportResolver res) {
+ connection = conn;
+ resolver = res;
+ }
+
+ /**
+ * Enables or disables the Jingle support on a given connection.
+ * <p>
+ *
+ * Before starting any Jingle media session, check that the user can handle
+ * it. Enable the Jingle support to indicate that this client handles Jingle
+ * messages.
+ *
+ * @param connection the connection where the service will be enabled or
+ * disabled
+ * @param enabled indicates if the service will be enabled or disabled
+ */
+ public synchronized static void setServiceEnabled(final XMPPConnection connection,
+ final boolean enabled) {
+ if (isServiceEnabled(connection) == enabled) {
+ return;
+ }
+
+ if (enabled) {
+ ServiceDiscoveryManager.getInstanceFor(connection).addFeature(
+ Jingle.NAMESPACE);
+ } else {
+ ServiceDiscoveryManager.getInstanceFor(connection).removeFeature(
+ Jingle.NAMESPACE);
+ }
+ }
+
+ /**
+ * Returns true if the Jingle support is enabled for the given connection.
+ *
+ * @param connection the connection to look for Jingle support
+ * @return a boolean indicating if the Jingle support is enabled for the
+ * given connection
+ */
+ public static boolean isServiceEnabled(final XMPPConnection connection) {
+ return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(
+ Jingle.NAMESPACE);
+ }
+
+ /**
+ * Returns true if the specified user handles Jingle messages.
+ *
+ * @param connection the connection to use to perform the service discovery
+ * @param userID the user to check. A fully qualified xmpp ID, e.g.
+ * jdoe@example.com
+ * @return a boolean indicating whether the specified user handles Jingle
+ * messages
+ */
+ public static boolean isServiceEnabled(final XMPPConnection connection,
+ final String userID) {
+ try {
+ DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection)
+ .discoverInfo(userID);
+ return result.containsFeature(Jingle.NAMESPACE);
+ } catch (XMPPException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Add a Jingle session request listener to listen to incoming session
+ * requests.
+ *
+ * @param li The listener
+ *
+ * @see #removeJingleSessionRequestListener(JingleListener.SessionRequest)
+ * @see JingleListener
+ */
+ public synchronized void addJingleSessionRequestListener(
+ final JingleListener.SessionRequest li) {
+ if (li != null) {
+ if (listeners == null) {
+ initJingleSessionRequestListeners();
+ }
+ synchronized (listeners) {
+ listeners.add(li);
+ }
+ }
+ }
+
+ /**
+ * Removes a Jingle session listener.
+ *
+ * @param li The jingle session listener to be removed
+ * @see #addJingleSessionRequestListener(JingleListener.SessionRequest)
+ * @see JingleListener
+ */
+ public void removeJingleSessionRequestListener(final JingleListener.SessionRequest li) {
+ if (listeners == null) {
+ return;
+ }
+ synchronized (listeners) {
+ listeners.remove(li);
+ }
+ }
+
+ /**
+ * Register the listeners, waiting for a Jingle packet that tries to
+ * establish a new session.
+ */
+ private void initJingleSessionRequestListeners() {
+ PacketFilter initRequestFilter = new PacketFilter() {
+ // Return true if we accept this packet
+ public boolean accept(Packet pin) {
+ if (pin instanceof IQ) {
+ IQ iq = (IQ) pin;
+ if (iq.getType().equals(IQ.Type.SET)) {
+ if (iq instanceof Jingle) {
+ Jingle jin = (Jingle) pin;
+ if (jin.getAction().equals(Jingle.Action.SESSIONINITIATE)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ listeners = new ArrayList();
+
+ // Start a packet listener for session initiation requests
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(final Packet packet) {
+ triggerSessionRequested((Jingle) packet);
+ }
+ }, initRequestFilter);
+ }
+
+ /**
+ * Activates the listeners on a Jingle session request.
+ *
+ * @param initJin The packet that must be passed to the listeners.
+ */
+ protected void triggerSessionRequested(final Jingle initJin) {
+ JingleListener.SessionRequest[] listeners = null;
+
+ // Make a synchronized copy of the listeners
+ synchronized (this.listeners) {
+ listeners = new JingleListener.SessionRequest[this.listeners.size()];
+ this.listeners.toArray(listeners);
+ }
+
+ // ... and let them know of the event
+ JingleSessionRequest request = new JingleSessionRequest(this, initJin);
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].sessionRequested(request);
+ }
+ }
+
+ // Session creation
+
+ /**
+ * Creates an Jingle session to start a communication with another user.
+ *
+ * @param responder The fully qualified jabber ID with resource of the other
+ * user.
+ * @return The session on which the negotiation can be run.
+ */
+ public OutgoingJingleSession createOutgoingJingleSession(final String responder,
+ final List payloadTypes) {
+
+ if (responder == null || StringUtils.parseName(responder).length() <= 0
+ || StringUtils.parseServer(responder).length() <= 0
+ || StringUtils.parseResource(responder).length() <= 0) {
+ throw new IllegalArgumentException(
+ "The provided user id was not fully qualified");
+ }
+
+ OutgoingJingleSession session = new OutgoingJingleSession(connection, responder,
+ payloadTypes, resolver);
+ return session;
+ }
+
+ /**
+ * When the session request is acceptable, this method should be invoked. It
+ * will create an JingleSession which allows the negotiation to procede.
+ *
+ * @param request The remote request that is being accepted.
+ * @return The session which manages the rest of the negotiation.
+ */
+ public IncomingJingleSession createIncomingJingleSession(
+ final JingleSessionRequest request, final List payloadTypes) {
+ if (request == null) {
+ throw new NullPointerException("Received request cannot be null");
+ }
+
+ IncomingJingleSession session = new IncomingJingleSession(connection, request
+ .getFrom(), payloadTypes, resolver);
+
+ return session;
+ }
+
+ /**
+ * Reject the session. If we don't want to accept the new session, send an
+ * appropriate error packet.
+ *
+ * @param request the request.
+ */
+ protected void rejectIncomingJingleSession(final JingleSessionRequest request) {
+ Jingle initiation = request.getJingle();
+
+ IQ rejection = JingleSession.createError(initiation.getPacketID(), initiation
+ .getFrom(), initiation.getTo(), 403, "Declined");
+ connection.sendPacket(rejection);
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java
new file mode 100644
index 000000000..277ac2258
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleManagerTest.java
@@ -0,0 +1,662 @@
+/**
+ * $RCSfile: JingleManagerTest.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2006/10/17 19:12:42 $
+ *
+ * Copyright (C) 2002-2006 Jive Software. All rights reserved.
+ * ====================================================================
+ * The Jive Software License (based on Apache Software License, Version 1.1)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution,
+ * if any, must include the following acknowledgment:
+ * "This product includes software developed by
+ * Jive Software (http://www.jivesoftware.com)."
+ * Alternately, this acknowledgment may appear in the software itself,
+ * if and wherever such third-party acknowledgments normally appear.
+ *
+ * 4. The names "Smack" and "Jive Software" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For written permission, please
+ * contact webmaster@jivesoftware.com.
+ *
+ * 5. Products derived from this software may not be called "Smack",
+ * nor may "Smack" appear in their name, without prior written
+ * permission of Jive Software.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ */
+
+package org.jivesoftware.smackx.jingle;
+
+import java.util.ArrayList;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.test.SmackTestCase;
+import org.jivesoftware.smackx.nat.FixedResolver;
+import org.jivesoftware.smackx.nat.TransportCandidate;
+import org.jivesoftware.smackx.nat.TransportResolver;
+import org.jivesoftware.smackx.packet.Jingle;
+
+/**
+ * Test the Jingle extension using the high level API
+ * </p>
+ *
+ * @author Alvaro Saurin
+ */
+public class JingleManagerTest extends SmackTestCase {
+
+ private int counter;
+
+ private final Object mutex = new Object();
+
+ /**
+ * Constructor for JingleManagerTest.
+ *
+ * @param name
+ */
+ public JingleManagerTest(final String name) {
+ super(name);
+
+ resetCounter();
+ }
+
+ // Counter management
+
+ private void resetCounter() {
+ synchronized (mutex) {
+ counter = 0;
+ }
+ }
+
+ private void incCounter() {
+ synchronized (mutex) {
+ counter++;
+ }
+ }
+
+ private int valCounter() {
+ int val;
+ synchronized (mutex) {
+ val = counter;
+ }
+ return val;
+ }
+
+ /**
+ * Generate a list of payload types
+ *
+ * @return A testing list
+ */
+ private ArrayList getTestPayloads1() {
+ ArrayList result = new ArrayList();
+
+ result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000));
+ result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000));
+ result.add(new PayloadType.Audio(36, "supercodec-3", 2, 28000));
+ result.add(new PayloadType.Audio(45, "supercodec-4", 1, 98000));
+
+ return result;
+ }
+
+ private ArrayList getTestPayloads2() {
+ ArrayList result = new ArrayList();
+
+ result.add(new PayloadType.Audio(27, "supercodec-3", 2, 28000));
+ result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000));
+ result.add(new PayloadType.Audio(32, "supercodec-4", 1, 98000));
+ result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000));
+
+ return result;
+ }
+
+ private ArrayList getTestPayloads3() {
+ ArrayList result = new ArrayList();
+
+ result.add(new PayloadType.Audio(91, "badcodec-1", 2, 28000));
+ result.add(new PayloadType.Audio(92, "badcodec-2", 1, 44000));
+ result.add(new PayloadType.Audio(93, "badcodec-3", 1, 98000));
+ result.add(new PayloadType.Audio(94, "badcodec-4", 2, 14000));
+
+ return result;
+ }
+
+ /**
+ * Test for the session request detection. Here, we use the same filter we
+ * use in the JingleManager...
+ */
+ public void testInitJingleSessionRequestListeners() {
+
+ resetCounter();
+
+ PacketFilter initRequestFilter = new PacketFilter() {
+ // Return true if we accept this packet
+ public boolean accept(Packet pin) {
+ if (pin instanceof IQ) {
+ IQ iq = (IQ) pin;
+ if (iq.getType().equals(IQ.Type.SET)) {
+ if (iq instanceof Jingle) {
+ Jingle jin = (Jingle) pin;
+ if (jin.getAction().equals(Jingle.Action.SESSIONINITIATE)) {
+ System.out
+ .println("Session initiation packet accepted... ");
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ // Start a packet listener for session initiation requests
+ getConnection(0).addPacketListener(new PacketListener() {
+ public void processPacket(final Packet packet) {
+ System.out.println("Packet detected... ");
+ incCounter();
+ }
+ }, initRequestFilter);
+
+ // Create a dummy packet for testing...
+ IQfake iqSent = new IQfake(
+ " <jingle xmlns='http://jabber.org/protocol/jingle'"
+ + " initiator=\"gorrino@viejo.com\""
+ + " responder=\"colico@hepatico.com\""
+ + " action=\"session-initiate\" sid=\"08666555\">"
+ + " <description xmlns='http://jabber.org/protocol/jingle/content/audio'>"
+ + " <payload-type id=\"34\" name=\"supercodec-34\"/>"
+ + " <payload-type id=\"23\" name=\"supercodec-23\"/>"
+ + " </description>"
+ + " <transport xmlns='http://jabber.org/protocol/jingle/transport/ice'>"
+ + " <candidate generation=\"1\"" + " ip=\"192.168.1.1\""
+ + " password=\"secret\"" + " port=\"8080\""
+ + " username=\"username\"" + " preference=\"1\"/>"
+ + " </transport>" + "</jingle>");
+
+ iqSent.setTo(getFullJID(0));
+ iqSent.setFrom(getFullJID(0));
+ iqSent.setType(IQ.Type.SET);
+
+ System.out.println("Sending packet and waiting... ");
+ getConnection(0).sendPacket(iqSent);
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ }
+
+ System.out.println("Awake... ");
+ assertTrue(valCounter() > 0);
+ }
+
+ /**
+ * High level API test. This is a simple test to use with a XMPP client and
+ * check if the client receives the message 1. User_1 will send an
+ * invitation to user_2.
+ */
+ public void testSendSimpleMessage() {
+
+ resetCounter();
+
+ try {
+ TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222);
+ TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567);
+
+ JingleManager man0 = new JingleManager(getConnection(0), tr1);
+ JingleManager man1 = new JingleManager(getConnection(1), tr2);
+
+ // Session 1 waits for connections
+ man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() {
+ /**
+ * Called when a new session request is detected
+ */
+ public void sessionRequested(final JingleSessionRequest request) {
+ incCounter();
+ System.out.println("Session request detected, from "
+ + request.getFrom());
+ }
+ });
+
+ // Session 0 starts a request
+ System.out.println("Starting session request, to " + getFullJID(1) + "...");
+ OutgoingJingleSession session0 = man0.createOutgoingJingleSession(
+ getFullJID(1), getTestPayloads1());
+ session0.start(null);
+
+ Thread.sleep(5000);
+
+ assertTrue(valCounter() > 0);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("An error occured with Jingle");
+ }
+ }
+
+ /**
+ * High level API test. This is a simple test to use with a XMPP client and
+ * check if the client receives the message 1. User_1 will send an
+ * invitation to user_2.
+ */
+ public void testAcceptJingleSession() {
+
+ resetCounter();
+
+ try {
+ TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222);
+ TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567);
+
+ final JingleManager man0 = new JingleManager(getConnection(0), tr1);
+ final JingleManager man1 = new JingleManager(getConnection(1), tr2);
+
+ man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() {
+ /**
+ * Called when a new session request is detected
+ */
+ public void sessionRequested(final JingleSessionRequest request) {
+ incCounter();
+ System.out.println("Session request detected, from "
+ + request.getFrom() + ": accepting.");
+
+ // We accept the request
+ IncomingJingleSession session1 = request.accept(getTestPayloads2());
+ try {
+ session1.start(request);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ // Session 0 starts a request
+ System.out.println("Starting session request, to " + getFullJID(1) + "...");
+ OutgoingJingleSession session0 = man0.createOutgoingJingleSession(
+ getFullJID(1), getTestPayloads1());
+ session0.start(null);
+
+ Thread.sleep(20000);
+
+ assertTrue(valCounter() > 0);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("An error occured with Jingle");
+ }
+ }
+
+ /**
+ * This is a simple test where both endpoints have exactly the same payloads
+ * and the session is accepted.
+ */
+ public void testEqualPayloadsSetSession() {
+
+ resetCounter();
+
+ try {
+ TransportResolver tr1 = new FixedResolver("127.0.0.1", 54213);
+ TransportResolver tr2 = new FixedResolver("127.0.0.1", 54531);
+
+ final JingleManager man0 = new JingleManager(getConnection(0), tr1);
+ final JingleManager man1 = new JingleManager(getConnection(1), tr2);
+
+ man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() {
+ /**
+ * Called when a new session request is detected
+ */
+ public void sessionRequested(final JingleSessionRequest request) {
+ System.out.println("Session request detected, from "
+ + request.getFrom() + ": accepting.");
+
+ // We accept the request
+ IncomingJingleSession session1 = request.accept(getTestPayloads1());
+
+ session1.addListener(new JingleListener.Session() {
+ public void sessionClosed(final String reason) {
+ System.out.println("sessionClosed().");
+ }
+
+ public void sessionClosedOnError(final XMPPException e) {
+ System.out.println("sessionClosedOnError().");
+ }
+
+ public void sessionDeclined(final String reason) {
+ System.out.println("sessionDeclined().");
+ }
+
+ public void sessionEstablished(final PayloadType pt,
+ final TransportCandidate rc, final TransportCandidate lc) {
+ incCounter();
+ System.out
+ .println("Responder: the session is fully established.");
+ System.out.println("+ Payload Type: " + pt.getId());
+ System.out.println("+ Local IP/port: " + lc.getIP() + ":"
+ + lc.getPort());
+ System.out.println("+ Remote IP/port: " + rc.getIP() + ":"
+ + rc.getPort());
+ }
+
+ public void sessionRedirected(final String redirection) {
+ }
+ });
+
+ try {
+ session1.start(request);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ // Session 0 starts a request
+ System.out.println("Starting session request with equal payloads, to "
+ + getFullJID(1) + "...");
+ OutgoingJingleSession session0 = man0.createOutgoingJingleSession(
+ getFullJID(1), getTestPayloads1());
+ session0.start(null);
+
+ Thread.sleep(20000);
+
+ assertTrue(valCounter() == 2);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("An error occured with Jingle");
+ }
+ }
+
+ /**
+ * This is a simple test where the user_2 rejects the Jingle session.
+ *
+ */
+ public void testStagesSession() {
+
+ resetCounter();
+
+ try {
+ TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222);
+ TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567);
+
+ final JingleManager man0 = new JingleManager(getConnection(0), tr1);
+ final JingleManager man1 = new JingleManager(getConnection(1), tr2);
+
+ man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() {
+ /**
+ * Called when a new session request is detected
+ */
+ public void sessionRequested(final JingleSessionRequest request) {
+ System.out.println("Session request detected, from "
+ + request.getFrom() + ": accepting.");
+
+ // We accept the request
+ IncomingJingleSession session1 = request.accept(getTestPayloads2());
+
+ session1.addListener(new JingleListener.Session() {
+ public void sessionClosed(final String reason) {
+ System.out.println("sessionClosed().");
+ }
+
+ public void sessionClosedOnError(final XMPPException e) {
+ System.out.println("sessionClosedOnError().");
+ }
+
+ public void sessionDeclined(final String reason) {
+ System.out.println("sessionDeclined().");
+ }
+
+ public void sessionEstablished(final PayloadType pt,
+ final TransportCandidate rc, final TransportCandidate lc) {
+ incCounter();
+ System.out
+ .println("Responder: the session is fully established.");
+ System.out.println("+ Payload Type: " + pt.getId());
+ System.out.println("+ Local IP/port: " + lc.getIP() + ":"
+ + lc.getPort());
+ System.out.println("+ Remote IP/port: " + rc.getIP() + ":"
+ + rc.getPort());
+ }
+
+ public void sessionRedirected(final String redirection) {
+ }
+ });
+
+ try {
+ session1.start(request);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ // Session 0 starts a request
+ System.out.println("Starting session request, to " + getFullJID(1) + "...");
+ OutgoingJingleSession session0 = man0.createOutgoingJingleSession(
+ getFullJID(1), getTestPayloads1());
+
+ session0.addListener(new JingleListener.Session() {
+ public void sessionClosed(final String reason) {
+ }
+
+ public void sessionClosedOnError(final XMPPException e) {
+ }
+
+ public void sessionDeclined(final String reason) {
+ }
+
+ public void sessionEstablished(final PayloadType pt,
+ final TransportCandidate rc, final TransportCandidate lc) {
+ incCounter();
+ System.out.println("Initiator: the session is fully established.");
+ System.out.println("+ Payload Type: " + pt.getId());
+ System.out.println("+ Local IP/port: " + lc.getIP() + ":"
+ + lc.getPort());
+ System.out.println("+ Remote IP/port: " + rc.getIP() + ":"
+ + rc.getPort());
+ }
+
+ public void sessionRedirected(final String redirection) {
+ }
+ });
+ session0.start(null);
+
+ Thread.sleep(20000);
+
+ assertTrue(valCounter() == 2);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("An error occured with Jingle");
+ }
+ }
+
+ /**
+ * This is a simple test where the user_2 rejects the Jingle session.
+ */
+ public void testRejectSession() {
+
+ resetCounter();
+
+ try {
+ TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222);
+ TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567);
+
+ final JingleManager man0 = new JingleManager(getConnection(0), tr1);
+ final JingleManager man1 = new JingleManager(getConnection(1), tr2);
+
+ man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() {
+ /**
+ * Called when a new session request is detected
+ */
+ public void sessionRequested(final JingleSessionRequest request) {
+ System.out.println("Session request detected, from "
+ + request.getFrom() + ": rejecting.");
+
+ // We reject the request
+ request.reject();
+ }
+ });
+
+ // Session 0 starts a request
+ System.out.println("Starting session request, to " + getFullJID(1) + "...");
+ OutgoingJingleSession session0 = man0.createOutgoingJingleSession(
+ getFullJID(1), getTestPayloads1());
+
+ session0.addListener(new JingleListener.Session() {
+ public void sessionClosed(final String reason) {
+ }
+
+ public void sessionClosedOnError(final XMPPException e) {
+ }
+
+ public void sessionDeclined(final String reason) {
+ incCounter();
+ System.out
+ .println("The session has been detected as rejected with reason: "
+ + reason);
+ }
+
+ public void sessionEstablished(final PayloadType pt,
+ final TransportCandidate rc, final TransportCandidate lc) {
+ }
+
+ public void sessionRedirected(final String redirection) {
+ }
+ });
+
+ session0.start(null);
+
+ Thread.sleep(20000);
+
+ assertTrue(valCounter() > 0);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("An error occured with Jingle");
+ }
+ }
+
+ /**
+ * This is a simple test where the user_2 rejects the Jingle session.
+ */
+ public void testIncompatibleCodecs() {
+
+ resetCounter();
+
+ try {
+ TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222);
+ TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567);
+
+ final JingleManager man0 = new JingleManager(getConnection(0), tr1);
+ final JingleManager man1 = new JingleManager(getConnection(1), tr2);
+
+ man1.addJingleSessionRequestListener(new JingleListener.SessionRequest() {
+ /**
+ * Called when a new session request is detected
+ */
+ public void sessionRequested(final JingleSessionRequest request) {
+ System.out.println("Session request detected, from "
+ + request.getFrom() + ": accepting.");
+
+ // We reject the request
+ IncomingJingleSession ses = request.accept(getTestPayloads3());
+ try {
+ ses.start(request);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ // Session 0 starts a request
+ System.out.println("Starting session request, to " + getFullJID(1) + "...");
+ OutgoingJingleSession session0 = man0.createOutgoingJingleSession(
+ getFullJID(1), getTestPayloads1());
+
+ session0.addListener(new JingleListener.Session() {
+ public void sessionClosed(final String reason) {
+ }
+
+ public void sessionClosedOnError(final XMPPException e) {
+ incCounter();
+ System.out
+ .println("The session has been close on error with reason: "
+ + e.getMessage());
+ }
+
+ public void sessionDeclined(final String reason) {
+ incCounter();
+ System.out
+ .println("The session has been detected as rejected with reason: "
+ + reason);
+ }
+
+ public void sessionEstablished(final PayloadType pt,
+ final TransportCandidate rc, final TransportCandidate lc) {
+ }
+
+ public void sessionRedirected(final String redirection) {
+ }
+ });
+
+ session0.start(null);
+
+ Thread.sleep(20000);
+
+ assertTrue(valCounter() > 0);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("An error occured with Jingle");
+ }
+ }
+
+ protected int getMaxConnections() {
+ return 2;
+ }
+
+ /**
+ * Simple class for testing an IQ...
+ *
+ * @author Alvaro Saurin
+ */
+ private class IQfake extends IQ {
+ private String s;
+
+ public IQfake(final String s) {
+ super();
+ this.s = s;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(s);
+ return buf.toString();
+ }
+ }
+}
+
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java
new file mode 100644
index 000000000..f7b1ce54e
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleNegotiator.java
@@ -0,0 +1,341 @@
+package org.jivesoftware.smackx.jingle;
+
+import java.util.ArrayList;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.Jingle;
+import org.jivesoftware.smackx.packet.JingleError;
+
+/**
+ * Basic Jingle negotiator.
+ *
+ * </p>
+ *
+ * JingleNegotiator implements some basic behavior for every Jingle negotiation.
+ * It implements a "state" pattern: each stage should process Jingle packets and
+ * act depending on the current state in the negotiation...
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin
+ */
+public abstract class JingleNegotiator {
+
+ private State state; // Current negotiation state
+
+ private XMPPConnection connection; // The connection associated
+
+ private final ArrayList listeners = new ArrayList();
+
+ private String expectedAckId;
+
+ /**
+ * Default constructor.
+ */
+ public JingleNegotiator() {
+ this(null);
+ }
+
+ /**
+ * Default constructor with a XMPPConnection
+ *
+ * @param connection the connection associated
+ */
+ public JingleNegotiator(final XMPPConnection connection) {
+ this.connection = connection;
+ state = null;
+ }
+
+ /**
+ * Get the XMPP connection associated with this negotiation.
+ *
+ * @return the connection
+ */
+ public XMPPConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Set the XMPP connection associated.
+ *
+ * @param connection the connection to set
+ */
+ public void setConnection(final XMPPConnection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Inform if current state is null
+ *
+ * @return true if current state is null
+ */
+ public boolean invalidState() {
+ return state == null;
+ }
+
+ /**
+ * Return the current state
+ *
+ * @return the state
+ */
+ public State getState() {
+ return state;
+ }
+
+ /**
+ * Return the current state class
+ *
+ * @return the state
+ */
+ public Class getStateClass() {
+ if (state != null) {
+ return state.getClass();
+ } else {
+ return Object.class;
+ }
+ }
+
+ /**
+ * Set the new state.
+ *
+ * @param state the state to set
+ * @throws XMPPException
+ */
+ protected void setState(final State newState) {
+ boolean transition = newState != state;
+
+ if (transition && state != null) {
+ state.eventExit();
+ }
+
+ state = newState;
+
+ if (transition && state != null) {
+ state.eventEnter();
+ }
+ }
+
+ // Acks management
+
+ public void addExpectedId(final String id) {
+ expectedAckId = id;
+ }
+
+ public boolean isExpectedId(final String id) {
+ if (id != null) {
+ return id.equals(expectedAckId);
+ } else {
+ return false;
+ }
+ }
+
+ public void removeExpectedId(final String id) {
+ addExpectedId((String) null);
+ }
+
+ // Listeners
+
+ /**
+ * Add a Jingle session listener to listen to incoming session requests.
+ *
+ * @param li The listener
+ *
+ * @see JingleListener
+ */
+ public void addListener(final JingleListener li) {
+ synchronized (listeners) {
+ listeners.add(li);
+ }
+ }
+
+ /**
+ * Removes a Jingle session listener.
+ *
+ * @param li The jingle session listener to be removed
+ * @see JingleListener
+ */
+ public void removeListener(final JingleListener li) {
+ synchronized (listeners) {
+ listeners.remove(li);
+ }
+ }
+
+ /**
+ * Get a copy of the listeners
+ *
+ * @return a copy of the listeners
+ */
+ protected ArrayList getListenersList() {
+ ArrayList result;
+
+ synchronized (listeners) {
+ result = new ArrayList(listeners);
+ }
+
+ return result;
+ }
+
+ /**
+ * Dispatch an incomming packet. This method is responsible for recognizing
+ * the packet type and, depending on the current state, deliverying the
+ * packet to the right event handler and wait for a response.
+ *
+ * @param iq the packet received
+ * @param id the ID of the response that will be sent
+ * @return the new packet to send (either a Jingle or an IQ error).
+ * @throws XMPPException
+ */
+ public abstract IQ dispatchIncomingPacket(final IQ iq, final String id)
+ throws XMPPException;
+
+ /**
+ * Close the negotiation.
+ */
+ public void close() {
+ setState(null);
+ }
+
+ /**
+ * A Jingle exception.
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+ public static class JingleException extends XMPPException {
+
+ private final JingleError error;
+
+ /**
+ * Default constructor.
+ */
+ public JingleException() {
+ super();
+ error = null;
+ }
+
+ /**
+ * Constructor with an error message.
+ *
+ * @param error The message.
+ */
+ public JingleException(final String msg) {
+ super(msg);
+ error = null;
+ }
+
+ /**
+ * Constructor with an error response.
+ *
+ * @param error The error message.
+ */
+ public JingleException(final JingleError error) {
+ super();
+ this.error = error;
+ }
+
+ /**
+ * Return the error message.
+ *
+ * @return the error
+ */
+ public JingleError getError() {
+ return error;
+ }
+ }
+
+ /**
+ * Negotiation state and events.
+ *
+ * </p>
+ *
+ * Describes the negotiation stage.
+ */
+ public static class State {
+
+ private JingleNegotiator neg; // The negotiator
+
+ /**
+ * Default constructor, with a reference to the negotiator.
+ *
+ * @param neg The negotiator instance.
+ */
+ public State(final JingleNegotiator neg) {
+ this.neg = neg;
+ }
+
+ /**
+ * Get the negotiator
+ *
+ * @return the negotiator.
+ */
+ public JingleNegotiator getNegotiator() {
+ return neg;
+ }
+
+ /**
+ * Set the negotiator.
+ *
+ * @param neg the neg to set
+ */
+ public void setNegotiator(final JingleNegotiator neg) {
+ this.neg = neg;
+ }
+
+ // State transition events
+
+ public Jingle eventAck(final IQ iq) throws XMPPException {
+ // We have received an Ack
+ return null;
+ }
+
+ public void eventError(final IQ iq) throws XMPPException {
+ throw new JingleException(iq.getError().getMessage());
+ }
+
+ public Jingle eventInvite() throws XMPPException {
+ throw new IllegalStateException(
+ "Negotiation can not be started in this state.");
+ }
+
+ public Jingle eventInitiate(final Jingle jin) throws XMPPException {
+ return null;
+ }
+
+ public Jingle eventAccept(final Jingle jin) throws XMPPException {
+ return null;
+ }
+
+ public Jingle eventRedirect(final Jingle jin) throws XMPPException {
+ return null;
+ }
+
+ public Jingle eventModify(final Jingle jin) throws XMPPException {
+ return null;
+ }
+
+ public Jingle eventDecline(final Jingle jin) throws XMPPException {
+ return null;
+ }
+
+ public Jingle eventInfo(final Jingle jin) throws XMPPException {
+ return null;
+ }
+
+ public Jingle eventTerminate(final Jingle jin) throws XMPPException {
+ if (neg != null) {
+ neg.close();
+ }
+ return null;
+ }
+
+ public void eventEnter() {
+ }
+
+ public void eventExit() {
+ if (neg != null) {
+ neg.removeExpectedId(null);
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java
new file mode 100644
index 000000000..39d686fd3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSession.java
@@ -0,0 +1,1020 @@
+/**
+ * $RCSfile: JingleSession.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2006/10/17 19:12:42 $
+ *
+ * Copyright (C) 2002-2006 Jive Software. All rights reserved.
+ * ====================================================================
+ * The Jive Software License (based on Apache Software License, Version 1.1)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution,
+ * if any, must include the following acknowledgment:
+ * "This product includes software developed by
+ * Jive Software (http://www.jivesoftware.com)."
+ * Alternately, this acknowledgment may appear in the software itself,
+ * if and wherever such third-party acknowledgments normally appear.
+ *
+ * 4. The names "Smack" and "Jive Software" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For written permission, please
+ * contact webmaster@jivesoftware.com.
+ *
+ * 5. Products derived from this software may not be called "Smack",
+ * nor may "Smack" appear in their name, without prior written
+ * permission of Jive Software.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ */
+
+package org.jivesoftware.smackx.jingle;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Random;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.nat.TransportCandidate;
+import org.jivesoftware.smackx.packet.Jingle;
+import org.jivesoftware.smackx.packet.JingleContentDescription;
+import org.jivesoftware.smackx.packet.JingleContentInfo;
+import org.jivesoftware.smackx.packet.JingleError;
+import org.jivesoftware.smackx.packet.JingleTransport;
+import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType;
+import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate;
+
+/**
+ * A Jingle session.
+ *
+ * </p>
+ *
+ * This class contains some basic properties of every Jingle session. However,
+ * the concrete implementation will be found in subclasses.
+ *
+ * </p>
+ *
+ * @see IncomingJingleSession
+ * @see OutgoingJingleSession
+ *
+ * </p>
+ * @author Alvaro Saurin
+ */
+public abstract class JingleSession extends JingleNegotiator {
+
+ // static
+
+ private static final HashMap sessions = new HashMap();
+
+ private static final Random randomGenerator = new Random();
+
+ // non-static
+
+ private String initiator; // Who started the communication
+
+ private String responder; // The other endpoint
+
+ private String sid; // A unique id that identifies this session
+
+ private MediaNegotiator mediaNeg; // The description...
+
+ private TransportNegotiator transNeg; // and transport negotiators
+
+ PacketListener packetListener;
+
+ PacketFilter packetFilter;
+
+ /**
+ * Default constructor.
+ */
+ public JingleSession(final XMPPConnection conn, final String ini, final String res,
+ final String sessionid) {
+ super(conn);
+
+ mediaNeg = null;
+ transNeg = null;
+
+ initiator = ini;
+ responder = res;
+ sid = sessionid;
+
+ // Add the session to the list and register the listeneres
+ registerInstance();
+ installConnectionListeners(conn);
+ }
+
+ /**
+ * Default constructor without session id.
+ */
+ public JingleSession(final XMPPConnection conn, final String ini, final String res) {
+ this(conn, ini, res, null);
+ }
+
+ /**
+ * Get the session initiator
+ *
+ * @return the initiator
+ */
+ public String getInitiator() {
+ return initiator;
+ }
+
+ /**
+ * Set the session initiator
+ *
+ * @param initiator the initiator to set
+ */
+ public void setInitiator(final String initiator) {
+ this.initiator = initiator;
+ }
+
+ /**
+ * Get the session responder
+ *
+ * @return the responder
+ */
+ public String getResponder() {
+ return responder;
+ }
+
+ /**
+ * Set the session responder.
+ *
+ * @param responder the receptor to set
+ */
+ public void setResponder(final String responder) {
+ this.responder = responder;
+ }
+
+ /**
+ * Get the session ID
+ *
+ * @return the sid
+ */
+ public String getSid() {
+ return sid;
+ }
+
+ /**
+ * Set the session ID
+ *
+ * @param sid the sid to set
+ */
+ protected void setSid(final String sessionId) {
+ sid = sessionId;
+ }
+
+ /**
+ * Generate a unique session ID.
+ */
+ protected String generateSessionId() {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(Math.abs(randomGenerator.nextLong()));
+
+ return buffer.toString();
+ }
+
+ /**
+ * Obtain the description negotiator for this session
+ *
+ * @return the description negotiator
+ */
+ protected MediaNegotiator getMediaNeg() {
+ return mediaNeg;
+ }
+
+ /**
+ * Set the media negotiator.
+ *
+ * @param mediaNeg the description negotiator to set
+ */
+ protected void setMediaNeg(final MediaNegotiator mediaNeg) {
+ destroyMediaNeg();
+ this.mediaNeg = mediaNeg;
+ }
+
+ /**
+ * Destroy the media negotiator.
+ */
+ protected void destroyMediaNeg() {
+ if (mediaNeg != null) {
+ mediaNeg.close();
+ mediaNeg = null;
+ }
+ }
+
+ /**
+ * Obtain the transport negotiator for this session.
+ *
+ * @return the transport negotiator instance
+ */
+ protected TransportNegotiator getTransportNeg() {
+ return transNeg;
+ }
+
+ /**
+ * @param transNeg the transNeg to set
+ */
+ protected void setTransportNeg(final TransportNegotiator transNeg) {
+ destroyTransportNeg();
+ this.transNeg = transNeg;
+ }
+
+ /**
+ * Destroy the transport negotiator.
+ */
+ protected void destroyTransportNeg() {
+ if (transNeg != null) {
+ transNeg.close();
+ transNeg = null;
+ }
+ }
+
+ /**
+ * Return true if the transport and content negotiators have finished
+ */
+ public boolean isFullyEstablished() {
+ if (!isValid()) {
+ return false;
+ }
+ if (!getTransportNeg().isFullyEstablished()
+ || !getMediaNeg().isFullyEstablished()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return true if the session is valid (<i>ie</i>, it has all the required
+ * elements initialized).
+ *
+ * @return true if the session is valid.
+ */
+ public boolean isValid() {
+ return mediaNeg != null && transNeg != null && sid != null && initiator != null;
+ }
+
+ /**
+ * Dispatch an incoming packet. The medthod is responsible for recognizing
+ * the packet type and, depending on the current state, deliverying the
+ * packet to the right event handler and wait for a response.
+ *
+ * @param iq the packet received
+ * @return the new Jingle packet to send.
+ * @throws XMPPException
+ */
+ public IQ dispatchIncomingPacket(final IQ iq, final String id) throws XMPPException {
+ IQ jout = null;
+
+ if (invalidState()) {
+ throw new IllegalStateException(
+ "Illegal state in dispatch packet in Session manager.");
+ } else {
+ if (iq == null) {
+ // If there is no input packet, then we must be inviting...
+ jout = getState().eventInvite();
+ } else {
+ if (iq.getType().equals(IQ.Type.ERROR)) {
+ // Process errors
+ getState().eventError(iq);
+ } else if (iq.getType().equals(IQ.Type.RESULT)) {
+ // Process ACKs
+ if (isExpectedId(iq.getPacketID())) {
+ jout = getState().eventAck(iq);
+ removeExpectedId(iq.getPacketID());
+ }
+ } else if (iq instanceof Jingle) {
+ // It is not an error: it is a Jingle packet...
+ Jingle jin = (Jingle) iq;
+ Jingle.Action action = jin.getAction();
+
+ if (action != null) {
+ if (action.equals(Jingle.Action.SESSIONACCEPT)) {
+ jout = getState().eventAccept(jin);
+ } else if (action.equals(Jingle.Action.SESSIONINFO)) {
+ jout = getState().eventInfo(jin);
+ } else if (action.equals(Jingle.Action.SESSIONINITIATE)) {
+ jout = getState().eventInitiate(jin);
+ } else if (action.equals(Jingle.Action.SESSIONREDIRECT)) {
+ jout = getState().eventRedirect(jin);
+ } else if (action.equals(Jingle.Action.SESSIONTERMINATE)) {
+ jout = getState().eventTerminate(jin);
+ }
+ } else {
+ jout = errorMalformedStanza(iq);
+ }
+ }
+ }
+
+ if (jout != null) {
+ // Save the packet id, for recognizing ACKs...
+ addExpectedId(jout.getPacketID());
+ }
+ }
+
+ return jout;
+ }
+
+ /**
+ * Process and respond to an incomming packet.
+ *
+ * This method is called from the packet listener dispatcher when a new
+ * packet has arrived. The medthod is responsible for recognizing the packet
+ * type and, depending on the current state, deliverying it to the right
+ * event handler and wait for a response. The response will be another
+ * Jingle packet that will be sent to the other endpoint.
+ *
+ * @param iq the packet received
+ * @return the new Jingle packet to send.
+ * @throws XMPPException
+ */
+ public synchronized IQ respond(final IQ iq) throws XMPPException {
+ IQ response = null;
+
+ if (isValid()) {
+ String responseId = null;
+ IQ sessionResponse = null;
+ IQ descriptionResponse = null;
+ IQ transportResponse = null;
+
+ // Send the packet to the right event handler for the session...
+ try {
+ sessionResponse = dispatchIncomingPacket(iq, null);
+ if (sessionResponse != null) {
+ responseId = sessionResponse.getPacketID();
+ }
+
+ // ... and do the same for the Description and Transport
+ // parts...
+ if (mediaNeg != null) {
+ descriptionResponse = mediaNeg.dispatchIncomingPacket(iq, responseId);
+ }
+
+ if (transNeg != null) {
+ transportResponse = transNeg.dispatchIncomingPacket(iq, responseId);
+ }
+
+ // Acknowledge the IQ reception
+ sendAck(iq);
+
+ // ... and send all these parts in a Jingle response.
+ response = sendJingleParts(iq, (Jingle) sessionResponse,
+ (Jingle) descriptionResponse, (Jingle) transportResponse);
+
+ } catch (JingleException e) {
+ // Send an error message, if present
+ JingleError error = e.getError();
+ if (error != null) {
+ sendFormattedError(iq, error);
+ }
+
+ // Notify the session end and close everything...
+ triggerSessionClosedOnError(e);
+ close();
+ }
+ }
+
+ return response;
+ }
+
+ // Packet formatting and delivery
+
+ /**
+ * Put together all the parts ina Jingle packet.
+ *
+ * @return the new Jingle packet
+ */
+ private Jingle sendJingleParts(final IQ iq, final Jingle jSes, final Jingle jDesc,
+ final Jingle jTrans) {
+ Jingle response = null;
+
+ if (jSes != null) {
+ jSes.addDescriptions(jDesc.getDescriptionsList());
+ jSes.addTransports(jTrans.getTransportsList());
+
+ response = sendFormattedJingle(iq, jSes);
+ } else {
+ // If we don't have a valid session message, then we must send
+ // separated messages for transport and media...
+ if (jDesc != null) {
+ response = sendFormattedJingle(iq, jDesc);
+ }
+
+ if (jTrans != null) {
+ response = sendFormattedJingle(iq, jTrans);
+ }
+ }
+
+ return response;
+ }
+
+ /**
+ * Complete and send an error. Complete all the null fields in an IQ error
+ * reponse, using the sesssion information we have or some info from the
+ * incoming packet.
+ *
+ * @param jin The Jingle packet we are responing to
+ * @param pout the IQ packet we want to complete and send
+ */
+ protected IQ sendFormattedError(final IQ iq, final JingleError error) {
+ IQ perror = null;
+ if (error != null) {
+ perror = createIQ(getSid(), iq.getFrom(), iq.getTo(), IQ.Type.ERROR);
+
+ // Fill in the fields with the info from the Jingle packet
+ perror.setPacketID(iq.getPacketID());
+ perror.addExtension(error);
+
+ getConnection().sendPacket(perror);
+ }
+ return perror;
+ }
+
+ /**
+ * Complete and send a packet. Complete all the null fields in a Jingle
+ * reponse, using the session information we have or some info from the
+ * incoming packet.
+ *
+ * @param jin The Jingle packet we are responing to
+ * @param jout the Jingle packet we want to complete and send
+ */
+ protected Jingle sendFormattedJingle(final IQ iq, final Jingle jout) {
+ if (jout != null) {
+ if (jout.getInitiator() == null) {
+ jout.setInitiator(getInitiator());
+ }
+
+ if (jout.getResponder() == null) {
+ jout.setResponder(getResponder());
+ }
+
+ if (jout.getSid() == null) {
+ jout.setSid(getSid());
+ }
+
+ String me = getConnection().getUser();
+ String other = getResponder().equals(me) ? getInitiator() : getResponder();
+
+ if (jout.getTo() == null) {
+ if (iq != null) {
+ jout.setTo(iq.getFrom());
+ } else {
+ jout.setTo(other);
+ }
+ }
+
+ if (jout.getFrom() == null) {
+ if (iq != null) {
+ jout.setFrom(iq.getTo());
+ } else {
+ jout.setFrom(me);
+ }
+ }
+
+ getConnection().sendPacket(jout);
+ }
+ return jout;
+ }
+
+ /**
+ * Complete and send a packet. Complete all the null fields in a Jingle
+ * reponse, using the session information we have.
+ *
+ * @param jout the Jingle packet we want to complete and send
+ */
+ protected Jingle sendFormattedJingle(final Jingle jout) {
+ return sendFormattedJingle(null, jout);
+ }
+
+ /**
+ * Send an error indicating that the stanza is malformed.
+ *
+ * @param iq
+ */
+ protected IQ errorMalformedStanza(final IQ iq) {
+ // FIXME: implement with the right message...
+ return createError(iq.getPacketID(), iq.getFrom(), getConnection().getUser(),
+ 400, "Bad Request");
+ }
+
+ /**
+ * Check if we have an established session and, in that case, send an Accept
+ * packet.
+ */
+ protected Jingle sendAcceptIfFullyEstablished() {
+ Jingle result = null;
+ if (isFullyEstablished()) {
+ // Ok, send a packet saying that we accept this session
+ Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT);
+ jout.setType(IQ.Type.SET);
+
+ result = sendFormattedJingle(jout);
+ }
+ return result;
+ }
+
+ /**
+ * Acknowledge a IQ packet.
+ *
+ * @param iq The IQ to acknowledge
+ */
+ private IQ sendAck(final IQ iq) {
+ IQ result = null;
+
+ if (iq != null) {
+ // Don't acknowledge ACKs, errors...
+ if (iq.getType().equals(IQ.Type.SET)) {
+ IQ ack = createIQ(iq.getPacketID(), iq.getFrom(), iq.getTo(),
+ IQ.Type.RESULT);
+
+ getConnection().sendPacket(ack);
+ result = ack;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Send a content info message.
+ */
+ public synchronized void sendContentInfo(final ContentInfo ci) {
+ if (isValid()) {
+ sendFormattedJingle(new Jingle(new JingleContentInfo(ci)));
+ }
+ }
+
+ /**
+ * Get the content description the other part has accepted.
+ *
+ * @param jin The Jingle packet where they have accepted the session.
+ * @return The audio PayloadType they have accepted.
+ * @throws XMPPException
+ */
+ protected PayloadType.Audio getAcceptedAudioPayloadType(final Jingle jin)
+ throws XMPPException {
+ PayloadType.Audio acceptedPayloadType = null;
+ ArrayList jda = jin.getDescriptionsList();
+
+ if (jin.getAction().equals(Jingle.Action.SESSIONACCEPT)) {
+
+ if (jda.size() > 1) {
+ throw new XMPPException(
+ "Unsupported feature: the number of accepted content descriptions is greater than 1.");
+ } else if (jda.size() == 1) {
+ JingleContentDescription jd = (JingleContentDescription) jda.get(0);
+ if (jd.getJinglePayloadTypesCount() > 1) {
+ throw new XMPPException(
+ "Unsupported feature: the number of accepted payload types is greater than 1.");
+ }
+ if (jd.getJinglePayloadTypesCount() == 1) {
+ JinglePayloadType jpt = (JinglePayloadType) jd
+ .getJinglePayloadTypesList().get(0);
+ acceptedPayloadType = (PayloadType.Audio) jpt.getPayloadType();
+ }
+ }
+ }
+ return acceptedPayloadType;
+ }
+
+ /**
+ * Get the accepted local candidate we have previously offered.
+ *
+ * @param jin The jingle packet where they accept the session
+ * @return The transport candidate they have accepted.
+ * @throws XMPPException
+ */
+ protected TransportCandidate getAcceptedLocalCandidate(final Jingle jin)
+ throws XMPPException {
+ ArrayList jta = jin.getTransportsList();
+ TransportCandidate acceptedLocalCandidate = null;
+
+ if (jin.getAction().equals(Jingle.Action.SESSIONACCEPT)) {
+ if (jta.size() > 1) {
+ throw new XMPPException(
+ "Unsupported feature: the number of accepted transports is greater than 1.");
+ } else if (jta.size() == 1) {
+ JingleTransport jt = (JingleTransport) jta.get(0);
+
+ if (jt.getCandidatesCount() > 1) {
+ throw new XMPPException(
+ "Unsupported feature: the number of accepted transport candidates is greater than 1.");
+ } else if (jt.getCandidatesCount() == 1) {
+ JingleTransportCandidate jtc = (JingleTransportCandidate) jt
+ .getCandidatesList().get(0);
+ acceptedLocalCandidate = jtc.getMediaTransport();
+ }
+ }
+ }
+
+ return acceptedLocalCandidate;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ public int hashCode() {
+ return Jingle.getSessionHash(getSid(), getInitiator());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final JingleSession other = (JingleSession) obj;
+
+ if (initiator == null) {
+ if (other.initiator != null) {
+ return false;
+ }
+ } else if (!initiator.equals(other.initiator)) {
+ return false;
+ }
+
+ if (responder == null) {
+ if (other.responder != null) {
+ return false;
+ }
+ } else if (!responder.equals(other.responder)) {
+ return false;
+ }
+
+ if (sid == null) {
+ if (other.sid != null) {
+ return false;
+ }
+ } else if (!sid.equals(other.sid)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Instances management
+
+ /**
+ * Clean a session from the list.
+ *
+ * @param connection The connection to clean up
+ */
+ private void unregisterInstanceFor(final XMPPConnection connection) {
+ synchronized (sessions) {
+ sessions.remove(connection);
+ }
+ }
+
+ /**
+ * Register this instance.
+ */
+ private void registerInstance() {
+ synchronized (sessions) {
+ sessions.put(getConnection(), this);
+ }
+ }
+
+ /**
+ * Returns the JingleSession related to a particular connection.
+ *
+ * @param con A XMPP connection
+ * @return a Jingle session
+ */
+ public static JingleSession getInstanceFor(final XMPPConnection con) {
+ if (con == null) {
+ throw new IllegalArgumentException("Connection cannot be null");
+ }
+
+ JingleSession result = null;
+ synchronized (sessions) {
+ if (sessions.containsKey(con)) {
+ result = (JingleSession) sessions.get(con);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Configure a session, setting some action listeners...
+ *
+ * @param session The connection to set up
+ */
+ private void installConnectionListeners(final XMPPConnection connection) {
+ if (connection != null) {
+ connection.addConnectionListener(new ConnectionListener() {
+ public void connectionClosed() {
+ unregisterInstanceFor(connection);
+ }
+
+ public void connectionClosedOnError(final java.lang.Exception e) {
+ unregisterInstanceFor(connection);
+ }
+ });
+ }
+ }
+
+ /**
+ * Remove the packet listener used for processing packet.
+ */
+ protected void removePacketListener() {
+ if (packetListener != null) {
+ getConnection().removePacketListener(packetListener);
+ }
+ }
+
+ /**
+ * Install the packet listener. The listener is responsible for responding
+ * to any packet that we receive...
+ */
+ protected void updatePacketListener() {
+ removePacketListener();
+
+ packetListener = new PacketListener() {
+ public void processPacket(final Packet packet) {
+ try {
+ respond((IQ) packet);
+ } catch (XMPPException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ packetFilter = new PacketFilter() {
+ public boolean accept(final Packet packet) {
+ if (packet instanceof IQ) {
+ IQ iq = (IQ) packet;
+
+ String me = getConnection().getUser();
+
+ if (!iq.getTo().equals(me)) {
+ return false;
+ }
+
+ String other = getResponder().equals(me) ? getInitiator()
+ : getResponder();
+
+ if (!iq.getFrom().equals(other)) {
+ return false;
+ }
+
+ if (iq instanceof Jingle) {
+ Jingle jin = (Jingle) iq;
+
+ String sid = jin.getSid();
+ if (!sid.equals(getSid())) {
+ return false;
+ }
+ String ini = jin.getInitiator();
+ if (!ini.equals(getInitiator())) {
+ return false;
+ }
+ } else {
+ // We accept some non-Jingle IQ packets: ERRORs and ACKs
+ if (iq.getType().equals(IQ.Type.SET)) {
+ return false;
+ } else if (iq.getType().equals(IQ.Type.GET)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+
+ getConnection().addPacketListener(packetListener, packetFilter);
+ }
+
+ // Listeners
+
+ /**
+ * Add a listener for media negotiation events
+ *
+ * @param li The listener
+ */
+ public void addMediaListener(final JingleListener.Media li) {
+ if (getMediaNeg() != null) {
+ getMediaNeg().addListener(li);
+ }
+ }
+
+ /**
+ * Remove a listener for media negotiation events
+ *
+ * @param li The listener
+ */
+ public void removeMediaListener(final JingleListener.Media li) {
+ if (getMediaNeg() != null) {
+ getMediaNeg().removeListener(li);
+ }
+ }
+
+ /**
+ * Add a listener for transport negotiation events
+ *
+ * @param li The listener
+ */
+ public void addTransportListener(final JingleListener.Transport li) {
+ if (getTransportNeg() != null) {
+ getTransportNeg().addListener(li);
+ }
+ }
+
+ /**
+ * Remove a listener for transport negotiation events
+ *
+ * @param li The listener
+ */
+ public void removeTransportListener(final JingleListener.Transport li) {
+ if (getTransportNeg() != null) {
+ getTransportNeg().removeListener(li);
+ }
+ }
+
+ // Triggers
+
+ /**
+ * Trigger a session closed event.
+ */
+ protected void triggerSessionClosed(final String reason) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Session) {
+ JingleListener.Session sli = (JingleListener.Session) li;
+ sli.sessionClosed(reason);
+ }
+ }
+ }
+
+ /**
+ * Trigger a session closed event due to an error.
+ */
+ protected void triggerSessionClosedOnError(final XMPPException exc) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Session) {
+ JingleListener.Session sli = (JingleListener.Session) li;
+ sli.sessionClosedOnError(exc);
+ }
+ }
+ }
+
+ /**
+ * Trigger a session established event.
+ */
+ protected void triggerSessionEstablished(final PayloadType pt,
+ final TransportCandidate rc, final TransportCandidate lc) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Session) {
+ JingleListener.Session sli = (JingleListener.Session) li;
+ sli.sessionEstablished(pt, rc, lc);
+ }
+ }
+ }
+
+ /**
+ * Trigger a session redirect event.
+ */
+ protected void triggerSessionRedirect(final String arg) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Session) {
+ JingleListener.Session sli = (JingleListener.Session) li;
+ sli.sessionRedirected(arg);
+ }
+ }
+ }
+
+ /**
+ * Trigger a session redirect event.
+ */
+ protected void triggerSessionDeclined(final String reason) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Session) {
+ JingleListener.Session sli = (JingleListener.Session) li;
+ sli.sessionDeclined(reason);
+ }
+ }
+ }
+
+ /**
+ * Start the negotiation.
+ *
+ * @throws JingleException
+ * @throws XMPPException
+ */
+ public abstract void start(final JingleSessionRequest jin) throws XMPPException;
+
+ /**
+ * Terminate negotiations.
+ */
+ public void close() {
+ destroyMediaNeg();
+ destroyTransportNeg();
+
+ removePacketListener();
+
+ super.close();
+ }
+
+ // Packet and error creation
+
+ /**
+ * A convience method to create an IQ packet.
+ *
+ * @param ID The packet ID of the
+ * @param to To whom the packet is addressed.
+ * @param from From whom the packet is sent.
+ * @param type The iq type of the packet.
+ * @return The created IQ packet.
+ */
+ public static IQ createIQ(final String ID, final String to, final String from,
+ final IQ.Type type) {
+ IQ iqPacket = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+
+ iqPacket.setPacketID(ID);
+ iqPacket.setTo(to);
+ iqPacket.setFrom(from);
+ iqPacket.setType(type);
+
+ return iqPacket;
+ }
+
+ /**
+ * A convience method to create an error packet.
+ *
+ * @param ID The packet ID of the
+ * @param to To whom the packet is addressed.
+ * @param from From whom the packet is sent.
+ * @param errCode The error code.
+ * @param errStr The error string.
+ *
+ * @return The created IQ packet.
+ */
+ public static IQ createError(final String ID, final String to, final String from,
+ final int errCode, final String errStr) {
+
+ IQ iqError = createIQ(ID, to, from, IQ.Type.ERROR);
+ XMPPError error = new XMPPError(errCode, errStr);
+ iqError.setError(error);
+
+ return iqError;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java
new file mode 100644
index 000000000..53edbf667
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionRequest.java
@@ -0,0 +1,91 @@
+package org.jivesoftware.smackx.jingle;
+
+import java.util.List;
+
+import org.jivesoftware.smackx.packet.Jingle;
+
+/**
+ * A Jingle session request.
+ *
+ * </p>
+ *
+ * This class is a facade of a received Jingle request. The user can have direct
+ * access to the Jingle packet (<i>JingleSessionRequest.getJingle() </i>) of
+ * the request or can use the convencience methods provided by this class.
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin
+ */
+public class JingleSessionRequest {
+
+ private final Jingle jingle; // The Jingle packet
+
+ private final JingleManager manager; // The manager associated to this
+
+ // request
+
+ /**
+ * A recieve request is constructed from the Jingle Initiation request
+ * received from the initator.
+ *
+ * @param manager The manager handling this request
+ * @param jingle The jingle IQ recieved from the initiator.
+ */
+ public JingleSessionRequest(final JingleManager manager, final Jingle jingle) {
+ this.manager = manager;
+ this.jingle = jingle;
+ }
+
+ /**
+ * Returns the fully-qualified jabber ID of the user that requested this
+ * session.
+ *
+ * @return Returns the fully-qualified jabber ID of the user that requested
+ * this session.
+ */
+ public String getFrom() {
+ return jingle.getFrom();
+ }
+
+ /**
+ * Returns the session ID that uniquely identifies this session.
+ *
+ * @return Returns the session ID that uniquely identifies this session
+ */
+ public String getSessionID() {
+ return jingle.getSid();
+ }
+
+ /**
+ * Returns the Jingle packet that was sent by the requestor which contains
+ * the parameters of the session.
+ */
+ protected Jingle getJingle() {
+ return jingle;
+ }
+
+ /**
+ * Accepts this request and creates the incoming Jingle session.
+ *
+ * @return Returns the <b><i>IncomingJingleSession</b></i> on which the
+ * negotiation can be carried out.
+ */
+ public synchronized IncomingJingleSession accept(final List pts) {
+ IncomingJingleSession session = null;
+ synchronized (manager) {
+ session = manager.createIncomingJingleSession(this,
+ pts);
+ }
+ return session;
+ }
+
+ /**
+ * Rejects the session request.
+ */
+ public synchronized void reject() {
+ synchronized (manager) {
+ manager.rejectIncomingJingleSession(this);
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java
new file mode 100644
index 000000000..6ce8add71
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/JingleSessionTest.java
@@ -0,0 +1,43 @@
+package org.jivesoftware.smackx.jingle;
+
+import org.jivesoftware.smack.test.SmackTestCase;
+
+public class JingleSessionTest extends SmackTestCase {
+
+ public JingleSessionTest(final String name) {
+ super(name);
+ }
+
+ public void testEqualsObject() {
+ JingleSession js1 = new OutgoingJingleSession(getConnection(0), "res1", null, null);
+ JingleSession js2 = new OutgoingJingleSession(getConnection(1), "res1", null, null);
+ JingleSession js3 = new OutgoingJingleSession(getConnection(2), "res2", null, null);
+
+ assertEquals(js1, js2);
+ assertEquals(js2, js1);
+
+ assertFalse(js1.equals(js3));
+ }
+
+ public void testGetInstanceFor() {
+ String ini1 = "initiator1";
+ String sid1 = "sid1";
+ String ini2 = "initiator2";
+ String sid2 = "sid2";
+
+ JingleSession js1 = new OutgoingJingleSession(getConnection(0), sid1, null, null);
+ JingleSession js2 = new OutgoingJingleSession(getConnection(1), sid2, null, null);
+
+ // For a packet, we should be able to get a session that handles that...
+ assertNotNull(JingleSession.getInstanceFor(getConnection(0)));
+ assertNotNull(JingleSession.getInstanceFor(getConnection(1)));
+
+ assertEquals(JingleSession.getInstanceFor(getConnection(0)), js1);
+ assertEquals(JingleSession.getInstanceFor(getConnection(1)), js2);
+ }
+
+ protected int getMaxConnections() {
+ return 3;
+ }
+}
+
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java
new file mode 100644
index 000000000..5cd2354b8
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/MediaNegotiator.java
@@ -0,0 +1,553 @@
+package org.jivesoftware.smackx.jingle;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.Jingle;
+import org.jivesoftware.smackx.packet.JingleContentDescription;
+import org.jivesoftware.smackx.packet.JingleError;
+import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType;
+
+/**
+ * Manager for media descriptor negotiation.
+ *
+ * </p>
+ *
+ * This class is responsible for managing the descriptor negotiation process,
+ * handling all the packet interchange and the stage control.
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin
+ */
+public class MediaNegotiator extends JingleNegotiator {
+
+ private final JingleSession session; // The session this negotiation
+
+ // Local and remote payload types...
+
+ private final List localAudioPts = new ArrayList();
+
+ private final List remoteAudioPts = new ArrayList();
+
+ private PayloadType.Audio bestCommonAudioPt;
+
+ // states
+
+ private final Inviting inviting;
+
+ private final Accepting accepting;
+
+ private final Pending pending;
+
+ private final Active active;
+
+ /**
+ * Default constructor. The constructor establishes some basic parameters,
+ * but it does not start the negotiation. For starting the negotiation, call
+ * startNegotiation.
+ *
+ * @param js The jingle session.
+ */
+ public MediaNegotiator(final JingleSession js, final List pts) {
+ super(js.getConnection());
+
+ session = js;
+
+ bestCommonAudioPt = null;
+
+ if (pts != null) {
+ if (pts.size() > 0) {
+ localAudioPts.addAll(pts);
+ }
+ }
+
+ // Create the states...
+ inviting = new Inviting(this);
+ accepting = new Accepting(this);
+ pending = new Pending(this);
+ active = new Active(this);
+ }
+
+ /**
+ * Dispatch an incomming packet. The medthod is responsible for recognizing
+ * the packet type and, depending on the current state, deliverying the
+ * packet to the right event handler and wait for a response.
+ *
+ * @param iq the packet received
+ * @return the new Jingle packet to send.
+ * @throws XMPPException
+ */
+ public IQ dispatchIncomingPacket(final IQ iq, final String id) throws XMPPException {
+ IQ jout = null;
+
+ if (invalidState()) {
+ if (iq == null) {
+ // With a null packet, we are just inviting the other end...
+ setState(inviting);
+ jout = getState().eventInvite();
+ } else {
+ if (iq instanceof Jingle) {
+ // If there is no specific media action associated, then we
+ // are being invited to a new session...
+ setState(accepting);
+ jout = getState().eventInitiate((Jingle) iq);
+ } else {
+ throw new IllegalStateException(
+ "Invitation IQ received is not a Jingle packet in Media negotiator.");
+ }
+ }
+ } else {
+ if (iq == null) {
+ return null;
+ } else {
+ if (iq.getType().equals(IQ.Type.ERROR)) {
+ // Process errors
+ getState().eventError(iq);
+ } else if (iq.getType().equals(IQ.Type.RESULT)) {
+ // Process ACKs
+ if (isExpectedId(iq.getPacketID())) {
+ jout = getState().eventAck(iq);
+ removeExpectedId(iq.getPacketID());
+ }
+ } else if (iq instanceof Jingle) {
+ // Get the action from the Jingle packet
+ Jingle jin = (Jingle) iq;
+ Jingle.Action action = jin.getAction();
+
+ if (action != null) {
+ if (action.equals(Jingle.Action.CONTENTACCEPT)) {
+ jout = getState().eventAccept(jin);
+ } else if (action.equals(Jingle.Action.CONTENTDECLINE)) {
+ jout = getState().eventDecline(jin);
+ } else if (action.equals(Jingle.Action.CONTENTINFO)) {
+ jout = getState().eventInfo(jin);
+ } else if (action.equals(Jingle.Action.CONTENTMODIFY)) {
+ jout = getState().eventModify(jin);
+ }
+ // Any unknown action will be ignored: it is not a msg
+ // to us...
+ }
+ }
+ }
+ }
+
+ // Save the Id for any ACK
+ if (id != null) {
+ addExpectedId(id);
+ } else {
+ if (jout != null) {
+ addExpectedId(jout.getPacketID());
+ }
+ }
+
+ return jout;
+ }
+
+ /**
+ * Return true if the content is negotiated.
+ *
+ * @return true if the content is negotiated.
+ */
+ public boolean isEstablished() {
+ return getBestCommonAudioPt() != null;
+ }
+
+ /**
+ * Return true if the content is fully negotiated.
+ *
+ * @return true if the content is fully negotiated.
+ */
+ public boolean isFullyEstablished() {
+ return isEstablished() && getState() == active;
+ }
+
+ // Payload types
+
+ private PayloadType.Audio calculateBestCommonAudioPt(final List remoteAudioPts) {
+ final ArrayList commonAudioPtsHere = new ArrayList();
+ final ArrayList commonAudioPtsThere = new ArrayList();
+ PayloadType.Audio result = null;
+
+ if (!remoteAudioPts.isEmpty()) {
+ commonAudioPtsHere.addAll(localAudioPts);
+ commonAudioPtsHere.retainAll(remoteAudioPts);
+
+ commonAudioPtsThere.addAll(remoteAudioPts);
+ commonAudioPtsThere.retainAll(localAudioPts);
+
+ if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) {
+ PayloadType.Audio bestPtHere = (PayloadType.Audio) commonAudioPtsHere
+ .get(0);
+ PayloadType.Audio bestPtThere = (PayloadType.Audio) commonAudioPtsThere
+ .get(0);
+
+ // If both match, use it
+ if (bestPtHere.equals(bestPtThere)) {
+ result = bestPtHere;
+ } else {
+ // Otherwise, use the one of the initiator...
+ // FIXME: this is an invented behavior!!!
+ String initiator = session.getInitiator();
+ String me = session.getConnection().getUser();
+
+ if (initiator.equals(me)) {
+ result = bestPtHere;
+ } else {
+ result = bestPtThere;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private List obtainPayloads(final Jingle jin) {
+ List result = new ArrayList();
+ Iterator iDescr = jin.getDescriptions();
+
+ // Add the list of payloads: iterate over the descriptions...
+ while (iDescr.hasNext()) {
+ JingleContentDescription.Audio descr = (JingleContentDescription.Audio) iDescr
+ .next();
+
+ if (descr != null) {
+ // ...and, then, over the payloads.
+ // Note: we use the last "description" in the packet...
+ result.clear();
+ result.addAll(descr.getAudioPayloadTypesList());
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Adds a payload type to the list of remote payloads.
+ *
+ * @param pt the remote payload type
+ */
+ public void addRemoteAudioPayloadType(final PayloadType.Audio pt) {
+ if (pt != null) {
+ synchronized (remoteAudioPts) {
+ remoteAudioPts.add(pt);
+ }
+ }
+ }
+
+ /**
+ * Create an offer for the list of audio payload types.
+ *
+ * @return a new Jingle packet with the list of audio Payload Types
+ */
+ private Jingle getAudioPayloadTypesOffer() {
+ JingleContentDescription.Audio audioDescr = new JingleContentDescription.Audio();
+
+ // Add the list of payloads for audio and create a
+ // JingleContentDescription
+ // where we announce our payloads...
+ audioDescr.addAudioPayloadTypes(localAudioPts);
+
+ return new Jingle(audioDescr);
+ }
+
+ // Predefined messages and Errors
+
+ /**
+ * Create an IQ "accept" message.
+ */
+ private Jingle createAcceptMessage() {
+ Jingle jout = null;
+
+ // If we hava a common best codec, send an accept right now...
+ jout = new Jingle(Jingle.Action.CONTENTACCEPT);
+ jout.addDescription(new JingleContentDescription.Audio(
+ new JinglePayloadType.Audio(bestCommonAudioPt)));
+
+ return jout;
+ }
+
+ // Payloads
+
+ /**
+ * Get the best common codec between both parts.
+ *
+ * @return The best common PayloadType codec.
+ */
+ public PayloadType.Audio getBestCommonAudioPt() {
+ return bestCommonAudioPt;
+ }
+
+ // Events
+
+ /**
+ * Trigger a session established event.
+ *
+ * @param best payload type that has been agreed.
+ */
+ protected void triggerMediaEstablished(final PayloadType bestPt) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Media) {
+ JingleListener.Media mli = (JingleListener.Media) li;
+ mli.mediaEstablished(bestPt);
+ }
+ }
+ }
+
+ /**
+ * Trigger a media closed event.
+ *
+ * @param currPt current payload type that is cancelled.
+ */
+ protected void triggerMediaClosed(final PayloadType currPt) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Media) {
+ JingleListener.Media mli = (JingleListener.Media) li;
+ mli.mediaClosed(currPt);
+ }
+ }
+ }
+
+ /**
+ * Terminate the media negotiator
+ */
+ public void close() {
+ super.close();
+ }
+
+ // States
+
+ /**
+ * First stage when we send a session request.
+ */
+ public class Inviting extends JingleNegotiator.State {
+ public Inviting(final MediaNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * Create an initial Jingle packet, with the list of payload types that
+ * we support. The list is in order of preference.
+ */
+ public Jingle eventInvite() {
+ return getAudioPayloadTypesOffer();
+ }
+
+ /**
+ * We have received the ACK for our invitation.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
+ */
+ public Jingle eventAck(final IQ iq) {
+ setState(pending);
+ return null;
+ }
+ }
+
+ /**
+ * We are accepting connections.
+ */
+ public class Accepting extends JingleNegotiator.State {
+
+ public Accepting(final MediaNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * We have received an invitation! Respond with a list of our payload
+ * types...
+ */
+ public Jingle eventInitiate(final Jingle jin) {
+ synchronized (remoteAudioPts) {
+ remoteAudioPts.addAll(obtainPayloads(jin));
+ }
+
+ return getAudioPayloadTypesOffer();
+ }
+
+ /**
+ * Process the ACK of our list of codecs (our offer).
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
+ */
+ public Jingle eventAck(final IQ iq) throws XMPPException {
+ Jingle response = null;
+
+ if (!remoteAudioPts.isEmpty()) {
+ // Calculate the best common codec
+ bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
+
+ // and send an accept if we havee an agreement...
+ if (bestCommonAudioPt != null) {
+ response = createAcceptMessage();
+ } else {
+ throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
+ }
+
+ setState(pending);
+ }
+
+ return response;
+ }
+ }
+
+ /**
+ * Pending class: we are waiting for the other enpoint, that must say if it
+ * accepts or not...
+ */
+ public class Pending extends JingleNegotiator.State {
+ public Pending(final MediaNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * A content info has been received. This is done for publishing the
+ * list of payload types...
+ *
+ * @param jin The input packet
+ * @return a Jingle packet
+ * @throws JingleException
+ */
+ public Jingle eventInfo(final Jingle jin) throws JingleException {
+ PayloadType.Audio oldBestCommonAudioPt = bestCommonAudioPt;
+ List offeredPayloads = new ArrayList();
+ Jingle response = null;
+ boolean ptChange = false;
+
+ offeredPayloads = obtainPayloads(jin);
+ if (!offeredPayloads.isEmpty()) {
+
+ synchronized (remoteAudioPts) {
+ remoteAudioPts.clear();
+ remoteAudioPts.addAll(offeredPayloads);
+ }
+
+ // Calculate the best common codec
+ bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
+ if (bestCommonAudioPt != null) {
+ // and send an accept if we have an agreement...
+ ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt);
+ if (oldBestCommonAudioPt == null || ptChange) {
+ response = createAcceptMessage();
+ }
+ } else {
+ throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
+ }
+ }
+
+ // Parse the Jingle and get the payload accepted
+ return response;
+ }
+
+ /**
+ * A media description has been accepted. In this case, we must save the
+ * accepted payload type and notify any listener...
+ *
+ * @param jin The input packet
+ * @return a Jingle packet
+ * @throws JingleException
+ */
+ public Jingle eventAccept(final Jingle jin) throws JingleException {
+ PayloadType.Audio agreedCommonAudioPt;
+ List offeredPayloads = new ArrayList();
+ Jingle response = null;
+
+ if (bestCommonAudioPt == null) {
+ // Update the best common audio PT
+ bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
+ response = createAcceptMessage();
+ }
+
+ offeredPayloads = obtainPayloads(jin);
+ if (!offeredPayloads.isEmpty()) {
+ if (offeredPayloads.size() == 1) {
+ agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0);
+ if (bestCommonAudioPt != null) {
+ // If the accepted PT matches the best payload
+ // everything is fine
+ if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) {
+ throw new JingleException(JingleError.NEGOTIATION_ERROR);
+ }
+ }
+
+ } else if (offeredPayloads.size() > 1) {
+ throw new JingleException(JingleError.MALFORMED_STANZA);
+ }
+ }
+
+ return response;
+ }
+
+ /**
+ * The other part has declined the our codec...
+ *
+ * @throws JingleException
+ */
+ public Jingle eventDecline(final Jingle inJingle) throws JingleException {
+ triggerMediaClosed(getBestCommonAudioPt());
+ throw new JingleException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ)
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerMediaClosed(getBestCommonAudioPt());
+ super.eventError(iq);
+ }
+
+ /**
+ * ACK received.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
+ */
+ public Jingle eventAck(final IQ iq) {
+ if (isEstablished()) {
+ setState(active);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * "Active" state: we have an agreement about the codec...
+ */
+ public class Active extends JingleNegotiator.State {
+
+ public Active(final MediaNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * We have an agreement.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventEnter() {
+ triggerMediaEstablished(getBestCommonAudioPt());
+ super.eventEnter();
+ }
+
+ /**
+ * We are breaking the contract...
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit()
+ */
+ public void eventExit() {
+ triggerMediaClosed(getBestCommonAudioPt());
+ super.eventExit();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java
new file mode 100644
index 000000000..2589f1d7b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java
@@ -0,0 +1,405 @@
+/**
+ * $RCSfile: OutgoingJingleSession.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2006/10/17 19:12:42 $
+ *
+ * Copyright (C) 2002-2006 Jive Software. All rights reserved.
+ * ====================================================================
+ * The Jive Software License (based on Apache Software License, Version 1.1)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The end-user documentation included with the redistribution,
+ * if any, must include the following acknowledgment:
+ * "This product includes software developed by
+ * Jive Software (http://www.jivesoftware.com)."
+ * Alternately, this acknowledgment may appear in the software itself,
+ * if and wherever such third-party acknowledgments normally appear.
+ *
+ * 4. The names "Smack" and "Jive Software" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For written permission, please
+ * contact webmaster@jivesoftware.com.
+ *
+ * 5. Products derived from this software may not be called "Smack",
+ * nor may "Smack" appear in their name, without prior written
+ * permission of Jive Software.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ */
+
+package org.jivesoftware.smackx.jingle;
+
+import java.util.List;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.nat.TransportCandidate;
+import org.jivesoftware.smackx.nat.TransportResolver;
+import org.jivesoftware.smackx.packet.Jingle;
+import org.jivesoftware.smackx.packet.JingleContentDescription;
+import org.jivesoftware.smackx.packet.JingleError;
+import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType;
+
+/**
+ * An outgoing Jingle session.
+ *
+ * </p>
+ *
+ * This class is not directly used by users. Instead, users should refer to the
+ * JingleManager class, that will create the appropiate instance...
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+public class OutgoingJingleSession extends JingleSession {
+
+ // states
+
+ private final Inviting inviting;
+
+ private final Pending pending;
+
+ private final Active active;
+
+ /**
+ * Constructor for a Jingle outgoing session.
+ *
+ * @param conn the XMPP connection
+ * @param responder the other endpoint
+ * @param payloadTypes A list of payload types, in order of preference.
+ * @param resolver The transport resolver.
+ */
+ public OutgoingJingleSession(final XMPPConnection conn, final String responder,
+ final List payloadTypes, final TransportResolver resolver) {
+
+ super(conn, conn.getUser(), responder);
+
+ setSid(generateSessionId());
+
+ // Initialize the states.
+ inviting = new Inviting(this);
+ pending = new Pending(this);
+ active = new Active(this);
+
+ // Create description and transport negotiatiors...
+ setMediaNeg(new MediaNegotiator(this, payloadTypes));
+ setTransportNeg(new TransportNegotiator.RawUdp(this, resolver));
+ }
+
+ /**
+ * Initiate the negotiation with an invitation. This method must be invoked
+ * for starting all negotiations. It is the initial starting point and,
+ * afterwards, any other packet processing is done with the packet listener
+ * callback...
+ *
+ * @throws IllegalStateException
+ */
+ public void start(final JingleSessionRequest req) throws IllegalStateException {
+ if (invalidState()) {
+ setState(inviting);
+
+ // Use the standard behavior, using a null Jingle packet
+ try {
+ updatePacketListener();
+ respond((Jingle) null);
+ } catch (XMPPException e) {
+ e.printStackTrace();
+ close();
+ }
+ } else {
+ throw new IllegalStateException("Starting session without null state.");
+ }
+ }
+
+ // States
+
+ /**
+ * Current state when we want to invite the other endpoint.
+ */
+ public class Inviting extends JingleNegotiator.State {
+
+ public Inviting(final JingleNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * Create an invitation packet.
+ */
+ public Jingle eventInvite() {
+ // Create an invitation packet, saving the Packet ID, for any ACK
+ return new Jingle(Jingle.Action.SESSIONINITIATE);
+ }
+
+ /**
+ * The receiver has partially accepted our invitation. We go to the
+ * pending state while the content and transport negotiators work...
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
+ */
+ public Jingle eventAck(final IQ iq) {
+ setState(pending);
+ return null;
+ }
+
+ /**
+ * The other endpoint has declined the invitation with an error.
+ *
+ * @throws XMPPException
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ)
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerSessionDeclined(null);
+ super.eventError(iq);
+ }
+
+ /**
+ * The other endpoint wants to redirect this connection.
+ */
+ public Jingle eventRedirect(final Jingle jin) {
+ String redirArg = null;
+
+ // TODO: parse the redirection parameters...
+
+ triggerSessionRedirect(redirArg);
+ return null;
+ }
+ }
+
+ /**
+ * "Pending" state: we are waiting for the transport and content
+ * negotiators.
+ *
+ * Note: the transition from/to this state is done with listeners...
+ */
+ public class Pending extends JingleNegotiator.State {
+ JingleListener.Media mediaListener;
+
+ JingleListener.Transport transportListener;
+
+ public Pending(final JingleNegotiator neg) {
+ super(neg);
+
+ // Create the listeners that will send a "session-accept" when
+ // the sub-negotiators are done.
+ mediaListener = new JingleListener.Media() {
+ public void mediaClosed(final PayloadType cand) {
+ }
+
+ public void mediaEstablished(final PayloadType pt) {
+ checkFullyEstablished();
+ }
+ };
+
+ transportListener = new JingleListener.Transport() {
+ public void transportEstablished(final TransportCandidate local,
+ final TransportCandidate remote) {
+ checkFullyEstablished();
+ }
+
+ public void transportClosed(final TransportCandidate cand) {
+ }
+
+ public void transportClosedOnError(final XMPPException e) {
+ }
+ };
+ }
+
+ /**
+ * Enter in the pending state: install the listeners.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventEnter() {
+ // Add the listeners to the sub-negotiators...
+ addMediaListener(mediaListener);
+ addTransportListener(transportListener);
+ }
+
+ /**
+ * Exit of the state: remove the listeners.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit()
+ */
+ public void eventExit() {
+ removeMediaListener(mediaListener);
+ removeTransportListener(transportListener);
+ }
+
+ /**
+ * Check if the session has been fully accepted by all the
+ * sub-negotiators and, in that case, send an "accept" message...
+ */
+ private void checkFullyEstablished() {
+
+ if (isFullyEstablished()) {
+
+ PayloadType.Audio bestCommonAudioPt = getMediaNeg()
+ .getBestCommonAudioPt();
+ TransportCandidate bestRemoteCandidate = getTransportNeg()
+ .getBestRemoteCandidate();
+
+ // Ok, send a packet saying that we accept this session
+ // with the audio payload type and the transport
+ // candidate
+ Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT);
+ jout.addDescription(new JingleContentDescription.Audio(
+ new JinglePayloadType(bestCommonAudioPt)));
+ jout.addTransport(getTransportNeg().getJingleTransport(
+ bestRemoteCandidate));
+
+ // Send the "accept" and wait for the ACK
+ addExpectedId(jout.getPacketID());
+ sendFormattedJingle(jout);
+ }
+ }
+
+ /**
+ * The other endpoint has finally accepted our invitation.
+ *
+ * @throws XMPPException
+ */
+ public Jingle eventAccept(final Jingle jin) throws XMPPException {
+
+ PayloadType acceptedPayloadType = null;
+ TransportCandidate acceptedLocalCandidate = null;
+
+ // We process the "accepted" if we have finished the
+ // sub-negotiators. Maybe this is not needed (ie, the other endpoint
+ // can take the first valid transport candidate), but otherwise we
+ // must cancel the negotiators...
+ //
+ if (isFullyEstablished()) {
+ acceptedPayloadType = getAcceptedAudioPayloadType(jin);
+ acceptedLocalCandidate = getAcceptedLocalCandidate(jin);
+
+ if (acceptedPayloadType != null && acceptedLocalCandidate != null) {
+ if (acceptedPayloadType.equals(getMediaNeg().getBestCommonAudioPt())
+ && acceptedLocalCandidate.equals(getTransportNeg()
+ .getAcceptedLocalCandidate())) {
+ setState(active);
+ }
+ } else {
+ throw new JingleException(JingleError.NEGOTIATION_ERROR);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * We have received the Ack of our "accept"
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
+ */
+ public Jingle eventAck(final IQ iq) {
+ setState(active);
+ return null;
+ }
+
+ /**
+ * The other endpoint wants to redirect this connection.
+ */
+ public Jingle eventRedirect(final Jingle jin) {
+ String redirArg = null;
+
+ // TODO: parse the redirection parameters...
+
+ triggerSessionRedirect(redirArg);
+ return null;
+ }
+
+ /**
+ * The other endpoint has rejected our invitation.
+ *
+ * @throws XMPPException
+ */
+ public Jingle eventTerminate(final Jingle jin) throws XMPPException {
+ triggerSessionDeclined(null);
+ return super.eventTerminate(jin);
+ }
+
+ /**
+ * An error has occurred.
+ *
+ * @throws XMPPException
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage()));
+ super.eventError(iq);
+ }
+ }
+
+ /**
+ * State when we have an established session.
+ */
+ public class Active extends JingleNegotiator.State {
+ public Active(final JingleNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * We have a established session: notify the listeners
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventEnter() {
+ PayloadType.Audio bestCommonAudioPt = getMediaNeg().getBestCommonAudioPt();
+ TransportCandidate bestRemoteCandidate = getTransportNeg()
+ .getBestRemoteCandidate();
+ TransportCandidate acceptedLocalCandidate = getTransportNeg()
+ .getAcceptedLocalCandidate();
+
+ // Trigger the session established flag
+ triggerSessionEstablished(bestCommonAudioPt, bestRemoteCandidate,
+ acceptedLocalCandidate);
+
+ super.eventEnter();
+ }
+
+ /**
+ * Terminate the connection.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventTerminate(org.jivesoftware.smackx.packet.Jingle)
+ */
+ public Jingle eventTerminate(final Jingle jin) throws XMPPException {
+ triggerSessionClosed(null);
+ return super.eventTerminate(jin);
+ }
+
+ /**
+ * An error has occurred.
+ *
+ * @throws XMPPException
+ */
+ public void eventError(final IQ iq) throws XMPPException {
+ triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage()));
+ super.eventError(iq);
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java
new file mode 100644
index 000000000..e1a97f1a7
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/PayloadType.java
@@ -0,0 +1,289 @@
+package org.jivesoftware.smackx.jingle;
+
+/**
+ * Represents a payload type.
+ *
+ * @author Alvaro Saurin
+ */
+public class PayloadType {
+
+ public static int MAX_FIXED_PT = 95;
+
+ public static int INVALID_PT = 65535;
+
+ private int id;
+
+ private String name;
+
+ private int channels;
+
+ /**
+ * Constructor with Id, name and number of channels
+ *
+ * @param id The identifier
+ * @param name A name
+ * @param channels The number of channels
+ */
+ public PayloadType(final int id, final String name, final int channels) {
+ super();
+ this.id = id;
+ this.name = name;
+ this.channels = channels;
+ }
+
+ /**
+ * Default constructor.
+ */
+ public PayloadType() {
+ this(INVALID_PT, null, 1);
+ }
+
+ /**
+ * Constructor with Id and name
+ *
+ * @param id The identification
+ * @param name A name
+ */
+ public PayloadType(final int id, final String name) {
+ this(id, name, 1);
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param pt The other payload type.
+ */
+ public PayloadType(final PayloadType pt) {
+ this(pt.getId(), pt.getName(), pt.getChannels());
+ }
+
+ /**
+ * Get the ID.
+ *
+ * @return the ID
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Set the ID.
+ *
+ * @param id ID
+ */
+ public void setId(final int id) {
+ this.id = id;
+ }
+
+ /**
+ * Get the printable name.
+ *
+ * @return printable name for the payload type
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Set the printable name.
+ *
+ * @param name the printable name
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Get the number of channels used by this payload type.
+ *
+ * @return the number of channels
+ */
+ public int getChannels() {
+ return channels;
+ }
+
+ /**
+ * Set the numer of channels for a payload type.
+ *
+ * @param channels The number of channels
+ */
+ public void setChannels(final int channels) {
+ this.channels = channels;
+ }
+
+ /**
+ * Return true if the Payload type is not valid
+ *
+ * @return true if the payload type is invalid
+ */
+ public boolean isNull() {
+ if (getId() == INVALID_PT) {
+ return true;
+ } else if (getName() == null) {
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ public int hashCode() {
+ final int PRIME = 31;
+ int result = 1;
+ result = PRIME * result + getChannels();
+ result = PRIME * result + getId();
+ result = PRIME * result + (getName() == null ? 0 : getName().hashCode());
+ return result;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final PayloadType other = (PayloadType) obj;
+ if (getChannels() != other.getChannels()) {
+ return false;
+ }
+ if (getId() != other.getId()) {
+ return false;
+ }
+
+ // Compare names only for dynamic payload types
+ if (getId() > MAX_FIXED_PT) {
+ if (getName() == null) {
+ if (other.getName() != null) {
+ return false;
+ }
+ } else if (!getName().equals(other.getName())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Audio payload type.
+ */
+ public static class Audio extends PayloadType {
+ private int clockRate;
+
+ /**
+ * Constructor with all the attributes of an Audio payload type
+ *
+ * @param id The identifier
+ * @param name The name assigned to this payload type
+ * @param channels The number of channels
+ * @param rate The clock rate
+ */
+ public Audio(final int id, final String name, final int channels, final int rate) {
+ super(id, name, channels);
+ clockRate = rate;
+ }
+
+ /**
+ * Empty constructor.
+ */
+ public Audio() {
+ super();
+ clockRate = 0;
+ }
+
+ /**
+ * Constructor with Id and name
+ *
+ * @param id the Id for the payload type
+ * @param name the name of the payload type
+ */
+ public Audio(final int id, final String name) {
+ super(id, name);
+ clockRate = 0;
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param pt the other payload type
+ */
+ public Audio(final PayloadType pt) {
+ super(pt);
+ clockRate = 0;
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param pt the other payload type
+ */
+ public Audio(final PayloadType.Audio pt) {
+ super(pt);
+ clockRate = pt.getClockRate();
+ }
+
+ /**
+ * Get the sampling clockRate for a payload type
+ *
+ * @return The sampling clockRate
+ */
+ public int getClockRate() {
+ return clockRate;
+ }
+
+ /**
+ * Set tha sampling clockRate for a playload type.
+ *
+ * @param rate The sampling clockRate
+ */
+ public void setClockRate(final int rate) {
+ clockRate = rate;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ public int hashCode() {
+ final int PRIME = 31;
+ int result = super.hashCode();
+ result = PRIME * result + getClockRate();
+ return result;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!super.equals(obj)) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Audio other = (Audio) obj;
+ if (getClockRate() != other.getClockRate()) {
+ return false;
+ }
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java
new file mode 100644
index 000000000..54acebab9
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportManager.java
@@ -0,0 +1,171 @@
+package org.jivesoftware.smackx.jingle;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.nat.BasicResolver;
+import org.jivesoftware.smackx.nat.FixedResolver;
+import org.jivesoftware.smackx.nat.STUNResolver;
+import org.jivesoftware.smackx.nat.TransportResolver;
+import org.jivesoftware.smackx.packet.JingleTransport;
+
+/**
+ * Transport manager for Jingle.
+ *
+ * This class makes easier the use of transport resolvers by presenting a simple
+ * interface for algorithm selection. The transport manager also keeps the match
+ * between the resolution method and the &lt;transport&gt; element present in
+ * Jingle packets.
+ *
+ * This class must be used with a JingleManager instance in the following way:
+ *
+ * <pre>
+ * TransportManager tm = new TransportManager();
+ * tm.useSTUN(); // or some other method...
+ *
+ * JingleManager jm = new JingleManager(connection, tm.getResolver());
+ * jm.createOutgoingJingleSession(responder, payloads);
+ * </pre>
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+public class TransportManager {
+ // This class implements the context of a Strategy pattern...
+
+ // Current resolver.
+ private TransportResolver resolver;
+
+ // An instance of the transport resolver. This doesn't need to match
+ // the resolver, as we can use a STUNResolver for the JingleTransport.Ice or
+ // JingleTransport.RawUdp resolvers...
+ private JingleTransport jingleTransport;
+
+ /**
+ * Deafult contructor.
+ */
+ public TransportManager() {
+ useSTUNResolver(); // We use Raw-UDP/STUN by default...
+ }
+
+ /**
+ * Contructor with an external resolver.
+ */
+ public TransportManager(final TransportResolver resol, final JingleTransport trans) {
+ resolver = resol;
+ jingleTransport = trans;
+ }
+
+ /**
+ * Use the simple resolver.
+ */
+ public void useSimpleResolver() {
+ if (resolver != null && !(resolver instanceof BasicResolver)) {
+ try {
+ resolver.cancel();
+ } catch (XMPPException e) {
+ e.printStackTrace();
+ }
+ }
+ resolver = new BasicResolver();
+
+ if (jingleTransport == null
+ || !(jingleTransport instanceof JingleTransport.RawUdp)) {
+ jingleTransport = new JingleTransport.RawUdp();
+ }
+ }
+
+ /**
+ * Use a simple resolver, with a fixed IP address and port.
+ *
+ * @param ip the IP address
+ * @param port the port to use (0 for any port)
+ */
+ public void useSimpleResolver(final String ip, final int port) {
+ if (resolver != null && !(resolver instanceof FixedResolver)) {
+ try {
+ resolver.cancel();
+ } catch (XMPPException e) {
+ e.printStackTrace();
+ }
+ }
+ resolver = new FixedResolver(ip, port);
+
+ if (jingleTransport == null
+ || !(jingleTransport instanceof JingleTransport.RawUdp)) {
+ jingleTransport = new JingleTransport.RawUdp();
+ }
+ }
+
+ /**
+ * Returns true if the transport manager is using the simple resolver
+ *
+ * @return true if the transport manager is using the simple resolver
+ */
+ public boolean isUsingSimpleResolver() {
+ return (resolver instanceof BasicResolver || resolver instanceof FixedResolver)
+ && jingleTransport instanceof JingleTransport.RawUdp;
+ }
+
+ /**
+ * Use the STUN resolver.
+ */
+ public void useSTUNResolver() {
+ if (resolver != null && !(resolver instanceof STUNResolver)) {
+ try {
+ resolver.cancel();
+ } catch (XMPPException e) {
+ e.printStackTrace();
+ }
+ }
+ resolver = new STUNResolver();
+
+ if (jingleTransport == null
+ || !(jingleTransport instanceof JingleTransport.RawUdp)) {
+ jingleTransport = new JingleTransport.RawUdp();
+ }
+ }
+
+ /**
+ * Returns true if the transport manager is using the STUN resolver
+ *
+ * @return true if the transport manager is using the STUN resolver
+ */
+ public boolean isUsingSTUNResolver() {
+ return resolver instanceof STUNResolver
+ && jingleTransport instanceof JingleTransport.RawUdp;
+ }
+
+ /**
+ * Use the ICE resolver.
+ */
+ public void useICEResolver() {
+ // Not implemented yet
+ resolver = null;
+ jingleTransport = new JingleTransport.Ice();
+ }
+
+ /**
+ * Returns true if the transport manager is using the ICE resolver
+ *
+ * @return true if the transport manager is using the ICE resolver
+ */
+ public boolean isUsingICEResolver() {
+ return false;
+ }
+
+ /**
+ * Obtain the JingleTransport for the current resolver
+ *
+ * @return a JingleTransport instance
+ */
+ public JingleTransport getJingleTransport() {
+ return jingleTransport;
+ }
+
+ /**
+ * Obtain the resolver
+ *
+ * @return a TransportResolver.
+ */
+ public TransportResolver getResolver() {
+ return resolver;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java
new file mode 100644
index 000000000..bddb84b5b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/jingle/TransportNegotiator.java
@@ -0,0 +1,797 @@
+package org.jivesoftware.smackx.jingle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.nat.TransportCandidate;
+import org.jivesoftware.smackx.nat.TransportResolver;
+import org.jivesoftware.smackx.nat.TransportResolverListener;
+import org.jivesoftware.smackx.packet.Jingle;
+import org.jivesoftware.smackx.packet.JingleTransport;
+import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate;
+
+/**
+ * Transport negotiator.
+ *
+ * </p>
+ *
+ * This class is responsible for managing the transport negotiation process,
+ * handling all the packet interchange and the stage control.
+ *
+ * </p>
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+public abstract class TransportNegotiator extends JingleNegotiator {
+
+ // The time we give to the candidates check before we accept or decline the
+ // transport (in milliseconds)
+ public final static int CANDIDATES_ACCEPT_PERIOD = 4000;
+
+ // The session this nenotiator belongs to
+ private JingleSession session;
+
+ // The transport manager
+ private final TransportResolver resolver;
+
+ // Transport candidates we have offered
+ private final List offeredCandidates = new ArrayList();
+
+ // List of remote transport candidates
+ private final List remoteCandidates = new ArrayList();
+
+ // Valid remote candidates
+ private final List validRemoteCandidates = new ArrayList();
+
+ // The best local candidate we have offered (and accepted by the other part)
+ private TransportCandidate acceptedLocalCandidate;
+
+ // The thread that will report the result to the other end
+ private Thread resultThread;
+
+ // Listener for the resolver
+ private TransportResolverListener.Resolver resolverListener;
+
+ // states
+ final Inviting inviting;
+
+ final Accepting accepting;
+
+ final Pending pending;
+
+ final Active active;
+
+ /**
+ * Default constructor.
+ *
+ * @param js The Jingle session
+ * @param transResolver The TransportManager to use
+ */
+ public TransportNegotiator(final JingleSession js,
+ final TransportResolver transResolver) {
+ super(js.getConnection());
+
+ session = js;
+ resolver = transResolver;
+
+ resultThread = null;
+
+ // Create the states...
+ inviting = new Inviting(this);
+ accepting = new Accepting(this);
+ pending = new Pending(this);
+ active = new Active(this);
+ }
+
+ /**
+ * Get a new instance of the right JingleTransport class with this
+ * candidate.
+ *
+ * @return A JingleTransport instance
+ */
+ protected abstract JingleTransport getJingleTransport(TransportCandidate cand);
+
+ /**
+ * Return true if the transport candidate is acceptable for the current
+ * negotiator.
+ *
+ * @return true if the transport candidate is acceptable
+ */
+ protected abstract boolean acceptableTransportCandidate(TransportCandidate tc);
+
+ /**
+ * Obtain the best local candidate we want to offer.
+ *
+ * @return the best local candidate
+ */
+ public TransportCandidate getBestLocalCandidate() {
+ return resolver.getPreferredCandidate();
+ }
+
+ /**
+ * Set the best local transport candidate we have offered and accepted by
+ * the other endpoint.
+ *
+ * @param acceptedLocalCandidate the acceptedLocalCandidate to set
+ */
+ protected void setAcceptedLocalCandidate(final TransportCandidate bestLocalCandidate)
+ throws XMPPException {
+ if (resolver.getCandidatesList().contains(bestLocalCandidate)) {
+ acceptedLocalCandidate = bestLocalCandidate;
+ } else {
+ throw new XMPPException("Local transport candidate has not be offered.");
+ }
+ }
+
+ /**
+ * Get the best accepted local candidate we have offered.
+ *
+ * @return a transport candidate we have offered.
+ */
+ public TransportCandidate getAcceptedLocalCandidate() {
+ return acceptedLocalCandidate;
+ }
+
+ /**
+ * Obtain the best common transport candidate obtained in the negotiation.
+ *
+ * @return the bestRemoteCandidate
+ */
+ public abstract TransportCandidate getBestRemoteCandidate();
+
+ /**
+ * Get the list of remote candidates.
+ *
+ * @return the remoteCandidates
+ */
+ public List getRemoteCandidates() {
+ return remoteCandidates;
+ }
+
+ /**
+ * Add a remote candidate to the list. The candidate will be checked in
+ * order to verify if it is usable.
+ *
+ * @param rc a remote candidate to add and check.
+ */
+ public void addRemoteCandidate(final TransportCandidate rc) {
+ // Add the candidate to the list
+ if (rc != null) {
+ if (acceptableTransportCandidate(rc)) {
+ synchronized (remoteCandidates) {
+ remoteCandidates.add(rc);
+ }
+
+ // Check if the new candidate can be used.
+ checkRemoteCandidate(rc);
+ }
+ }
+ }
+
+ /**
+ * Add a offered candidate to the list.
+ *
+ * @param rc a remote candidate we have offered.
+ */
+ public void addOfferedCandidate(final TransportCandidate rc) {
+ // Add the candidate to the list
+ if (rc != null) {
+ synchronized (offeredCandidates) {
+ offeredCandidates.add(rc);
+ }
+ }
+ }
+
+ /**
+ * Check asynchronously the new transport candidate.
+ *
+ * @param offeredCandidate a transport candidates to check
+ */
+ protected void checkRemoteCandidate(final TransportCandidate offeredCandidate) {
+ resolver.addListener(new TransportResolverListener.Checker() {
+ public void candidateChecked(final TransportCandidate cand,
+ final boolean validCandidate) {
+ if (validCandidate) {
+ addValidRemoteCandidate(offeredCandidate);
+ }
+ }
+ });
+ resolver.check(offeredCandidate);
+ }
+
+ /**
+ * Return true if the transport is established.
+ *
+ * @return true if the transport is established.
+ */
+ public boolean isEstablished() {
+ return getBestRemoteCandidate() != null && getAcceptedLocalCandidate() != null;
+ }
+
+ /**
+ * Return true if the transport is fully established.
+ *
+ * @return true if the transport is fully established.
+ */
+ public boolean isFullyEstablished() {
+ return isEstablished() && getState() == active;
+ }
+
+ /**
+ * Launch a thread that checks, after some time, if any of the candidates
+ * offered by the other endpoint is usable. The thread does not check the
+ * candidates: it just checks if we have got a valid one and sends an Accept
+ * in that case.
+ */
+ private void delayedCheckBestCandidate(final JingleSession js, final Jingle jin) {
+ //
+ // If this is the first insertion in the list, start the thread that
+ // will send the result of our checks...
+ //
+ if (resultThread == null && !getRemoteCandidates().isEmpty()) {
+ resultThread = new Thread(new Runnable() {
+ public void run() {
+
+ // Sleep for some time, waiting for the candidates checks
+ try {
+ Thread.sleep(CANDIDATES_ACCEPT_PERIOD
+ + TransportResolver.CHECK_TIMEOUT);
+ } catch (InterruptedException e) {
+ }
+
+ // Once we are in pending state, look for any valid remote
+ // candidate, and send an "accept" if we have one...
+ TransportCandidate bestRemote = getBestRemoteCandidate();
+ State state = getState();
+
+ if (bestRemote != null && (state == pending || state == active)) {
+ // Accepting the remote candidate
+ Jingle jout = new Jingle(Jingle.Action.TRANSPORTACCEPT);
+ jout.addTransport(getJingleTransport(bestRemote));
+
+ // Send the packet
+ js.sendFormattedJingle(jin, jout);
+
+ if (isEstablished()) {
+ setState(active);
+ }
+ }
+ }
+ }, "Waiting for all the transport candidates checks...");
+
+ resultThread.setName("Transport Resolver Result");
+ resultThread.start();
+ }
+ }
+
+ /**
+ * Add a valid remote candidate to the list. The remote candidate has been
+ * checked, and the remote
+ *
+ * @param remoteCandidate a remote candidate to add
+ */
+ public void addValidRemoteCandidate(final TransportCandidate remoteCandidate) {
+ // Add the candidate to the list
+ if (remoteCandidate != null) {
+ synchronized (validRemoteCandidates) {
+ validRemoteCandidates.add(remoteCandidate);
+ }
+ }
+ }
+
+ /**
+ * Get the list of valid (ie, checked) remote candidates.
+ *
+ * @return The list of valid (ie, already checked) remote candidates.
+ */
+ public ArrayList getValidRemoteCandidatesList() {
+ synchronized (validRemoteCandidates) {
+ return new ArrayList(validRemoteCandidates);
+ }
+ }
+
+ /**
+ * Get an iterator for the list of valid (ie, checked) remote candidates.
+ *
+ * @return The iterator for the list of valid (ie, already checked) remote
+ * candidates.
+ */
+ public Iterator getValidRemoteCandidates() {
+ return Collections.unmodifiableList(getRemoteCandidates()).iterator();
+ }
+
+ /**
+ * Add an offered remote candidate. The transport candidate can be unusable:
+ * we must check if we can use it.
+ *
+ * @param rc the remote candidate to add.
+ */
+ public void addRemoteCandidates(final List rc) {
+ if (rc != null) {
+ if (rc.size() > 0) {
+ Iterator iter = rc.iterator();
+ while (iter.hasNext()) {
+ addRemoteCandidate((TransportCandidate) iter.next());
+ }
+ }
+ }
+ }
+
+ /**
+ * Parse the list of transport candidates from a Jingle packet.
+ *
+ * @param jin The input jingle packet
+ */
+ private ArrayList obtainCandidatesList(final Jingle jin) {
+ ArrayList result = new ArrayList();
+
+ if (jin != null) {
+ // Get the list of candidates from the packet
+ Iterator iTrans = jin.getTransports();
+ while (iTrans.hasNext()) {
+ JingleTransport trans = (JingleTransport) iTrans.next();
+
+ Iterator iCand = trans.getCandidates();
+ while (iCand.hasNext()) {
+ JingleTransportCandidate cand = (JingleTransportCandidate) iCand
+ .next();
+ TransportCandidate transCand = cand.getMediaTransport();
+ result.add(transCand);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ final private boolean isOfferStarted() {
+ return resolver.isResolving() || resolver.isResolved();
+ }
+
+ /**
+ * Send an offer for a transport candidate
+ *
+ * @param cand
+ */
+ private synchronized void sendTransportCandidateOffer(final TransportCandidate cand) {
+ if (!cand.isNull()) {
+ addOfferedCandidate(cand);
+
+ // Offer our new candidate...
+ session.sendFormattedJingle(new Jingle(getJingleTransport(cand)));
+ }
+ }
+
+ /**
+ * Create a Jingle packet where we announce our transport candidates.
+ *
+ * @throws XMPPException
+ */
+ protected void sendTransportCandidatesOffer() throws XMPPException {
+ List notOffered = resolver.getCandidatesList();
+ notOffered.removeAll(offeredCandidates);
+
+ // Send any unset candidate
+ Iterator iter = notOffered.iterator();
+ while (iter.hasNext()) {
+ sendTransportCandidateOffer((TransportCandidate) iter.next());
+ }
+
+ // .. and start a listener that will send any future candidate
+ if (resolverListener == null) {
+ // Add a listener that sends the offer when the resolver finishes...
+ resolverListener = new TransportResolverListener.Resolver() {
+ public void candidateAdded(final TransportCandidate cand) {
+ sendTransportCandidateOffer(cand);
+ }
+
+ public void end() {
+ }
+
+ public void init() {
+ }
+ };
+
+ resolver.addListener(resolverListener);
+ }
+
+ if (!(resolver.isResolving() || resolver.isResolved())) {
+ // Resolve our IP and port
+ resolver.resolve();
+ }
+ }
+
+ /**
+ * Dispatch an incoming packet. The method is responsible for recognizing
+ * the packet type and, depending on the current state, deliverying the
+ * packet to the right event handler and wait for a response.
+ *
+ * @param iq the packet received
+ * @return the new Jingle packet to send.
+ * @throws XMPPException
+ */
+ public IQ dispatchIncomingPacket(final IQ iq, final String id) throws XMPPException {
+ IQ jout = null;
+
+ if (invalidState()) {
+ if (iq == null) {
+ // With a null packet, we are just inviting the other end...
+ setState(inviting);
+ jout = getState().eventInvite();
+
+ } else {
+ if (iq instanceof Jingle) {
+ // If there is no specific media action associated, then we
+ // are being invited to a new session...
+ setState(accepting);
+ jout = getState().eventInitiate((Jingle) iq);
+ } else {
+ throw new IllegalStateException(
+ "Invitation IQ received is not a Jingle packet in Transport negotiator.");
+ }
+ }
+ } else {
+ if (iq == null) {
+ return null;
+ } else {
+ if (iq.getType().equals(IQ.Type.ERROR)) {
+ // Process errors
+ getState().eventError(iq);
+ } else if (iq.getType().equals(IQ.Type.RESULT)) {
+ // Process ACKs
+ if (isExpectedId(iq.getPacketID())) {
+ jout = getState().eventAck(iq);
+ removeExpectedId(iq.getPacketID());
+ }
+ } else if (iq instanceof Jingle) {
+ // Get the action from the Jingle packet
+ Jingle jin = (Jingle) iq;
+ Jingle.Action action = jin.getAction();
+
+ if (action != null) {
+ if (action.equals(Jingle.Action.TRANSPORTACCEPT)) {
+ jout = getState().eventAccept(jin);
+ } else if (action.equals(Jingle.Action.TRANSPORTDECLINE)) {
+ jout = getState().eventDecline(jin);
+ } else if (action.equals(Jingle.Action.TRANSPORTINFO)) {
+ jout = getState().eventInfo(jin);
+ } else if (action.equals(Jingle.Action.TRANSPORTMODIFY)) {
+ jout = getState().eventModify(jin);
+ }
+ }
+ }
+ }
+ }
+
+ // Save the Id for any ACK
+ if (id != null) {
+ addExpectedId(id);
+ } else {
+ if (jout != null) {
+ addExpectedId(jout.getPacketID());
+ }
+ }
+
+ return jout;
+ }
+
+ /**
+ * Trigger a session established event.
+ *
+ * @param best payload type that has been agreed.
+ */
+ protected void triggerTransportEstablished(final TransportCandidate local,
+ final TransportCandidate remote) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Transport) {
+ JingleListener.Transport mli = (JingleListener.Transport) li;
+ mli.transportEstablished(local, remote);
+ }
+ }
+ }
+
+ /**
+ * Trigger a media closed event.
+ *
+ * @param currPt current payload type that is cancelled.
+ */
+ protected void triggerTransportClosed(final TransportCandidate cand) {
+ ArrayList listeners = getListenersList();
+ Iterator iter = listeners.iterator();
+ while (iter.hasNext()) {
+ JingleListener li = (JingleListener) iter.next();
+ if (li instanceof JingleListener.Transport) {
+ JingleListener.Transport mli = (JingleListener.Transport) li;
+ mli.transportClosed(cand);
+ }
+ }
+ }
+
+ // States
+
+ /**
+ * First stage when we send a session request.
+ */
+ public class Inviting extends JingleNegotiator.State {
+
+ public Inviting(final TransportNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * Create an initial Jingle packet with an empty transport.
+ */
+ public Jingle eventInvite() {
+ return new Jingle(getJingleTransport(null));
+ }
+
+ /**
+ * We have received some candidates. This can happen _before_ the ACK
+ * has been recieved...
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventInfo(org.jivesoftware.smackx.packet.Jingle)
+ */
+ public Jingle eventInfo(final Jingle jin) throws XMPPException {
+ // Parse the Jingle and get any proposed transport candidates
+ addRemoteCandidates(obtainCandidatesList(jin));
+
+ // Wait for some time and check if we have a valid candidate to
+ // use...
+ delayedCheckBestCandidate(session, jin);
+
+ return super.eventInfo(jin);
+ }
+
+ /**
+ * The other endpoint has partially accepted our invitation: start
+ * offering a list of candidates.
+ *
+ * @return an IQ packet
+ * @throws XMPPException
+ */
+ public Jingle eventAck(final IQ iq) throws XMPPException {
+ sendTransportCandidatesOffer();
+ setState(pending);
+ return super.eventAck(iq);
+ }
+ }
+
+ /**
+ * We are accepting connections. This is the starting state when we accept a
+ * connection...
+ */
+ public class Accepting extends JingleNegotiator.State {
+ public Accepting(final TransportNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * We have received an invitation. The packet will be ACKed by lower
+ * levels...
+ */
+ public Jingle eventInitiate(final Jingle jin) throws XMPPException {
+ // Parse the Jingle and get any proposed transport candidates
+ addRemoteCandidates(obtainCandidatesList(jin));
+
+ // Start offering candidates
+ sendTransportCandidatesOffer();
+
+ // All these candidates will be checked asyncronously. Wait for some
+ // time and check if we have a valid candidate to use...
+ delayedCheckBestCandidate(session, jin);
+
+ // Set the next state
+ setState(pending);
+
+ return super.eventInitiate(jin);
+ }
+ }
+
+ /**
+ * We are still receiving candidates
+ */
+ public class Pending extends JingleNegotiator.State {
+
+ public Pending(final TransportNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * One of our transport candidates has been accepted.
+ *
+ * @param jin The input packet
+ * @return a Jingle packet
+ * @throws XMPPException an exception
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAccept(org.jivesoftware.smackx.packet.Jingle)
+ */
+ public Jingle eventAccept(final Jingle jin) throws XMPPException {
+ Jingle response = null;
+
+ // Parse the Jingle and get the accepted candidate
+ ArrayList accepted = obtainCandidatesList(jin);
+ if (!accepted.isEmpty()) {
+ TransportCandidate cand = (TransportCandidate) accepted.get(0);
+
+ setAcceptedLocalCandidate(cand);
+
+ if (isEstablished()) {
+ setState(active);
+ }
+ }
+ return response;
+ }
+
+ /**
+ * We have received another remote transport candidates.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventInfo(org.jivesoftware.smackx.packet.Jingle)
+ */
+ public Jingle eventInfo(final Jingle jin) throws XMPPException {
+
+ sendTransportCandidatesOffer();
+
+ // Parse the Jingle and get any proposed transport candidates
+ addRemoteCandidates(obtainCandidatesList(jin));
+
+ // Wait for some time and check if we have a valid candidate to
+ // use...
+ delayedCheckBestCandidate(session, jin);
+
+ return super.eventInfo(jin);
+ }
+
+ /**
+ * None of our transport candidates has been accepted...
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventDecline(org.jivesoftware.smackx.packet.Jingle)
+ */
+ public Jingle eventDecline(final Jingle inJingle) throws JingleException {
+ throw new JingleException("No common payload found.");
+ }
+ }
+
+ /**
+ * "Active" state: we have an agreement about the codec...
+ */
+ public class Active extends JingleNegotiator.State {
+
+ public Active(final TransportNegotiator neg) {
+ super(neg);
+ }
+
+ /**
+ * We have an agreement.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventEnter() {
+ triggerTransportEstablished(getAcceptedLocalCandidate(),
+ getBestRemoteCandidate());
+ super.eventEnter();
+ }
+
+ /**
+ * We have finished the transport.
+ *
+ * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
+ */
+ public void eventExit() {
+ triggerTransportClosed(null);
+ super.eventExit();
+ }
+ }
+
+ // Subclasses
+
+ /**
+ * Raw-UDP transport negotiator
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+ public static class RawUdp extends TransportNegotiator {
+
+ /**
+ * Default constructor, with a JingleSession and transport manager.
+ *
+ * @param js The Jingle session this negotiation belongs to.
+ * @param res The transport resolver to use.
+ */
+ public RawUdp(final JingleSession js, final TransportResolver res) {
+ super(js, res);
+ }
+
+ /**
+ * Get a JingleTransport instance.
+ */
+ protected JingleTransport getJingleTransport(final TransportCandidate bestRemote) {
+ JingleTransport.RawUdp jt = new JingleTransport.RawUdp();
+ jt.addCandidate(new JingleTransport.RawUdp.Candidate(bestRemote));
+ return jt;
+ }
+
+ /**
+ * Obtain the best common transport candidate obtained in the
+ * negotiation.
+ *
+ * @return the bestRemoteCandidate
+ */
+ public TransportCandidate getBestRemoteCandidate() {
+ // Hopefully, we only have one validRemoteCandidate
+ ArrayList cands = getValidRemoteCandidatesList();
+ if (!cands.isEmpty()) {
+ return (TransportCandidate) cands.get(0);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return true for fixed candidates.
+ */
+ protected boolean acceptableTransportCandidate(final TransportCandidate tc) {
+ return tc instanceof TransportCandidate.Fixed;
+ }
+ }
+
+ /**
+ * Ice transport negotiator.
+ *
+ * @author Alvaro Saurin <alvaro.saurin@gmail.com>
+ */
+ public static class Ice extends TransportNegotiator {
+
+ /**
+ * Default constructor, with a JingleSession and transport manager.
+ *
+ * @param js The Jingle session this negotiation belongs to.
+ * @param res The transport manager to use.
+ */
+ public Ice(final JingleSession js, final TransportResolver res) {
+ super(js, res);
+ }
+
+ /**
+ * Get a JingleTransport instance.
+ *
+ * @param bestRemote
+ */
+ protected JingleTransport getJingleTransport(final TransportCandidate bestRemote) {
+ JingleTransport.Ice jt = new JingleTransport.Ice();
+ jt.addCandidate(new JingleTransport.Ice.Candidate(bestRemote));
+ return jt;
+ }
+
+ /**
+ * Obtain the best remote candidate obtained in the negotiation so far.
+ *
+ * @return the bestRemoteCandidate
+ */
+ public TransportCandidate getBestRemoteCandidate() {
+ TransportCandidate.Ice result = null;
+
+ ArrayList cands = getValidRemoteCandidatesList();
+ if (!cands.isEmpty()) {
+ Collections.sort(cands);
+ // Return the last candidate
+ result = (TransportCandidate.Ice) cands.get(cands.size() - 1);
+ }
+
+ return result;
+ }
+
+ /**
+ * Return true for ICE candidates.
+ */
+ protected boolean acceptableTransportCandidate(final TransportCandidate tc) {
+ return tc instanceof TransportCandidate.Ice;
+ }
+ }
+}

Back to the top