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     * reponse, 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     * reponse, 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     *  @param inJingle TODO javadoc me please
501     *  @param inAction TODO javadoc me please
502     */
503    //    private void sendUnknownStateAction(Jingle inJingle, JingleActionEnum inAction) {
504    //
505    //        if (inAction == JingleActionEnum.SESSION_INITIATE) {
506    //            // Prepare to receive and act on response packets.
507    //            updatePacketListener();
508    //
509    //            // Send the actual packet.
510    //            sendStanza(inJingle);
511    //
512    //            // Change to the PENDING state.
513    //            setSessionState(JingleSessionStateEnum.PENDING);
514    //        } else {
515    //            throw new IllegalStateException("Only session-initiate allowed in the UNKNOWN state.");
516    //        }
517    //    }
518
519    /**
520     * Acknowledge a IQ packet.
521     *
522     * @param iq The IQ to acknowledge.
523     * @return the ack IQ.
524     */
525    public IQ createAck(IQ iq) {
526        IQ result = null;
527
528        if (iq != null) {
529            // Don't acknowledge ACKs, errors...
530            if (iq.getType().equals(IQ.Type.set)) {
531                IQ ack = IQ.createResultIQ(iq);
532
533                // No! Don't send it.  Let it flow to the normal way IQ results get processed and sent.
534                // getConnection().sendStanza(ack);
535                result = ack;
536            }
537        }
538        return result;
539    }
540
541    /**
542     * Send a content info message.
543     */
544    //    public synchronized void sendContentInfo(ContentInfo ci) {
545    //        sendStanza(new Jingle(new JingleContentInfo(ci)));
546    //    }
547    /*
548     * (non-Javadoc)
549     *
550     * @see java.lang.Object#hashCode()
551     */
552    @Override
553    public int hashCode() {
554        return Jingle.getSessionHash(getSid(), getInitiator());
555    }
556
557    /*
558     * (non-Javadoc)
559     *
560     * @see java.lang.Object#equals(java.lang.Object)
561     */
562    @Override
563    public boolean equals(Object obj) {
564        if (this == obj) {
565            return true;
566        }
567        if (obj == null) {
568            return false;
569        }
570        if (getClass() != obj.getClass()) {
571            return false;
572        }
573
574        final JingleSession other = (JingleSession) obj;
575
576        if (initiator == null) {
577            if (other.initiator != null) {
578                return false;
579            }
580        } else if (!initiator.equals(other.initiator)) {
581            // Todo check behavior
582            // return false;
583        }
584
585        if (responder == null) {
586            if (other.responder != null) {
587                return false;
588            }
589        } else if (!responder.equals(other.responder)) {
590            return false;
591        }
592
593        if (sid == null) {
594            if (other.sid != null) {
595                return false;
596            }
597        } else if (!sid.equals(other.sid)) {
598            return false;
599        }
600
601        return true;
602    }
603
604    // Instances management
605
606    /**
607     * Clean a session from the list.
608     *
609     * @param connection TODO javadoc me please
610     *            The connection to clean up
611     */
612    private static void unregisterInstanceFor(XMPPConnection connection) {
613        synchronized (sessions) {
614            sessions.remove(connection);
615        }
616    }
617
618    /**
619     * Register this instance.
620     */
621    private void registerInstance() {
622        synchronized (sessions) {
623            sessions.put(getConnection(), this);
624        }
625    }
626
627    /**
628     * Returns the JingleSession related to a particular connection.
629     *
630     * @param con TODO javadoc me please
631     *            A XMPP connection
632     * @return a Jingle session
633     */
634    public static synchronized JingleSession getInstanceFor(XMPPConnection con) {
635        if (con == null) {
636            throw new IllegalArgumentException("XMPPConnection cannot be null");
637        }
638
639        JingleSession result = null;
640        synchronized (sessions) {
641            if (sessions.containsKey(con)) {
642                result = sessions.get(con);
643            }
644        }
645
646        return result;
647    }
648
649    /**
650     * Configure a session, setting some action listeners...
651     *
652     * @param connection TODO javadoc me please
653     *            The connection to set up
654     */
655    private void installConnectionListeners(final XMPPConnection connection) {
656        if (connection != null) {
657            connectionListener = new AbstractConnectionClosedListener() {
658                @Override
659                public void connectionTerminated() {
660                    unregisterInstanceFor(connection);
661                }
662            };
663            connection.addConnectionListener(connectionListener);
664        }
665    }
666
667    private void removeConnectionListener() {
668        // CHECKSTYLE:OFF
669        if (connectionListener != null) {
670            getConnection().removeConnectionListener(connectionListener);
671
672            LOGGER.fine("JINGLE SESSION: REMOVE CONNECTION LISTENER");
673        }
674        // CHECKSTYLE:ON
675    }
676
677    /**
678     * Remove the stanza listener used for processing packet.
679     */
680    void removeAsyncPacketListener() {
681        if (packetListener != null) {
682            getConnection().removeAsyncStanzaListener(packetListener);
683
684            LOGGER.fine("JINGLE SESSION: REMOVE PACKET LISTENER");
685        }
686    }
687
688    /**
689     * Install the stanza listener. The listener is responsible for responding
690     * to any stanza that we receive...
691     */
692    void updatePacketListener() {
693        removeAsyncPacketListener();
694
695        LOGGER.fine("UpdatePacketListener");
696
697        packetListener = new StanzaListener() {
698            @Override
699            public void processStanza(Stanza packet) {
700                try {
701                    receivePacketAndRespond((IQ) packet);
702                } catch (Exception e) {
703                    LOGGER.log(Level.WARNING, "exception", e);
704                }
705            }
706        };
707
708        packetFilter = new StanzaFilter() {
709            @Override
710            public boolean accept(Stanza packet) {
711
712                if (packet instanceof IQ) {
713                    IQ iq = (IQ) packet;
714
715                    Jid me = getConnection().getUser();
716
717                    if (!iq.getTo().equals(me)) {
718                        return false;
719                    }
720
721                    Jid other = getResponder().equals(me) ? getInitiator() : getResponder();
722
723                    if (iq.getFrom() == null || !iq.getFrom().equals(other == null ? "" : other)) {
724                        return false;
725                    }
726
727                    if (iq instanceof Jingle) {
728                        Jingle jin = (Jingle) iq;
729
730                        String sid = jin.getSid();
731                        if (sid == null || !sid.equals(getSid())) {
732                            LOGGER.fine("Ignored Jingle(SID) " + sid + "|" + getSid() + " :" + iq.toXML());
733                            return false;
734                        }
735                        Jid ini = jin.getInitiator();
736                        if (!ini.equals(getInitiator())) {
737                            LOGGER.fine("Ignored Jingle(INI): " + iq.toXML());
738                            return false;
739                        }
740                    } else {
741                        // We accept some non-Jingle IQ packets: ERRORs and ACKs
742                        if (iq.getType().equals(IQ.Type.set)) {
743                            LOGGER.fine("Ignored Jingle(TYPE): " + iq.toXML());
744                            return false;
745                        } else if (iq.getType().equals(IQ.Type.get)) {
746                            LOGGER.fine("Ignored Jingle(TYPE): " + iq.toXML());
747                            return false;
748                        }
749                    }
750                    return true;
751                }
752                return false;
753            }
754        };
755
756        getConnection().addAsyncStanzaListener(packetListener, packetFilter);
757    }
758
759    // Listeners
760
761    /**
762     * Add a listener for jmf negotiation events.
763     *
764     * @param li TODO javadoc me please
765     *            The listener
766     */
767    public void addMediaListener(JingleMediaListener li) {
768        for (ContentNegotiator contentNegotiator : contentNegotiators) {
769            if (contentNegotiator.getMediaNegotiator() != null) {
770                contentNegotiator.getMediaNegotiator().addListener(li);
771            }
772        }
773
774    }
775
776    /**
777     * Remove a listener for jmf negotiation events.
778     *
779     * @param li TODO javadoc me please
780     *            The listener
781     */
782    public void removeMediaListener(JingleMediaListener li) {
783        for (ContentNegotiator contentNegotiator : contentNegotiators) {
784            if (contentNegotiator.getMediaNegotiator() != null) {
785                contentNegotiator.getMediaNegotiator().removeListener(li);
786            }
787        }
788    }
789
790    /**
791     * Add a listener for transport negotiation events.
792     *
793     * @param li TODO javadoc me please
794     *            The listener
795     */
796    public void addTransportListener(JingleTransportListener li) {
797        for (ContentNegotiator contentNegotiator : contentNegotiators) {
798            if (contentNegotiator.getTransportNegotiator() != null) {
799                contentNegotiator.getTransportNegotiator().addListener(li);
800            }
801        }
802    }
803
804    /**
805     * Remove a listener for transport negotiation events.
806     *
807     * @param li TODO javadoc me please
808     *            The listener
809     */
810    public void removeTransportListener(JingleTransportListener li) {
811        for (ContentNegotiator contentNegotiator : contentNegotiators) {
812            if (contentNegotiator.getTransportNegotiator() != null) {
813                contentNegotiator.getTransportNegotiator().removeListener(li);
814            }
815        }
816    }
817
818    /**
819     * Setup the listeners that act on events coming from the lower level negotiators.
820     */
821
822    public void setupListeners() {
823
824        JingleMediaListener jingleMediaListener = new JingleMediaListener() {
825            @Override
826            public void mediaClosed(PayloadType cand) {
827            }
828
829            @Override
830            public void mediaEstablished(PayloadType pt) throws NotConnectedException, InterruptedException {
831                if (isFullyEstablished()) {
832                    Jingle jout = new Jingle(JingleActionEnum.SESSION_ACCEPT);
833
834                    // Build up a response packet from each media manager.
835                    for (ContentNegotiator contentNegotiator : contentNegotiators) {
836                        if (contentNegotiator.getNegotiatorState() == JingleNegotiatorState.SUCCEEDED)
837                            jout.addContent(contentNegotiator.getJingleContent());
838                    }
839                    // Send the "accept" and wait for the ACK
840                    addExpectedId(jout.getStanzaId());
841                    sendStanza(jout);
842
843                    // triggerSessionEstablished();
844
845                }
846            }
847        };
848
849        JingleTransportListener jingleTransportListener = new JingleTransportListener() {
850
851            @Override
852            public void transportEstablished(TransportCandidate local, TransportCandidate remote) throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException {
853                if (isFullyEstablished()) {
854                // CHECKSTYLE:OFF
855                    // Indicate that this session is active.
856                    setSessionState(JingleSessionStateActive.getInstance());
857
858                    for (ContentNegotiator contentNegotiator : contentNegotiators) {
859                // CHECKSTYLE:ON
860                        if (contentNegotiator.getNegotiatorState() == JingleNegotiatorState.SUCCEEDED)
861                            contentNegotiator.triggerContentEstablished();
862                    }
863
864                    if (getSessionState().equals(JingleSessionStatePending.getInstance())) {
865
866                        Jingle jout = new Jingle(JingleActionEnum.SESSION_ACCEPT);
867
868                        // Build up a response packet from each media manager.
869                        for (ContentNegotiator contentNegotiator : contentNegotiators) {
870                            if (contentNegotiator.getNegotiatorState() == JingleNegotiatorState.SUCCEEDED)
871                                jout.addContent(contentNegotiator.getJingleContent());
872                        }
873                        // Send the "accept" and wait for the ACK
874                        addExpectedId(jout.getStanzaId());
875                        sendStanza(jout);
876                    }
877                }
878            }
879
880            @Override
881            public void transportClosed(TransportCandidate cand) {
882            }
883
884            @Override
885            public void transportClosedOnError(XMPPException e) {
886            }
887        };
888
889        addMediaListener(jingleMediaListener);
890        addTransportListener(jingleTransportListener);
891    }
892
893    // Triggers
894
895    /**
896     * Trigger a session closed event.
897     *
898     * @param reason the reason.
899     */
900    void triggerSessionClosed(String reason) {
901        //        for (ContentNegotiator contentNegotiator : contentNegotiators) {
902        //
903        //            contentNegotiator.stopJingleMediaSession();
904        //
905        //            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
906        //                candidate.removeCandidateEcho();
907        //        }
908
909        List<JingleListener> listeners = getListenersList();
910        for (JingleListener li : listeners) {
911            if (li instanceof JingleSessionListener) {
912                JingleSessionListener sli = (JingleSessionListener) li;
913                sli.sessionClosed(reason, this);
914            }
915        }
916        close();
917    }
918
919    /**
920     * Trigger a session closed event due to an error.
921     *
922     * @param exc the exception.
923     */
924    void triggerSessionClosedOnError(XMPPException exc) {
925        for (ContentNegotiator contentNegotiator : contentNegotiators) {
926
927            contentNegotiator.stopJingleMediaSession();
928
929            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
930                candidate.removeCandidateEcho();
931        }
932        List<JingleListener> listeners = getListenersList();
933        for (JingleListener li : listeners) {
934            if (li instanceof JingleSessionListener) {
935                JingleSessionListener sli = (JingleSessionListener) li;
936                sli.sessionClosedOnError(exc, this);
937            }
938        }
939        close();
940    }
941
942    /**
943     * Trigger a session established event.
944     */
945    //    protected void triggerSessionEstablished() {
946    //        List<JingleListener> listeners = getListenersList();
947    //        for (JingleListener li : listeners) {
948    //            if (li instanceof JingleSessionListener) {
949    //                JingleSessionListener sli = (JingleSessionListener) li;
950    //                sli.sessionEstablished(this);
951    //            }
952    //        }
953    //    }
954    /**
955     * Trigger a media received event.
956     *
957     * @param participant the participant.
958     */
959    void triggerMediaReceived(String participant) {
960        List<JingleListener> listeners = getListenersList();
961        for (JingleListener li : listeners) {
962            if (li instanceof JingleSessionListener) {
963                JingleSessionListener sli = (JingleSessionListener) li;
964                sli.sessionMediaReceived(this, participant);
965            }
966        }
967    }
968
969    /**
970     * Trigger a session redirect event.
971     */
972    //    protected void triggerSessionRedirect(String arg) {
973    //        List<JingleListener> listeners = getListenersList();
974    //        for (JingleListener li : listeners) {
975    //            if (li instanceof JingleSessionListener) {
976    //                JingleSessionListener sli = (JingleSessionListener) li;
977    //                sli.sessionRedirected(arg, this);
978    //            }
979    //        }
980    //    }
981    /**
982     * Trigger a session decline event.
983     */
984    //    protected void triggerSessionDeclined(String reason) {
985    //        List<JingleListener> listeners = getListenersList();
986    //        for (JingleListener li : listeners) {
987    //            if (li instanceof JingleSessionListener) {
988    //                JingleSessionListener sli = (JingleSessionListener) li;
989    //                sli.sessionDeclined(reason, this);
990    //            }
991    //        }
992    //        for (ContentNegotiator contentNegotiator : contentNegotiators) {
993    //            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
994    //                candidate.removeCandidateEcho();
995    //        }
996    //    }
997    /**
998     * Terminates the session with default reason.
999     *
1000     * @throws XMPPException if an XMPP protocol error was received.
1001     * @throws NotConnectedException if the XMPP connection is not connected.
1002     * @throws InterruptedException if the calling thread was interrupted.
1003     */
1004    public void terminate() throws XMPPException, NotConnectedException, InterruptedException {
1005        terminate("Closed Locally");
1006    }
1007
1008    /**
1009     * Terminates the session with a custom reason.
1010     *
1011     * @param reason the reason.
1012     * @throws XMPPException if an XMPP protocol error was received.
1013     * @throws NotConnectedException if the XMPP connection is not connected.
1014     * @throws InterruptedException if the calling thread was interrupted.
1015     */
1016    public void terminate(String reason) throws XMPPException, NotConnectedException, InterruptedException {
1017        if (isClosed())
1018            return;
1019        LOGGER.fine("Terminate " + reason);
1020        Jingle jout = new Jingle(JingleActionEnum.SESSION_TERMINATE);
1021        jout.setType(IQ.Type.set);
1022        sendStanza(jout);
1023        triggerSessionClosed(reason);
1024    }
1025
1026    /**
1027     * Terminate negotiations.
1028     */
1029    @Override
1030    public void close() {
1031        if (isClosed())
1032            return;
1033
1034        // Set the session state to ENDED.
1035        setSessionState(JingleSessionStateEnded.getInstance());
1036
1037        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1038
1039            contentNegotiator.stopJingleMediaSession();
1040
1041            for (TransportCandidate candidate : contentNegotiator.getTransportNegotiator().getOfferedCandidates())
1042                candidate.removeCandidateEcho();
1043
1044            contentNegotiator.close();
1045        }
1046        removeAsyncPacketListener();
1047        removeConnectionListener();
1048        getConnection().removeConnectionListener(connectionListener);
1049        LOGGER.fine("Negotiation Closed: " + getConnection().getUser() + " " + sid);
1050        super.close();
1051
1052    }
1053
1054    public boolean isClosed() {
1055        return getSessionState().equals(JingleSessionStateEnded.getInstance());
1056    }
1057
1058    // Packet and error creation
1059
1060    /**
1061     * Complete and send an error. Complete all the null fields in an IQ error
1062     * response, using the session information we have or some info from the
1063     * incoming packet.
1064     *
1065     * @param iq
1066     *            The Jingle stanza we are responding to
1067     * @param jingleError
1068     *            the IQ stanza we want to complete and send
1069     * @return the jingle error IQ.
1070     */
1071    public IQ createJingleError(IQ iq, JingleError jingleError) {
1072        IQ errorPacket = null;
1073        if (jingleError != null) {
1074            // TODO This is wrong according to XEP-166 ยง 10, but this jingle implementation is deprecated anyways
1075            StanzaError builder = StanzaError.getBuilder()
1076                            .setCondition(StanzaError.Condition.undefined_condition)
1077                            .addExtension(jingleError)
1078                            .build();
1079
1080            errorPacket = IQ.createErrorResponse(iq, builder);
1081
1082            //            errorPacket.addExtension(jingleError);
1083
1084            // NO! Let the normal state machinery do all of the sending.
1085            // getConnection().sendStanza(perror);
1086            LOGGER.severe("Error sent: " + errorPacket.toXML());
1087        }
1088        return errorPacket;
1089    }
1090
1091    /**
1092     * Called when new Media is received.
1093     */
1094    @Override
1095    public void mediaReceived(String participant) {
1096        triggerMediaReceived(participant);
1097    }
1098
1099    /**
1100     * This is the starting point for intitiating a new session.
1101     *
1102     * @throws IllegalStateException if an illegal state was encountered
1103     * @throws SmackException if Smack detected an exceptional situation.
1104     * @throws InterruptedException if the calling thread was interrupted.
1105     */
1106    public void startOutgoing() throws IllegalStateException, SmackException, InterruptedException {
1107
1108        updatePacketListener();
1109        setSessionState(JingleSessionStatePending.getInstance());
1110
1111        Jingle jingle = new Jingle(JingleActionEnum.SESSION_INITIATE);
1112
1113        // Create a content negotiator for each media manager on the session.
1114        for (JingleMediaManager mediaManager : getMediaManagers()) {
1115            ContentNegotiator contentNeg = new ContentNegotiator(this, ContentNegotiator.INITIATOR, mediaManager.getName());
1116
1117            // Create the media negotiator for this content description.
1118            contentNeg.setMediaNegotiator(new MediaNegotiator(this, mediaManager, mediaManager.getPayloads(), contentNeg));
1119
1120            JingleTransportManager transportManager = mediaManager.getTransportManager();
1121            TransportResolver resolver = null;
1122            try {
1123                resolver = transportManager.getResolver(this);
1124            } catch (XMPPException e) {
1125                LOGGER.log(Level.WARNING, "exception", e);
1126            }
1127
1128            if (resolver.getType().equals(TransportResolver.Type.rawupd)) {
1129                contentNeg.setTransportNegotiator(new TransportNegotiator.RawUdp(this, resolver, contentNeg));
1130            }
1131            if (resolver.getType().equals(TransportResolver.Type.ice)) {
1132                contentNeg.setTransportNegotiator(new TransportNegotiator.Ice(this, resolver, contentNeg));
1133            }
1134
1135            addContentNegotiator(contentNeg);
1136        }
1137
1138        // Give each of the content negotiators a chance to return a portion of the structure to make the Jingle packet.
1139        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1140            jingle.addContent(contentNegotiator.getJingleContent());
1141        }
1142
1143        // Save the session-initiate packet ID, so that we can respond to it.
1144        sessionInitPacketID = jingle.getStanzaId();
1145
1146        sendStanza(jingle);
1147
1148        // Now setup to track the media negotiators, so that we know when (if) to send a session-accept.
1149        setupListeners();
1150
1151        // Give each of the content negotiators a chance to start
1152        // and return a portion of the structure to make the Jingle packet.
1153
1154// Don't do this anymore.  The problem is that the other side might not be ready.
1155// Later when we receive our first jingle packet from the other side we'll fire-up the negotiators
1156// before processing it.  (See receivePacketAndRespond() above.
1157//        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1158//            contentNegotiator.start();
1159//        }
1160    }
1161
1162    /**
1163     *  This is the starting point for responding to a new session.
1164     */
1165    public void startIncoming() {
1166
1167        // updatePacketListener();
1168    }
1169
1170    @Override
1171    protected void doStart() {
1172
1173    }
1174
1175    /**
1176     * When we initiate a session we need to start a bunch of negotiators right after we receive the result
1177     * stanza for our session-initiate.  This is where we start them.
1178     *
1179     */
1180    private void startNegotiators() {
1181
1182        for (ContentNegotiator contentNegotiator : contentNegotiators) {
1183            TransportNegotiator transNeg = contentNegotiator.getTransportNegotiator();
1184            transNeg.start();
1185        }
1186    }
1187
1188    /**
1189     * The jingle session may have one or more media managers that are trying to establish media sessions.
1190     * When the media manager succeeds in creating a media session is registers it with the session by the
1191     * media manager's static name.  This routine is where the media manager does the registering.
1192     *
1193     * @param mediaManagerName the name of the media manager.
1194     * @param mediaSession the jingle media session.
1195     */
1196    public void addJingleMediaSession(String mediaManagerName, JingleMediaSession mediaSession) {
1197        mediaSessionMap.put(mediaManagerName, mediaSession);
1198    }
1199
1200    /**
1201     * The jingle session may have one or more media managers that are trying to establish media sessions.
1202     * When the media manager succeeds in creating a media session is registers it with the session by the
1203     * media manager's static name. This routine is where other objects can access the registered media sessions.
1204     * NB: If the media manager has not succeeded in establishing a media session then this could return null.
1205     *
1206     * @param mediaManagerName the name of the media manager.
1207     * @return the jingle media session.
1208     */
1209    public JingleMediaSession getMediaSession(String mediaManagerName) {
1210        return mediaSessionMap.get(mediaManagerName);
1211    }
1212}