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