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