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}