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