001/**
002 *
003 * Copyright 2003-2005 Jive Software.
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.Collection;
021import java.util.List;
022import java.util.logging.Level;
023import java.util.logging.Logger;
024
025import org.jivesoftware.smack.ConnectionCreationListener;
026import org.jivesoftware.smack.SmackException;
027import org.jivesoftware.smack.StanzaListener;
028import org.jivesoftware.smack.XMPPConnection;
029import org.jivesoftware.smack.XMPPConnectionRegistry;
030import org.jivesoftware.smack.XMPPException;
031import org.jivesoftware.smack.filter.StanzaFilter;
032import org.jivesoftware.smack.packet.IQ;
033import org.jivesoftware.smack.packet.Presence;
034import org.jivesoftware.smack.packet.Stanza;
035import org.jivesoftware.smack.provider.ProviderManager;
036import org.jivesoftware.smack.roster.Roster;
037import org.jivesoftware.smack.roster.RosterListener;
038
039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
040import org.jivesoftware.smackx.jingleold.listeners.CreatedJingleSessionListener;
041import org.jivesoftware.smackx.jingleold.listeners.JingleListener;
042import org.jivesoftware.smackx.jingleold.listeners.JingleSessionListener;
043import org.jivesoftware.smackx.jingleold.listeners.JingleSessionRequestListener;
044import org.jivesoftware.smackx.jingleold.media.JingleMediaManager;
045import org.jivesoftware.smackx.jingleold.media.PayloadType;
046import org.jivesoftware.smackx.jingleold.nat.BasicTransportManager;
047import org.jivesoftware.smackx.jingleold.nat.TransportCandidate;
048import org.jivesoftware.smackx.jingleold.nat.TransportResolver;
049import org.jivesoftware.smackx.jingleold.packet.Jingle;
050import org.jivesoftware.smackx.jingleold.provider.JingleProvider;
051
052import org.jxmpp.jid.EntityFullJid;
053import org.jxmpp.jid.Jid;
054
055/**
056 * Jingle is a session establishment protocol defined in (XEP-0166).
057 * It defines a framework for negotiating and managing out-of-band ( data that is send and receive through other connection than XMPP connection) data sessions over XMPP.
058 * With this protocol you can setup VOIP Calls, Video Streaming, File transfers and whatever out-of-band session based transmission.
059 * <p/>
060 * To create a Jingle Session you need a Transport method and a Payload type.
061 * <p/>
062 * A transport method is how it will trasmit and receive network packets. Transport MUST have one or more candidates.
063 * A transport candidate is an IP Address with a defined port, that other party must send data to.
064 * <p/>
065 * A supported payload type, is the data encoding format that the jmf will be transmitted.
066 * For instance an Audio Payload "GSM".
067 * <p/>
068 * A Jingle session negociates a payload type and a pair of transport candidates.
069 * Which means that when a Jingle Session is establhished you will have two defined transport candidates with addresses
070 * and a defined Payload type.
071 * In other words, you will have two IP address with their respective ports, and a Codec type defined.
072 * <p/>
073 * The JingleManager is a facade built upon Jabber Jingle (XEP-166) to allow the
074 * use of Jingle. This implementation allows the user to simply
075 * use this class for setting the Jingle parameters, create and receive Jingle Sessions.
076 * <p/>
077 * In order to use the Jingle, the user must provide a
078 * TransportManager that will handle the resolution of potential IP addresses taht can be used to transport the streaming (jmf).
079 * This TransportManager can be initialized with several default resolvers,
080 * including a fixed solver that can be used when the address and port are know
081 * in advance.
082 * This API have ready to use Transport Managers, for instance: BasicTransportManager, STUNTransportManager, BridgedTransportManager.
083 * <p/>
084 * You should also especify a JingleMediaManager if you want that JingleManager assume Media control
085 * Using a JingleMediaManager implementation is the easier way to implement a Jingle Application.
086 * <p/>
087 * Otherwise before creating an outgoing connection, the user must create jingle session
088 * listeners that will be called when different events happen. The most
089 * important event is <i>sessionEstablished()</i>, that will be called when all
090 * the negotiations are finished, providing the payload type for the
091 * transmission as well as the remote and local addresses and ports for the
092 * communication. See JingleSessionListener for a complete list of events that can be
093 * observed.
094 * <p/>
095 * This is an example of how to use the JingleManager:
096 * <i>This example implements a Jingle VOIP Call between two users.</i>
097 * <p/>
098 * <pre>
099 * <p/>
100 *                               To wait for an Incoming Jingle Session:
101 * <p/>
102 *                               try {
103 * <p/>
104 *                                           // Connect to an XMPP Server
105 *                                           XMPPConnection x1 = new XMPPTCPConnection("xmpp.com");
106 *                                           x1.connect();
107 *                                           x1.login("juliet", "juliet");
108 * <p/>
109 *                                           // Create a JingleManager using a BasicResolver
110 *                                           final JingleManager jm1 = new JingleManager(
111 *                                                   x1, new BasicTransportManager());
112 * <p/>
113 *                                           // Create a JingleMediaManager. In this case using Jingle Audio Media API
114 *                                           JingleMediaManager jingleMediaManager = new AudioMediaManager();
115 * <p/>
116 *                                           // Set the JingleMediaManager
117 *                                           jm1.setMediaManager(jingleMediaManager);
118 * <p/>
119 *                                           // Listen for incoming calls
120 *                                           jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() {
121 *                                               public void sessionRequested(JingleSessionRequest request) {
122 * <p/>
123 *                                                   try {
124 *                                                      // Accept the call
125 *                                                      IncomingJingleSession session = request.accept();
126 * <p/>
127 * <p/>
128 *                                                       // Start the call
129 *                                                       session.start();
130 *                                                   } catch (XMPPException e) {
131 *                                                       LOGGER.log(Level.WARNING, "exception", e);
132 *                                                   }
133 * <p/>
134 *                                               }
135 *                                           });
136 * <p/>
137 *                                       Thread.sleep(15000);
138 * <p/>
139 *                                       } catch (Exception e) {
140 *                                           LOGGER.log(Level.WARNING, "exception", e);
141 *                                       }
142 * <p/>
143 *                               To create an Outgoing Jingle Session:
144 * <p/>
145 *                                     try {
146 * <p/>
147 *                                           // Connect to an XMPP Server
148 *                                           XMPPConnection x0 = new XMPPTCPConnection("xmpp.com");
149 *                                           x0.connect();
150 *                                           x0.login("romeo", "romeo");
151 * <p/>
152 *                                           // Create a JingleManager using a BasicResolver
153 *                                           final JingleManager jm0 = new JingleManager(
154 *                                                   x0, new BasicTransportManager());
155 * <p/>
156 *                                           // Create a JingleMediaManager. In this case using Jingle Audio Media API
157 *                                           JingleMediaManager jingleMediaManager = new AudioMediaManager(); // Using Jingle Media API
158 * <p/>
159 *                                           // Set the JingleMediaManager
160 *                                           jm0.setMediaManager(jingleMediaManager);
161 * <p/>
162 *                                           // Create a new Jingle Call with a full JID
163 *                                           OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("juliet@xmpp.com/Smack");
164 * <p/>
165 *                                           // Start the call
166 *                                           js0.start();
167 * <p/>
168 *                                           Thread.sleep(10000);
169 *                                           js0.terminate();
170 * <p/>
171 *                                           Thread.sleep(3000);
172 * <p/>
173 *                                       } catch (Exception e) {
174 *                                           LOGGER.log(Level.WARNING, "exception", e);
175 *                                       }
176 *                               </pre>
177 *
178 * @author Thiago Camargo
179 * @author Alvaro Saurin
180 * @author Jeff Williams
181 * @see JingleListener
182 * @see TransportResolver
183 * @see JingleSession
184 * @see JingleSession
185 * @see JingleMediaManager
186 * @see BasicTransportManager , STUNTransportManager, BridgedTransportManager, TransportResolver, BridgedResolver, ICEResolver, STUNResolver and BasicResolver.
187 */
188@SuppressWarnings("SynchronizeOnNonFinalField")
189public class JingleManager implements JingleSessionListener {
190
191    private static final Logger LOGGER = Logger.getLogger(JingleManager.class.getName());
192
193    // non-static
194
195    final List<JingleSession> jingleSessions = new ArrayList<JingleSession>();
196
197    // Listeners for manager events (ie, session requests...)
198    private List<JingleSessionRequestListener> jingleSessionRequestListeners;
199
200    // Listeners for created JingleSessions
201    private List<CreatedJingleSessionListener> creationListeners = new ArrayList<CreatedJingleSessionListener>();
202
203    // The XMPP connection
204    private XMPPConnection connection;
205
206    // The Media Managers
207    private List<JingleMediaManager> jingleMediaManagers;
208
209     /**
210     * Default constructor with a defined XMPPConnection, Transport Resolver and a Media Manager.
211     * If a fully implemented JingleMediaSession is entered, JingleManager manage Jingle signalling and jmf
212     *
213     * @param connection             XMPP XMPPConnection to be used
214     * @param jingleMediaManagers     an implemeted JingleMediaManager to be used.
215     * @throws SmackException 
216     * @throws XMPPException 
217     */
218    public JingleManager(XMPPConnection connection, List<JingleMediaManager> jingleMediaManagers) throws XMPPException, SmackException {
219        this.connection = connection;
220        this.jingleMediaManagers = jingleMediaManagers;
221
222        Roster.getInstanceFor(connection).addRosterListener(new RosterListener() {
223
224            @Override
225            public void entriesAdded(Collection<Jid> addresses) {
226            }
227
228            @Override
229            public void entriesUpdated(Collection<Jid> addresses) {
230            }
231
232            @Override
233            public void entriesDeleted(Collection<Jid> addresses) {
234            }
235
236            @Override
237            public void presenceChanged(Presence presence) {
238                if (!presence.isAvailable()) {
239                    Jid xmppAddress = presence.getFrom();
240                    JingleSession aux = null;
241                    for (JingleSession jingleSession : jingleSessions) {
242                        if (jingleSession.getInitiator().equals(xmppAddress) || jingleSession.getResponder().equals(xmppAddress)) {
243                            aux = jingleSession;
244                        }
245                    }
246                    if (aux != null)
247                        try {
248                            aux.terminate();
249                        } catch (Exception e) {
250                            LOGGER.log(Level.WARNING, "exception", e);
251                        }
252                }
253            }
254        });
255
256    }
257
258
259    /**
260     * Setup the jingle system to let the remote clients know we support Jingle.
261     * (This used to be a static part of construction.  The problem is a remote client might
262     * attempt a Jingle connection to us after we've created an XMPPConnection, but before we've
263     * setup an instance of a JingleManager.  We will appear to not support Jingle.  With the new
264     * method you just call it once and all new connections will report Jingle support.)
265     */
266    public static void setJingleServiceEnabled() {
267        ProviderManager.addIQProvider("jingle", "urn:xmpp:tmp:jingle", new JingleProvider());
268
269        // Enable the Jingle support on every established connection
270        // The ServiceDiscoveryManager class should have been already
271        // initialized
272        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
273            @Override
274            public void connectionCreated(XMPPConnection connection) {
275                JingleManager.setServiceEnabled(connection, true);
276            }
277        });
278    }
279
280    /**
281     * Enables or disables the Jingle support on a given connection.
282     * <p/>
283     * <p/>
284     * Before starting any Jingle jmf session, check that the user can handle
285     * it. Enable the Jingle support to indicate that this client handles Jingle
286     * messages.
287     *
288     * @param connection the connection where the service will be enabled or
289     *                   disabled
290     * @param enabled    indicates if the service will be enabled or disabled
291     */
292    public synchronized static void setServiceEnabled(XMPPConnection connection, boolean enabled) {
293        if (isServiceEnabled(connection) == enabled) {
294            return;
295        }
296
297        if (enabled) {
298            ServiceDiscoveryManager.getInstanceFor(connection).addFeature(Jingle.NAMESPACE);
299        } else {
300            ServiceDiscoveryManager.getInstanceFor(connection).removeFeature(Jingle.NAMESPACE);
301        }
302    }
303
304    /**
305     * Returns true if the Jingle support is enabled for the given connection.
306     *
307     * @param connection the connection to look for Jingle support
308     * @return a boolean indicating if the Jingle support is enabled for the
309     *         given connection
310     */
311    public static boolean isServiceEnabled(XMPPConnection connection) {
312        return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(Jingle.NAMESPACE);
313    }
314
315    /**
316     * Returns true if the specified user handles Jingle messages.
317     *
318     * @param connection the connection to use to perform the service discovery
319     * @param userID     the user to check. A fully qualified xmpp ID, e.g.
320     *                   jdoe@example.com
321     * @return a boolean indicating whether the specified user handles Jingle
322     *         messages
323     * @throws SmackException if there was no response from the server.
324     * @throws XMPPException 
325     * @throws InterruptedException 
326     */
327    public static boolean isServiceEnabled(XMPPConnection connection, Jid userID) throws XMPPException, SmackException, InterruptedException {
328            return ServiceDiscoveryManager.getInstanceFor(connection).supportsFeature(userID, Jingle.NAMESPACE);
329    }
330
331    /**
332     * Get the Media Managers of this Jingle Manager.
333     *
334     * @return the list of JingleMediaManagers
335     */
336    public List<JingleMediaManager> getMediaManagers() {
337        return jingleMediaManagers;
338    }
339
340    /**
341     * Set the Media Managers of this Jingle Manager.
342     *
343     * @param jingleMediaManagers JingleMediaManager to be used for open, close, start and stop jmf streamings
344     */
345    public void setMediaManagers(List<JingleMediaManager> jingleMediaManagers) {
346        this.jingleMediaManagers = jingleMediaManagers;
347    }
348
349    /**
350    * Add a Jingle session request listenerJingle to listen to incoming session
351    * requests.
352    *
353    * @param jingleSessionRequestListener an implemented JingleSessionRequestListener
354    * @see #removeJingleSessionRequestListener(JingleSessionRequestListener)
355    * @see JingleListener
356    */
357    public synchronized void addJingleSessionRequestListener(final JingleSessionRequestListener jingleSessionRequestListener) {
358        if (jingleSessionRequestListener != null) {
359            if (jingleSessionRequestListeners == null) {
360                initJingleSessionRequestListeners();
361            }
362            synchronized (jingleSessionRequestListeners) {
363                jingleSessionRequestListeners.add(jingleSessionRequestListener);
364            }
365        }
366    }
367
368    /**
369     * Removes a Jingle session listenerJingle.
370     *
371     * @param jingleSessionRequestListener The jingle session jingleSessionRequestListener to be removed
372     * @see #addJingleSessionRequestListener(JingleSessionRequestListener)
373     * @see JingleListener
374     */
375    public void removeJingleSessionRequestListener(JingleSessionRequestListener jingleSessionRequestListener) {
376        if (jingleSessionRequestListeners == null) {
377            return;
378        }
379        synchronized (jingleSessionRequestListeners) {
380            jingleSessionRequestListeners.remove(jingleSessionRequestListener);
381        }
382    }
383
384    /**
385     * Adds a CreatedJingleSessionListener.
386     * This listener will be called when a session is created by the JingleManager instance.
387     *
388     * @param createdJingleSessionListener
389     */
390    public void addCreationListener(CreatedJingleSessionListener createdJingleSessionListener) {
391        this.creationListeners.add(createdJingleSessionListener);
392    }
393
394    /**
395     * Removes a CreatedJingleSessionListener.
396     * This listener will be called when a session is created by the JingleManager instance.
397     *
398     * @param createdJingleSessionListener
399     */
400    public void removeCreationListener(CreatedJingleSessionListener createdJingleSessionListener) {
401        this.creationListeners.remove(createdJingleSessionListener);
402    }
403
404    /**
405     * Trigger CreatedJingleSessionListeners that a session was created.
406     *
407     * @param jingleSession
408     */
409    public void triggerSessionCreated(JingleSession jingleSession) {
410        jingleSessions.add(jingleSession);
411        jingleSession.addListener(this);
412        for (CreatedJingleSessionListener createdJingleSessionListener : creationListeners) {
413            try {
414                createdJingleSessionListener.sessionCreated(jingleSession);
415            } catch (Exception e) {
416                LOGGER.log(Level.WARNING, "exception", e);
417            }
418        }
419    }
420
421    @Override
422    public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) {
423    }
424
425    @Override
426    public void sessionDeclined(String reason, JingleSession jingleSession) {
427        jingleSession.removeListener(this);
428        jingleSessions.remove(jingleSession);
429        jingleSession.close();
430        LOGGER.severe("Declined:" + reason);
431    }
432
433    @Override
434    public void sessionRedirected(String redirection, JingleSession jingleSession) {
435        jingleSession.removeListener(this);
436        jingleSessions.remove(jingleSession);
437    }
438
439    @Override
440    public void sessionClosed(String reason, JingleSession jingleSession) {
441        jingleSession.removeListener(this);
442        jingleSessions.remove(jingleSession);
443    }
444
445    @Override
446    public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) {
447        jingleSession.removeListener(this);
448        jingleSessions.remove(jingleSession);
449    }
450
451    @Override
452    public void sessionMediaReceived(JingleSession jingleSession, String participant) {
453        // Do Nothing
454    }
455
456    /**
457     * Register the listenerJingles, waiting for a Jingle stanza(/packet) that tries to
458     * establish a new session.
459     */
460    private void initJingleSessionRequestListeners() {
461        StanzaFilter initRequestFilter = new StanzaFilter() {
462            // Return true if we accept this packet
463            @Override
464            public boolean accept(Stanza pin) {
465                if (pin instanceof IQ) {
466                    IQ iq = (IQ) pin;
467                    if (iq.getType().equals(IQ.Type.set)) {
468                        if (iq instanceof Jingle) {
469                            Jingle jin = (Jingle) pin;
470                            if (jin.getAction().equals(JingleActionEnum.SESSION_INITIATE)) {
471                                return true;
472                            }
473                        }
474                    }
475                }
476                return false;
477            }
478        };
479
480        jingleSessionRequestListeners = new ArrayList<JingleSessionRequestListener>();
481
482        // Start a packet listener for session initiation requests
483        connection.addAsyncStanzaListener(new StanzaListener() {
484            @Override
485            public void processStanza(Stanza packet) {
486                triggerSessionRequested((Jingle) packet);
487            }
488        }, initRequestFilter);
489    }
490
491    /**
492     * Disconnect all Jingle Sessions.
493     */
494    public void disconnectAllSessions() {
495
496        List<JingleSession> sessions = jingleSessions.subList(0, jingleSessions.size());
497
498        for (JingleSession jingleSession : sessions)
499            try {
500                jingleSession.terminate();
501            } catch (Exception e) {
502                LOGGER.log(Level.WARNING, "exception", e);
503            }
504
505        sessions.clear();
506    }
507
508    /**
509     * Activates the listenerJingles on a Jingle session request.
510     *
511     * @param initJin the stanza(/packet) that must be passed to the jingleSessionRequestListener.
512     */
513    void triggerSessionRequested(Jingle initJin) {
514
515        JingleSessionRequestListener[] jingleSessionRequestListeners = null;
516
517        // Make a synchronized copy of the listenerJingles
518        synchronized (this.jingleSessionRequestListeners) {
519            jingleSessionRequestListeners = new JingleSessionRequestListener[this.jingleSessionRequestListeners.size()];
520            this.jingleSessionRequestListeners.toArray(jingleSessionRequestListeners);
521        }
522
523        // ... and let them know of the event
524        JingleSessionRequest request = new JingleSessionRequest(this, initJin);
525        for (int i = 0; i < jingleSessionRequestListeners.length; i++) {
526            jingleSessionRequestListeners[i].sessionRequested(request);
527        }
528    }
529
530    // Session creation
531
532    /**
533     * Creates an Jingle session to start a communication with another user.
534     *
535     * @param responder    the fully qualified jabber ID with resource of the other
536     *                     user.
537     * @return The session on which the negotiation can be run.
538     */
539    public JingleSession createOutgoingJingleSession(EntityFullJid responder) throws XMPPException {
540        JingleSession session = new JingleSession(connection, (JingleSessionRequest) null, connection.getUser(), responder, jingleMediaManagers);
541
542        triggerSessionCreated(session);
543
544        return session;
545    }
546
547    /**
548     * Creates an Jingle session to start a communication with another user.
549     *
550     * @param responder the fully qualified jabber ID with resource of the other
551     *                  user.
552     * @return the session on which the negotiation can be run.
553     */
554    //    public OutgoingJingleSession createOutgoingJingleSession(String responder) throws XMPPException {
555    //        if (this.getMediaManagers() == null) return null;
556    //        return createOutgoingJingleSession(responder, this.getMediaManagers());
557    //    }
558    /**
559     * When the session request is acceptable, this method should be invoked. It
560     * will create an JingleSession which allows the negotiation to procede.
561     *
562     * @param request      the remote request that is being accepted.
563     * @return the session which manages the rest of the negotiation.
564     */
565    public JingleSession createIncomingJingleSession(JingleSessionRequest request) throws XMPPException {
566        if (request == null) {
567            throw new NullPointerException("Received request cannot be null");
568        }
569
570        JingleSession session = new JingleSession(connection, request, request.getFrom(), connection.getUser(), jingleMediaManagers);
571
572        triggerSessionCreated(session);
573
574        return session;
575    }
576
577    /**
578     * When the session request is acceptable, this method should be invoked. It
579     * will create an JingleSession which allows the negotiation to procede.
580     * This method use JingleMediaManager to select the supported Payload types.
581     *
582     * @param request the remote request that is being accepted.
583     * @return the session which manages the rest of the negotiation.
584     */
585    //    IncomingJingleSession createIncomingJingleSession(JingleSessionRequest request) throws XMPPException {
586    //        if (request == null) {
587    //            throw new NullPointerException("JingleMediaManager is not defined");
588    //        }
589    //        if (jingleMediaManager != null)
590    //            return createIncomingJingleSession(request, jingleMediaManager.getPayloads());
591    //
592    //        return createIncomingJingleSession(request, null);
593    //    }
594    /**
595     * Get a session with the informed JID. If no session is found, return null.
596     *
597     * @param jid
598     * @return the JingleSession
599     */
600    public JingleSession getSession(String jid) {
601        for (JingleSession jingleSession : jingleSessions) {
602            if (jingleSession.getResponder().equals(jid)) {
603                return jingleSession;
604            }
605        }
606        return null;
607    }
608}