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