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