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 198 * @throws XMPPException 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 307 * @throws InterruptedException 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 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 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 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 */ 521 public JingleSession createOutgoingJingleSession(EntityFullJid responder) throws XMPPException { 522 JingleSession session = new JingleSession(connection, null, connection.getUser(), responder, jingleMediaManagers); 523 524 triggerSessionCreated(session); 525 526 return session; 527 } 528 529 /** 530 * Creates an Jingle session to start a communication with another user. 531 * 532 * @param responder the fully qualified jabber ID with resource of the other 533 * user. 534 * @return the session on which the negotiation can be run. 535 */ 536 // public OutgoingJingleSession createOutgoingJingleSession(String responder) throws XMPPException { 537 // if (this.getMediaManagers() == null) return null; 538 // return createOutgoingJingleSession(responder, this.getMediaManagers()); 539 // } 540 /** 541 * When the session request is acceptable, this method should be invoked. It 542 * will create an JingleSession which allows the negotiation to procede. 543 * 544 * @param request the remote request that is being accepted. 545 * @return the session which manages the rest of the negotiation. 546 */ 547 public JingleSession createIncomingJingleSession(JingleSessionRequest request) throws XMPPException { 548 if (request == null) { 549 throw new NullPointerException("Received request cannot be null"); 550 } 551 552 JingleSession session = new JingleSession(connection, request, request.getFrom(), connection.getUser(), jingleMediaManagers); 553 554 triggerSessionCreated(session); 555 556 return session; 557 } 558 559 /** 560 * When the session request is acceptable, this method should be invoked. It 561 * will create an JingleSession which allows the negotiation to procede. 562 * This method use JingleMediaManager to select the supported Payload types. 563 * 564 * @param request the remote request that is being accepted. 565 * @return the session which manages the rest of the negotiation. 566 */ 567 // IncomingJingleSession createIncomingJingleSession(JingleSessionRequest request) throws XMPPException { 568 // if (request == null) { 569 // throw new NullPointerException("JingleMediaManager is not defined"); 570 // } 571 // if (jingleMediaManager != null) 572 // return createIncomingJingleSession(request, jingleMediaManager.getPayloads()); 573 // 574 // return createIncomingJingleSession(request, null); 575 // } 576 /** 577 * Get a session with the informed JID. If no session is found, return null. 578 * 579 * @param jid 580 * @return the JingleSession 581 */ 582 public JingleSession getSession(String jid) { 583 for (JingleSession jingleSession : jingleSessions) { 584 if (jingleSession.getResponder().equals(jid)) { 585 return jingleSession; 586 } 587 } 588 return null; 589 } 590}