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