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