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