001/*
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.jingleold;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Random;
024import java.util.logging.Level;
025import java.util.logging.Logger;
026
027import org.jivesoftware.smack.AbstractConnectionClosedListener;
028import org.jivesoftware.smack.ConnectionListener;
029import org.jivesoftware.smack.SmackException;
030import org.jivesoftware.smack.SmackException.NoResponseException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.StanzaListener;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.XMPPException;
035import org.jivesoftware.smack.XMPPException.XMPPErrorException;
036import org.jivesoftware.smack.filter.StanzaFilter;
037import org.jivesoftware.smack.packet.IQ;
038import org.jivesoftware.smack.packet.Stanza;
039import org.jivesoftware.smack.packet.StanzaError;
040
041import org.jivesoftware.smackx.jingleold.listeners.JingleListener;
042import org.jivesoftware.smackx.jingleold.listeners.JingleMediaListener;
043import org.jivesoftware.smackx.jingleold.listeners.JingleSessionListener;
044import org.jivesoftware.smackx.jingleold.listeners.JingleTransportListener;
045import org.jivesoftware.smackx.jingleold.media.JingleMediaManager;
046import org.jivesoftware.smackx.jingleold.media.JingleMediaSession;
047import org.jivesoftware.smackx.jingleold.media.MediaNegotiator;
048import org.jivesoftware.smackx.jingleold.media.MediaReceivedListener;
049import org.jivesoftware.smackx.jingleold.media.PayloadType;
050import org.jivesoftware.smackx.jingleold.nat.JingleTransportManager;
051import org.jivesoftware.smackx.jingleold.nat.TransportCandidate;
052import org.jivesoftware.smackx.jingleold.nat.TransportNegotiator;
053import org.jivesoftware.smackx.jingleold.nat.TransportResolver;
054import org.jivesoftware.smackx.jingleold.packet.Jingle;
055import org.jivesoftware.smackx.jingleold.packet.JingleError;
056
057import org.jxmpp.jid.Jid;
058
059/**
060 * An abstract Jingle session. This class contains some basic properties of
061 * every Jingle session. However, the concrete implementation can be found in
062 * subclasses.
063 *
064 * @author Alvaro Saurin
065 * @author Jeff Williams
066 */
067public final class JingleSession extends JingleNegotiator implements MediaReceivedListener {
068
069    private static final Logger LOGGER = Logger.getLogger(JingleSession.class.getName());
070
071    // static
072    private static final HashMap<XMPPConnection, JingleSession> sessions = new HashMap<>();
073
074    private static final Random randomGenerator = new Random();
075
076    // non-static
077
078    private Jid initiator; // Who started the communication
079
080    private Jid responder; // The other endpoint
081
082    private String sid; // A unique id that identifies this session
083
084    private ConnectionListener connectionListener;
085
086    private StanzaListener packetListener;
087
088    private StanzaFilter packetFilter;
089
090    List<JingleMediaManager> jingleMediaManagers = null;
091
092    private JingleSessionState sessionState;
093
094    private final List<ContentNegotiator> contentNegotiators;
095
096    private final XMPPConnection connection;
097
098    private String sessionInitPacketID;
099
100    private final Map<String, JingleMediaSession> mediaSessionMap;
101
102    /**
103     * Full featured JingleSession constructor.
104     *
105     * @param conn TODO javadoc me please
106     *            the XMPPConnection which is used
107     * @param initiator TODO javadoc me please
108     *            the initiator JID
109     * @param responder TODO javadoc me please
110     *            the responder JID
111     * @param sessionid TODO javadoc me please
112     *            the session ID
113     * @param jingleMediaManagers TODO javadoc me please
114     *            the jingleMediaManager
115     */
116    public JingleSession(XMPPConnection conn, Jid initiator, Jid responder, String sessionid,
117            List<JingleMediaManager> jingleMediaManagers) {
118        super();
119
120        this.initiator = initiator;
121        this.responder = responder;
122        this.sid = sessionid;
123        this.jingleMediaManagers = jingleMediaManagers;
124        this.setSession(this);
125        this.connection = conn;
126
127        // Initially, we don't known the session state.
128        setSessionState(JingleSessionStateUnknown.getInstance());
129
130        contentNegotiators = new ArrayList<>();
131        mediaSessionMap = new HashMap<>();
132
133        // Add the session to the list and register the listeners
134        registerInstance();
135        installConnectionListeners(conn);
136    }
137
138    /**
139     * JingleSession constructor (for an outgoing Jingle session).
140     *
141     * @param conn
142     *            Connection
143     * @param request the request.
144     * @param initiator
145     *            the initiator JID
146     * @param responder
147     *            the responder JID
148     * @param jingleMediaManagers
149     *            the jingleMediaManager
150     */
151    public JingleSession(XMPPConnection conn, JingleSessionRequest request, Jid initiator, Jid responder,
152            List<JingleMediaManager> jingleMediaManagers) {
153        this(conn, initiator, responder, generateSessionId(), jingleMediaManagers);
154        // sessionRequest = request; // unused
155    }
156
157    /**
158     * Get the session initiator.
159     *
160     * @return the initiator
161     */
162    public Jid getInitiator() {
163        return initiator;
164    }
165
166    @Override
167    public XMPPConnection getConnection() {
168        return connection;
169    }
170
171    /**
172     * Set the session initiator.
173     *
174     * @param initiator TODO javadoc me please
175     *            the initiator to set
176     */
177    public void setInitiator(Jid initiator) {
178        this.initiator = initiator;
179    }
180
181    /**
182     * Get the Media Manager of this Jingle Session.
183     *
184     * @return the JingleMediaManagers
185     */
186    public List<JingleMediaManager> getMediaManagers() {
187        return jingleMediaManagers;
188    }
189
190    /**
191     * Set the Media Manager of this Jingle Session.
192     *
193     * @param jingleMediaManagers TODO javadoc me please
194     */
195    public void setMediaManagers(List<JingleMediaManager> jingleMediaManagers) {
196        this.jingleMediaManagers = jingleMediaManagers;
197    }
198
199    /**
200     * Get the session responder.
201     *
202     * @return the responder
203     */
204    public Jid getResponder() {
205        return responder;
206    }
207
208    /**
209     * Set the session responder.
210     *
211     * @param responder TODO javadoc me please
212     *            the receptor to set
213     */
214    public void setResponder(Jid responder) {
215        this.responder = responder;
216    }
217
218    /**
219     * Get the session ID.
220     *
221     * @return the sid
222     */
223    public String getSid() {
224        return sid;
225    }
226
227    /**
228     * Set the session ID
229     *
230     * @param sessionId TODO javadoc me please
231     *            the sid to set
232     */
233    void setSid(String sessionId) {
234        sid = sessionId;
235    }
236
237    /**
238     * Generate a unique session ID.
239     *
240     * @return the generated session ID.
241     */
242    static String generateSessionId() {
243        return String.valueOf(randomGenerator.nextInt(Integer.MAX_VALUE) + randomGenerator.nextInt(Integer.MAX_VALUE));
244    }
245
246    /**
247     * Validate the state changes.
248     *
249     * @param stateIs the jingle session state.
250     */
251
252    public void setSessionState(JingleSessionState stateIs) {
253
254        LOGGER.fine("Session state change: " + sessionState + "->" + stateIs);
255        stateIs.enter();
256        sessionState = stateIs;
257    }
258
259    public JingleSessionState getSessionState() {
260        return sessionState;
261    }
262
263    /**
264     * Return true if all of the media managers have finished.
265     *
266     * @return <code>true</code> if fully established.
267     */
268    public boolean isFullyEstablished() {
269        boolean result = true;
270        for (ContentNegotiator contentNegotiator : contentNegotiators) {
271            if (!contentNegotiator.isFullyEstablished())
272                result = false;
273        }
274        return result;
275    }
276
277    // ----------------------------------------------------------------------------------------------------------
278    // Receive section
279    // ----------------------------------------------------------------------------------------------------------
280
281    /**
282     * Process and respond to an incoming packet. This method is called
283     * from the stanza listener dispatcher when a new stanza has arrived. The
284     * method is responsible for recognizing the stanza type and, depending on
285     * the current state, delivering it to the right event handler and wait for
286     * a response. The response will be another Jingle stanza that will be sent
287     * to the other end point.
288     *
289     * @param iq TODO javadoc me please
290     *            the stanza received
291     * @throws XMPPException if an XMPP protocol error was received.
292     * @throws SmackException if Smack detected an exceptional situation.
293     * @throws InterruptedException if the calling thread was interrupted.
294     */
295    public synchronized void receivePacketAndRespond(IQ iq) throws XMPPException, SmackException, InterruptedException {
296        List<IQ> responses = new ArrayList<>();
297
298        String responseId;
299
300        LOGGER.fine("Packet: " + iq.toXML());
301
302        try {
303
304            // Dispatch the packet to the JingleNegotiators and get back a list of the results.
305            responses.addAll(dispatchIncomingPacket(iq, null));
306
307            if (iq != null) {
308                responseId = iq.getStanzaId();
309
310                // Send the IQ to each of the content negotiators for further processing.
311                // Each content negotiator may pass back a list of JingleContent for addition to the response packet.
312                // CHECKSTYLE:OFF
313                for (ContentNegotiator contentNegotiator : contentNegotiators) {
314                    // If at this point the content negotiator isn't started, it's because we sent a session-init jingle
315                    // packet from startOutgoing() and we're waiting for the other side to let us know they're ready
316                    // to take jingle packets.  (This packet might be a session-terminate, but that will get handled
317                    // later.
318                    if (!contentNegotiator.isStarted()) {
319                        contentNegotiator.start();
320                    }
321                    responses.addAll(contentNegotiator.dispatchIncomingPacket(iq, responseId));
322                }
323                // CHECKSTYLE:ON
324
325            }
326            // Acknowledge the IQ reception
327            // Not anymore.  The state machine generates an appropriate response IQ that
328            // gets sent back at the end of this routine.
329            // sendAck(iq);
330
331        } catch (JingleException e) {
332            // Send an error message, if present
333            JingleError error = e.getError();
334            if (error != null) {
335                responses.add(createJingleError(iq, error));
336            }
337
338            // Notify the session end and close everything...
339            triggerSessionClosedOnError(e);
340        }
341
342        //        // If the response is anything other than a RESULT then send it now.
343        //        if ((response != null) && (!response.getType().equals(IQ.Type.result))) {
344        //            getConnection().sendStanza(response);
345        //        }
346
347        // Loop through all of the responses and send them.
348        for (IQ response : responses) {
349            sendStanza(response);
350        }
351    }
352
353    /**
354     * Dispatch an incoming packet. The method is responsible for recognizing
355     * the stanza type and, depending on the current state, delivering the
356     * stanza to the right event handler and wait for a response.
357     *
358     * @param iq TODO javadoc me please
359     *            the stanza received
360     * @return the new Jingle stanza to send.
361     * @throws XMPPException if an XMPP protocol error was received.
362     * @throws SmackException if Smack detected an exceptional situation.
363     * @throws InterruptedException if the calling thread was interrupted.
364     */
365    @Override
366    public List<IQ> dispatchIncomingPacket(IQ iq, String id) throws XMPPException, SmackException, InterruptedException {
367        List<IQ> responses = new ArrayList<>();
368        IQ response = null;
369
370        if (iq != null) {
371            if (iq.getType().equals(IQ.Type.error)) {
372                // Process errors
373                // TODO getState().eventError(iq);
374            } else if (iq.getType().equals(IQ.Type.result)) {
375                // Process ACKs
376                if (isExpectedId(iq.getStanzaId())) {
377
378                    // The other side provisionally accepted our session-initiate.
379                    // Kick off some negotiators.
380                    if (iq.getStanzaId().equals(sessionInitPacketID)) {
381                        startNegotiators();
382                    }
383                    removeExpectedId(iq.getStanzaId());
384                }
385            } else if (iq instanceof Jingle) {
386                // It is not an error: it is a Jingle packet...
387                Jingle jin = (Jingle) iq;
388                JingleActionEnum action = jin.getAction();
389
390                // Depending on the state we're in we'll get different processing actions.
391                // (See Design Patterns AKA GoF State behavioral pattern.)
392                response = getSessionState().processJingle(this, jin, action);
393            }
394        }
395
396        if (response != null) {
397            // Save the packet id, for recognizing ACKs...
398            addExpectedId(response.getStanzaId());
399            responses.add(response);
400        }
401
402        return responses;
403    }
404
405    /**
406     * Add a new content negotiator on behalf of a &lt;content/&gt; section received.
407     *
408     * @param inContentNegotiator the content negotiator.
409     */
410    public void addContentNegotiator(ContentNegotiator inContentNegotiator) {
411        contentNegotiators.add(inContentNegotiator);
412    }
413
414
415
416     // ----------------------------------------------------------------------------------------------------------
417    // Send section
418    // ----------------------------------------------------------------------------------------------------------
419
420    public void sendStanza(IQ iq) throws NotConnectedException, InterruptedException {
421
422        if (iq instanceof Jingle) {
423
424            sendFormattedJingle((Jingle) iq);
425
426        } else {
427
428            getConnection().sendStanza(iq);
429        }
430    }
431
432    /**
433     * Complete and send a packet. Complete all the null fields in a Jingle
434     * response, using the session information we have.
435     *
436     * @param jout
437     *            the Jingle stanza we want to complete and send
438     * @return the Jingle stanza.
439     * @throws NotConnectedException if the XMPP connection is not connected.
440     * @throws InterruptedException if the calling thread was interrupted.
441     */
442    public Jingle sendFormattedJingle(Jingle jout) throws NotConnectedException, InterruptedException {
443        return sendFormattedJingle(null, jout);
444    }
445
446    /**
447     * Complete and send a packet. Complete all the null fields in a Jingle
448     * response, using the session information we have or some info from the
449     * incoming packet.
450     *
451     * @param iq The Jingle stanza we are responding to
452     * @param jout the Jingle stanza we want to complete and send
453     * @return the Jingle stanza.
454     * @throws NotConnectedException if the XMPP connection is not connected.
455     * @throws InterruptedException if the calling thread was interrupted.
456     */
457    public Jingle sendFormattedJingle(IQ iq, Jingle jout) throws NotConnectedException, InterruptedException {
458        if (jout != null) {
459            if (jout.getInitiator() == null) {
460                jout.setInitiator(getInitiator());
461            }
462
463            if (jout.getResponder() == null) {
464                jout.setResponder(getResponder());
465            }
466
467            if (jout.getSid() == null) {
468                jout.setSid(getSid());
469            }
470
471            Jid me = getConnection().getUser();
472            Jid other = getResponder().equals(me) ? getInitiator() : getResponder();
473
474            if (jout.getTo() == null) {
475                if (iq != null) {
476                    jout.setTo(iq.getFrom());
477                } else {
478                    jout.setTo(other);
479                }
480            }
481
482            if (jout.getFrom() == null) {
483                if (iq != null) {
484                    jout.setFrom(iq.getTo());
485                } else {
486                    jout.setFrom(me);
487                }
488            }
489
490            // the packet.
491            // CHECKSTYLE:OFF
492            if ((getConnection() != null) && getConnection().isConnected())
493                getConnection().sendStanza(jout);
494            // CHECKSTYLE:ON
495        }
496        return jout;
497    }
498
499    /**
500     * Acknowledge a IQ packet.
501     *
502     * @param iq The IQ to acknowledge.
503     * @return the ack IQ.
504     */
505    public IQ createAck(IQ iq) {
506        IQ result = null;
507
508        if (iq != null) {
509            // Don't acknowledge ACKs, errors...
510            if (iq.getType().equals(IQ.Type.set)) {
511                IQ ack = IQ.createResultIQ(iq);
512
513                // No! Don't send it.  Let it flow to the normal way IQ results get processed and sent.
514                // getConnection().sendStanza(ack);
515                result = ack;
516            }
517        }
518        return result;
519    }
520
521    /**
522     * Send a content info message.
523     */
524    //    public synchronized void sendContentInfo(ContentInfo ci) {
525    //        sendStanza(new Jingle(new JingleContentInfo(ci)));
526    //    }
527
528    @Override
529    public int hashCode() {
530        return Jingle.getSessionHash(getSid(), getInitiator());
531    }
532
533    @Override
534    public boolean equals(Object obj) {
535        if (this == obj) {
536            return true;
537        }
538        if (obj == null) {
539            return false;
540        }
541        if (getClass() != obj.getClass()) {
542            return false;
543        }
544
545        final JingleSession other = (JingleSession) obj;
546
547        if (initiator == null) {
548            if (other.initiator != null) {
549                return false;
550            }
551        } else if (!initiator.equals(other.initiator)) {
552            // Todo check behavior
553            // return false;
554        }
555
556        if (responder == null) {
557            if (other.responder != null) {
558                return false;
559            }
560        } else if (!responder.equals(other.responder)) {
561            return false;
562        }
563
564        if (sid == null) {
565            if (other.sid != null) {
566                return false;
567            }
568        } else if (!sid.equals(other.sid)) {
569            return false;
570        }
571
572        return true;
573    }
574
575    // Instances management
576
577    /**
578     * Clean a session from the list.
579     *
580     * @param connection TODO javadoc me please
581     *            The connection to clean up
582     */
583    private static void unregisterInstanceFor(XMPPConnection connection) {
584        synchronized (sessions) {
585            sessions.remove(connection);
586        }
587    }
588
589    /**
590     * Register this instance.
591     */
592    private void registerInstance() {
593        synchronized (sessions) {
594            sessions.put(getConnection(), this);
595        }
596    }
597
598    /**
599     * Returns the JingleSession related to a particular connection.
600     *
601     * @param con TODO javadoc me please
602     *            A XMPP connection
603     * @return a Jingle session
604     */
605    public static synchronized JingleSession getInstanceFor(XMPPConnection con) {
606        if (con == null) {
607            throw new IllegalArgumentException("XMPPConnection cannot be null");
608        }
609
610        JingleSession result = null;
611        synchronized (sessions) {
612            if (sessions.containsKey(con)) {
613                result = sessions.get(con);
614            }
615        }
616
617        return result;
618    }
619
620    /**
621     * Configure a session, setting some action listeners...
622     *
623     * @param connection TODO javadoc me please
624     *            The connection to set up
625     */
626    private void installConnectionListeners(final XMPPConnection connection) {
627        if (connection != null) {
628            connectionListener = new AbstractConnectionClosedListener() {
629                @Override
630                public void connectionTerminated() {
631                    unregisterInstanceFor(connection);
632                }
633            };
634            connection.addConnectionListener(connectionListener);
635        }
636    }
637
638    private void removeConnectionListener() {
639        // CHECKSTYLE:OFF
640        if (connectionListener != null) {
641            getConnection().removeConnectionListener(connectionListener);
642
643            LOGGER.fine("JINGLE SESSION: REMOVE CONNECTION LISTENER");
644        }
645        // CHECKSTYLE:ON
646    }
647
648    /**
649     * Remove the stanza listener used for processing packet.
650     */
651    void removeAsyncPacketListener() {
652        if (packetListener != null) {
653            getConnection().removeAsyncStanzaListener(packetListener);
654
655            LOGGER.fine("JINGLE SESSION: REMOVE PACKET LISTENER");
656        }
657    }
658
659    /**
660     * Install the stanza listener. The listener is responsible for responding
661     * to any stanza that we receive...
662     */
663    void updatePacketListener() {
664        removeAsyncPacketListener();
665
666        LOGGER.fine("UpdatePacketListener");
667
668        packetListener = new StanzaListener() {
669            @Override
670            public void processStanza(Stanza packet) {
671                try {
672                    receivePacketAndRespond((IQ) packet);
673                } catch (Exception e) {
674                    LOGGER.log(Level.WARNING, "exception", e);
675                }
676            }
677        };
678
679        packetFilter = new StanzaFilter() {
680            @Override
681            public boolean accept(Stanza packet) {
682
683                if (packet instanceof IQ) {
684                    IQ iq = (IQ) packet;
685
686                    Jid me = getConnection().getUser();
687
688                    if (!iq.getTo().equals(me)) {
689                        return false;
690                    }
691
692                    Jid other = getResponder().equals(me) ? getInitiator() : getResponder();
693
694                    if (iq.getFrom() == null || !iq.getFrom().equals(other == null ? "" : other)) {
695                        return false;
696                    }
697
698                    if (iq instanceof Jingle) {
699                        Jingle jin = (Jingle) iq;
700
701                        String sid = jin.getSid();
702                        if (sid == null || !sid.equals(getSid())) {
703                            LOGGER.fine("Ignored Jingle(SID) " + sid + "|" + getSid() + " :" + iq.toXML());
704                            return false;
705                        }
706                        Jid ini = jin.getInitiator();
707                        if (!ini.equals(getInitiator())) {
708                            LOGGER.fine("Ignored Jingle(INI): " + iq.toXML());
709                            return false;
710                        }
711                    } else {
712                        // We accept some non-Jingle IQ packets: ERRORs and ACKs
713                        if (iq.getType().equals(IQ.Type.set)) {
714                            LOGGER.fine("Ignored Jingle(TYPE): " + iq.toXML());
715                            return false;
716                        } else if (iq.getType().equals(IQ.Type.get)) {
717                            LOGGER.fine("Ignored Jingle(TYPE): " + iq.toXML());
718                            return false;
719                        }
720                    }
721                    return true;
722                }
723                return false;
724            }
725        };
726
727        getConnection().addAsyncStanzaListener(packetListener, packetFilter);
728    }
729
730    // Listeners
731
732    /**
733     * Add a listener for jmf negotiation events.
734     *
735     * @param li TODO javadoc me please
736     *            The listener
737     */
738    public void addMediaListener(JingleMediaListener li) {
739        for (ContentNegotiator contentNegotiator : contentNegotiators) {
740            if (contentNegotiator.getMediaNegotiator() != null) {
741                contentNegotiator.getMediaNegotiator().addListener(li);
742            }
743        }
744
745    }
746
747    /**
748     * Remove a listener for jmf negotiation events.
749     *
750     * @param li TODO javadoc me please
751     *            The listener
752     */
753    public void removeMediaListener(JingleMediaListener li) {
754        for (ContentNegotiator contentNegotiator : contentNegotiators) {
755            if (contentNegotiator.getMediaNegotiator() != null) {
756                contentNegotiator.getMediaNegotiator().removeListener(li);
757            }
758        }
759    }
760
761    /**
762     * Add a listener for transport negotiation events.
763     *
764     * @param li TODO javadoc me please
765     *            The listener
766     */
767    public void addTransportListener(JingleTransportListener li) {
768        for (ContentNegotiator contentNegotiator : contentNegotiators) {
769            if (contentNegotiator.getTransportNegotiator() != null) {
770                contentNegotiator.getTransportNegotiator().addListener(li);
771            }
772        }
773    }
774
775    /**
776     * Remove a listener for transport negotiation events.
777     *
778     * @param li TODO javadoc me please
779     *            The listener
780     */
781    public void removeTransportListener(JingleTransportListener li) {
782        for (ContentNegotiator contentNegotiator : contentNegotiators) {
783            if (contentNegotiator.getTransportNegotiator() != null) {
784                contentNegotiator.getTransportNegotiator().removeListener(li);
785            }
786        }
787    }
788
789    /**
790     * Setup the listeners that act on events coming from the lower level negotiators.
791     */
792
793    public void setupListeners() {
794
795        JingleMediaListener jingleMediaListener = new JingleMediaListener() {
796            @Override
797            public void mediaClosed(PayloadType cand) {
798            }
799
800            @Override
801            public void mediaEstablished(PayloadType pt) throws NotConnectedException, InterruptedException {
802                if (isFullyEstablished()) {
803                    Jingle jout = new Jingle(JingleActionEnum.SESSION_ACCEPT);
804
805                    // Build up a response packet from each media manager.
806                    for (ContentNegotiator contentNegotiator : contentNegotiators) {
807                        if (contentNegotiator.getNegotiatorState() == JingleNegotiatorState.SUCCEEDED)
808                            jout.addContent(contentNegotiator.getJingleContent());
809                    }
810                    // Send the "accept" and wait for the ACK
811                    addExpectedId(jout.getStanzaId());
812                    sendStanza(jout);
813
814                    // triggerSessionEstablished();
815
816                }
817            }
818        };
819
820        JingleTransportListener jingleTransportListener = new JingleTransportListener() {
821
822            @Override
823            public void transportEstablished(TransportCandidate local, TransportCandidate remote) throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException {
824                if (isFullyEstablished()) {
825                // CHECKSTYLE:OFF
826                    // Indicate that this session is active.
827                    setSessionState(JingleSessionStateActive.getInstance());
828
829                    for (ContentNegotiator contentNegotiator : contentNegotiators) {
830                // CHECKSTYLE:ON
831                        if (contentNegotiator.getNegotiatorState() == JingleNegotiatorState.SUCCEEDED)
832                            contentNegotiator.triggerContentEstablished();
833                    }
834
835                    if (getSessionState().equals(JingleSessionStatePending.getInstance())) {
836
837                        Jingle jout = new Jingle(JingleActionEnum.SESSION_ACCEPT);
838
839                        // Build up a response packet from each media manager.
840                        for (ContentNegotiator contentNegotiator : contentNegotiators) {
841                            if (contentNegotiator.getNegotiatorState() == JingleNegotiatorState.SUCCEEDED)
842                                jout.addContent(contentNegotiator.getJingleContent());
843                        }
844                        // Send the "accept" and wait for the ACK
845                        addExpectedId(jout.getStanzaId());
846                        sendStanza(jout);
847                    }
848                }
849            }
850
851            @Override
852            public void transportClosed(TransportCandidate cand) {
853            }
854
855            @Override
856            public void transportClosedOnError(XMPPException e) {
857            }
858        };
859
860        addMediaListener(jingleMediaListener);
861        addTransportListener(jingleTransportListener);
862    }
863
864    // Triggers
865
866    /**
867     * Trigger a session closed event.
868     *
869     * @param reason the reason.
870     */
871    void triggerSessionClosed(String reason) {
872        //        for (ContentNegotiator contentNegotiator : contentNegotiators) {
873        //
874        //            contentNegotiator.stopJingleMediaSession();
875        //
876        //            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
877        //                candidate.removeCandidateEcho();
878        //        }
879
880        List<JingleListener> listeners = getListenersList();
881        for (JingleListener li : listeners) {
882            if (li instanceof JingleSessionListener) {
883                JingleSessionListener sli = (JingleSessionListener) li;
884                sli.sessionClosed(reason, this);
885            }
886        }
887        close();
888    }
889
890    /**
891     * Trigger a session closed event due to an error.
892     *
893     * @param exc the exception.
894     */
895    void triggerSessionClosedOnError(XMPPException exc) {
896        for (ContentNegotiator contentNegotiator : contentNegotiators) {
897
898            contentNegotiator.stopJingleMediaSession();
899
900            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
901                candidate.removeCandidateEcho();
902        }
903        List<JingleListener> listeners = getListenersList();
904        for (JingleListener li : listeners) {
905            if (li instanceof JingleSessionListener) {
906                JingleSessionListener sli = (JingleSessionListener) li;
907                sli.sessionClosedOnError(exc, this);
908            }
909        }
910        close();
911    }
912
913
914    /**
915     * Trigger a media received event.
916     *
917     * @param participant the participant.
918     */
919    void triggerMediaReceived(String participant) {
920        List<JingleListener> listeners = getListenersList();
921        for (JingleListener li : listeners) {
922            if (li instanceof JingleSessionListener) {
923                JingleSessionListener sli = (JingleSessionListener) li;
924                sli.sessionMediaReceived(this, participant);
925            }
926        }
927    }
928
929    /**
930     * Terminates the session with default reason.
931     *
932     * @throws XMPPException if an XMPP protocol error was received.
933     * @throws NotConnectedException if the XMPP connection is not connected.
934     * @throws InterruptedException if the calling thread was interrupted.
935     */
936    public void terminate() throws XMPPException, NotConnectedException, InterruptedException {
937        terminate("Closed Locally");
938    }
939
940    /**
941     * Terminates the session with a custom reason.
942     *
943     * @param reason the reason.
944     * @throws XMPPException if an XMPP protocol error was received.
945     * @throws NotConnectedException if the XMPP connection is not connected.
946     * @throws InterruptedException if the calling thread was interrupted.
947     */
948    public void terminate(String reason) throws XMPPException, NotConnectedException, InterruptedException {
949        if (isClosed())
950            return;
951        LOGGER.fine("Terminate " + reason);
952        Jingle jout = new Jingle(JingleActionEnum.SESSION_TERMINATE);
953        jout.setType(IQ.Type.set);
954        sendStanza(jout);
955        triggerSessionClosed(reason);
956    }
957
958    /**
959     * Terminate negotiations.
960     */
961    @Override
962    public void close() {
963        if (isClosed())
964            return;
965
966        // Set the session state to ENDED.
967        setSessionState(JingleSessionStateEnded.getInstance());
968
969        for (ContentNegotiator contentNegotiator : contentNegotiators) {
970
971            contentNegotiator.stopJingleMediaSession();
972
973            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
974                candidate.removeCandidateEcho();
975
976            contentNegotiator.close();
977        }
978        removeAsyncPacketListener();
979        removeConnectionListener();
980        getConnection().removeConnectionListener(connectionListener);
981        LOGGER.fine("Negotiation Closed: " + getConnection().getUser() + " " + sid);
982        super.close();
983
984    }
985
986    public boolean isClosed() {
987        return getSessionState().equals(JingleSessionStateEnded.getInstance());
988    }
989
990    // Packet and error creation
991
992    /**
993     * Complete and send an error. Complete all the null fields in an IQ error
994     * response, using the session information we have or some info from the
995     * incoming packet.
996     *
997     * @param iq
998     *            The Jingle stanza we are responding to
999     * @param jingleError
1000     *            the IQ stanza we want to complete and send
1001     * @return the jingle error IQ.
1002     */
1003    public IQ createJingleError(IQ iq, JingleError jingleError) {
1004        IQ errorPacket = null;
1005        if (jingleError != null) {
1006            // TODO This is wrong according to XEP-166 ยง 10, but this jingle implementation is deprecated anyways
1007            StanzaError builder = StanzaError.getBuilder()
1008                            .setCondition(StanzaError.Condition.undefined_condition)
1009                            .addExtension(jingleError)
1010                            .build();
1011
1012            errorPacket = IQ.createErrorResponse(iq, builder);
1013
1014            //            errorPacket.addExtension(jingleError);
1015
1016            // NO! Let the normal state machinery do all of the sending.
1017            // getConnection().sendStanza(perror);
1018            LOGGER.severe("Error sent: " + errorPacket.toXML());
1019        }
1020        return errorPacket;
1021    }
1022
1023    /**
1024     * Called when new Media is received.
1025     */
1026    @Override
1027    public void mediaReceived(String participant) {
1028        triggerMediaReceived(participant);
1029    }
1030
1031    /**
1032     * This is the starting point for initiating a new session.
1033     *
1034     * @throws IllegalStateException if an illegal state was encountered
1035     * @throws SmackException if Smack detected an exceptional situation.
1036     * @throws InterruptedException if the calling thread was interrupted.
1037     */
1038    public void startOutgoing() throws IllegalStateException, SmackException, InterruptedException {
1039
1040        updatePacketListener();
1041        setSessionState(JingleSessionStatePending.getInstance());
1042
1043        Jingle jingle = new Jingle(JingleActionEnum.SESSION_INITIATE);
1044
1045        // Create a content negotiator for each media manager on the session.
1046        for (JingleMediaManager mediaManager : getMediaManagers()) {
1047            ContentNegotiator contentNeg = new ContentNegotiator(this, ContentNegotiator.INITIATOR, mediaManager.getName());
1048
1049            // Create the media negotiator for this content description.
1050            contentNeg.setMediaNegotiator(new MediaNegotiator(this, mediaManager, mediaManager.getPayloads(), contentNeg));
1051
1052            JingleTransportManager transportManager = mediaManager.getTransportManager();
1053            TransportResolver resolver = null;
1054            try {
1055                resolver = transportManager.getResolver(this);
1056            } catch (XMPPException e) {
1057                LOGGER.log(Level.WARNING, "exception", e);
1058            }
1059
1060            if (resolver.getType().equals(TransportResolver.Type.rawupd)) {
1061                contentNeg.setTransportNegotiator(new TransportNegotiator.RawUdp(this, resolver, contentNeg));
1062            }
1063            if (resolver.getType().equals(TransportResolver.Type.ice)) {
1064                contentNeg.setTransportNegotiator(new TransportNegotiator.Ice(this, resolver, contentNeg));
1065            }
1066
1067            addContentNegotiator(contentNeg);
1068        }
1069
1070        // Give each of the content negotiators a chance to return a portion of the structure to make the Jingle packet.
1071        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1072            jingle.addContent(contentNegotiator.getJingleContent());
1073        }
1074
1075        // Save the session-initiate packet ID, so that we can respond to it.
1076        sessionInitPacketID = jingle.getStanzaId();
1077
1078        sendStanza(jingle);
1079
1080        // Now setup to track the media negotiators, so that we know when (if) to send a session-accept.
1081        setupListeners();
1082
1083        // Give each of the content negotiators a chance to start
1084        // and return a portion of the structure to make the Jingle packet.
1085
1086// Don't do this anymore.  The problem is that the other side might not be ready.
1087// Later when we receive our first jingle packet from the other side we'll fire-up the negotiators
1088// before processing it.  (See receivePacketAndRespond() above.
1089//        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1090//            contentNegotiator.start();
1091//        }
1092    }
1093
1094    /**
1095     *  This is the starting point for responding to a new session.
1096     */
1097    public void startIncoming() {
1098
1099        // updatePacketListener();
1100    }
1101
1102    @Override
1103    protected void doStart() {
1104
1105    }
1106
1107    /**
1108     * When we initiate a session we need to start a bunch of negotiators right after we receive the result
1109     * stanza for our session-initiate.  This is where we start them.
1110     *
1111     */
1112    private void startNegotiators() {
1113
1114        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1115            TransportNegotiator transNeg = contentNegotiator.getTransportNegotiator();
1116            transNeg.start();
1117        }
1118    }
1119
1120    /**
1121     * The jingle session may have one or more media managers that are trying to establish media sessions.
1122     * When the media manager succeeds in creating a media session is registers it with the session by the
1123     * media manager's static name.  This routine is where the media manager does the registering.
1124     *
1125     * @param mediaManagerName the name of the media manager.
1126     * @param mediaSession the jingle media session.
1127     */
1128    public void addJingleMediaSession(String mediaManagerName, JingleMediaSession mediaSession) {
1129        mediaSessionMap.put(mediaManagerName, mediaSession);
1130    }
1131
1132    /**
1133     * The jingle session may have one or more media managers that are trying to establish media sessions.
1134     * When the media manager succeeds in creating a media session is registers it with the session by the
1135     * media manager's static name. This routine is where other objects can access the registered media sessions.
1136     * NB: If the media manager has not succeeded in establishing a media session then this could return null.
1137     *
1138     * @param mediaManagerName the name of the media manager.
1139     * @return the jingle media session.
1140     */
1141    public JingleMediaSession getMediaSession(String mediaManagerName) {
1142        return mediaSessionMap.get(mediaManagerName);
1143    }
1144}