/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.jabber; import java.util.*; import org.jivesoftware.smack.*; import org.jivesoftware.smackx.*; import org.jivesoftware.smackx.jingle.*; import org.jivesoftware.smackx.jingle.media.*; import org.jivesoftware.smackx.jingle.listeners.*; import org.jivesoftware.smackx.jingle.nat.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.OperationSetBasicTelephony.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.impl.protocol.jabber.mediamgr.*; import org.jivesoftware.smackx.packet.DiscoverInfo; /** * Implements all call management logic and exports basic telephony support by * implementing OperationSetBasicTelephony. * * @author Symphorien Wanko */ public class OperationSetBasicTelephonyJabberImpl extends AbstractOperationSetBasicTelephony implements RegistrationStateChangeListener, JingleMediaListener, JingleTransportListener, JingleSessionRequestListener, CreatedJingleSessionListener, JingleSessionStateListener, JingleSessionListener { /** * The logger used by this class */ private static final Logger logger = Logger.getLogger(OperationSetBasicTelephonyJabberImpl.class); /** * A reference to the ProtocolProviderServiceJabberImpl instance * that created us. */ private ProtocolProviderServiceJabberImpl protocolProvider = null; /** * Contains references for all currently active (non ended) calls. */ private ActiveCallsRepository activeCallsRepository = new ActiveCallsRepository(this); /** * The manager we use to initiate, receive and ... manage jingle session. */ private JingleManager jingleManager = null; /** * The transport manager is used by the jingleManager to handle transport * method. */ private JingleTransportManager transportManager = null; /** * The media manager is used by the jingleManager to handle media * session. */ private JingleMediaManager mediaManager = null; /** * Creates a new instance. * * @param protocolProvider a reference to the * ProtocolProviderServiceJabberImpl instance that created us. */ public OperationSetBasicTelephonyJabberImpl( ProtocolProviderServiceJabberImpl protocolProvider) { this.protocolProvider = protocolProvider; protocolProvider.addRegistrationStateChangeListener(this); transportManager = new BasicTransportManager(); mediaManager = new JingleScMediaManager(); } /** * Implementation of method registrationStateChange from * interface RegistrationStateChangeListener for setting up (or down) * our JingleManager when an XMPPConnection is available * * @param evt the event received */ public void registrationStateChanged(RegistrationStateChangeEvent evt) { if ((evt.getNewState() == RegistrationState.REGISTERED)) { transportManager = new ICETransportManager( protocolProvider.getConnection(), "stun.iptel.org", 3478); jingleManager = new JingleManager( protocolProvider.getConnection(), transportManager, mediaManager); jingleManager.addCreationListener(this); jingleManager.addJingleSessionRequestListener(this); logger.info("Jingle : ON "); } else if ((evt.getNewState() == RegistrationState.UNREGISTERED)) { if (jingleManager != null) { jingleManager.removeCreationListener(this); jingleManager.removeJingleSessionRequestListener(this); jingleManager = null; logger.info("Jingle : OFF "); } } } /** * Create a new call and invite the specified CallParticipant to it. * * @param callee the jabber address of the callee that we should invite to a * new call. * @return CallParticipant the CallParticipant that will represented by * the specified uri. All following state change events will be * delivered through that call participant. The Call that this * participant is a member of could be retrieved from the * CallParticipatn instance with the use of the corresponding method. * @throws OperationFailedException with the corresponding code if we fail * to create the call. */ public Call createCall(String callee) throws OperationFailedException { return createOutgoingCall(callee); } /** * Create a new call and invite the specified CallParticipant to it. * * @param callee the address of the callee that we should invite to a * new call. * @return CallParticipant the CallParticipant that will represented by * the specified uri. All following state change events will be * delivered through that call participant. The Call that this * participant is a member of could be retrieved from the * CallParticipatn instance with the use of the corresponding method. * @throws OperationFailedException with the corresponding code if we fail * to create the call. */ public Call createCall(Contact callee) throws OperationFailedException { return createOutgoingCall(callee.getAddress()); } /** * Init and establish the specified call. * * @param calleeAddress the address of the callee that we'd like to connect * with. * * @return CallParticipant the CallParticipant that represented by * the specified uri. All following state change events will be * delivered through that call participant. The Call that this * participant is a member of could be retrieved from the * CallParticipatn instance with the use of the corresponding method. * * @throws OperationFailedException with the corresponding code if we fail * to create the call. */ private CallJabberImpl createOutgoingCall(String calleeAddress) throws OperationFailedException { OutgoingJingleSession outJS; logger.info("creating outgoing call..."); if (protocolProvider.getConnection() == null) { throw new OperationFailedException( "Failed to create OutgoingJingleSession.\n" + "we don't have a valid XMPPConnection." , OperationFailedException.INTERNAL_ERROR); } // we determine on which resource the remote user is connected if the // resource isn't already provided String fullCalleeURI = null; if (calleeAddress.indexOf('/') > 0) { fullCalleeURI = calleeAddress; } else { fullCalleeURI = protocolProvider.getConnection(). getRoster().getPresence(calleeAddress).getFrom(); } if (fullCalleeURI.indexOf('/') < 0) { throw new OperationFailedException( "Failed to create OutgoingJingleSession.\n" + "User " + calleeAddress + " is unknown to us." , OperationFailedException.INTERNAL_ERROR); } try { // with discovered info, we can check if the remote clients // supports telephony but not if he don't, because // a non conforming client can supports a feature // without advertising it. So we don't rely on it (for the moment) DiscoverInfo di = ServiceDiscoveryManager .getInstanceFor(protocolProvider.getConnection()) .discoverInfo(fullCalleeURI); if (di.containsFeature("http://www.xmpp.org/extensions/xep-0166.html#ns")) { logger.info(fullCalleeURI + ": jingle supported "); } else { logger.info(calleeAddress + ": jingle not supported ??? "); // // throw new OperationFailedException( // "Failed to create OutgoingJingleSession.\n" // + fullCalleeURI + " do not supports jingle" // , OperationFailedException.INTERNAL_ERROR); } } catch (XMPPException ex) { logger.warn("could not retrieve info for " + fullCalleeURI, ex); } try { outJS = jingleManager.createOutgoingJingleSession(fullCalleeURI); } catch (XMPPException ex) { throw new OperationFailedException( "Failed to create OutgoingJingleSession.\n" + "This is most probably a network connection error." , OperationFailedException.INTERNAL_ERROR , ex); } CallJabberImpl call = new CallJabberImpl(protocolProvider); CallParticipantJabberImpl callParticipant = new CallParticipantJabberImpl(calleeAddress, call); callParticipant.setJingleSession(outJS); callParticipant.setState(CallParticipantState.INITIATING_CALL); fireCallEvent(CallEvent.CALL_INITIATED, call); activeCallsRepository.addCall(call); outJS.start(); return (CallJabberImpl) callParticipant.getCall(); } /** * Returns an iterator over all currently active calls. * * @return an iterator over all currently active calls. */ public Iterator getActiveCalls() { return activeCallsRepository.getActiveCalls(); } /** * Resumes communication with a call participant previously put on hold. * * @param participant the call participant to put on hold. */ public void putOffHold(CallParticipant participant) { /** @todo implement putOffHold() */ ((CallParticipantJabberImpl) participant).getJingleSession(). getJingleMediaSession().setTrasmit(true); } /** * Puts the specified CallParticipant "on hold". * * @param participant the participant that we'd like to put on hold. */ public void putOnHold(CallParticipant participant) { /** @todo implement putOnHold() */ ((CallParticipantJabberImpl) participant).getJingleSession(). getJingleMediaSession().setTrasmit(false); } /** * Implements method hangupCallParticipant * from OperationSetBasicTelephony. * * @param participant the participant that we'd like to hang up on. * @throws ClassCastException if participant is not an instance of * CallParticipantJabberImpl. * * @throws OperationFailedException if we fail to terminate the call. * * // TODO: ask for suppression of OperationFailedException from the interface. * // what happens if hangup fails ? are we forced to continue to talk ? :o) */ public void hangupCallParticipant(CallParticipant participant) throws ClassCastException, OperationFailedException { CallParticipantJabberImpl callParticipant = (CallParticipantJabberImpl)participant; try { callParticipant.getJingleSession().terminate(); } catch (XMPPException ex) { ex.printStackTrace(); } finally { callParticipant.setState(CallParticipantState.DISCONNECTED); } } /** * Implements method answerCallParticipant * from OperationSetBasicTelephony. * * @param participant the call participant that we want to answer * @throws OperationFailedException if we fails to answer */ public void answerCallParticipant(CallParticipant participant) throws OperationFailedException { CallParticipantJabberImpl callParticipant = (CallParticipantJabberImpl)participant; try { ((IncomingJingleSession)callParticipant.getJingleSession()). start(); } catch (XMPPException ex) { throw new OperationFailedException( "Failed to answer an incoming call" , OperationFailedException.INTERNAL_ERROR); } } /** * Closes all active calls. And releases resources. */ public void shutdown() { logger.trace("Ending all active calls. "); Iterator activeCalls = this.activeCallsRepository.getActiveCalls(); // this is fast, but events aren't triggered ... //jingleManager.disconnectAllSessions(); //go through all active calls. while(activeCalls.hasNext()) { CallJabberImpl call = (CallJabberImpl) activeCalls.next(); Iterator callParticipants = call.getCallParticipants(); //go through all call participants and say bye to every one. while (callParticipants.hasNext()) { CallParticipant participant = (CallParticipant) callParticipants.next(); try { this.hangupCallParticipant(participant); } catch (Exception ex) { logger.warn("Failed to properly hangup participant " + participant , ex); } } } } /** * Implements method sessionRequested from JingleSessionRequestListener. * * @param jingleSessionRequest the session requested */ public void sessionRequested(JingleSessionRequest jingleSessionRequest) { IncomingJingleSession inJS; logger.info("session requested "); try { inJS = jingleSessionRequest.accept(); } catch (XMPPException ex) { logger.error("Failed to accept incoming jingle request : " + ex); return; } CallJabberImpl call = new CallJabberImpl(protocolProvider); String from = jingleSessionRequest.getFrom(); // we remove the ressource information at ends if any, as it is for // no meaning for the user if (from.indexOf("/") > 0) { from = from.substring(0, from.indexOf("/")); } CallParticipantJabberImpl callParticipant = new CallParticipantJabberImpl(from, call); callParticipant.setJingleSession(inJS); callParticipant.setState(CallParticipantState.INCOMING_CALL); activeCallsRepository.addCall(call); fireCallEvent(CallEvent.CALL_RECEIVED, call); } /** * Implements method sessionCreated from CreatedJingleSessionListener. * * @param jingleSession the newly created jingle session */ public void sessionCreated(JingleSession jingleSession) { logger.info("session created : " + jingleSession); jingleSession.addListener(this); jingleSession.addMediaListener(this); jingleSession.addStateListener(this); jingleSession.addTransportListener(this); } /** * Implements method mediaClosed from JingleMediaListener. * * @param payloadType payload supported by the closed media */ public void mediaClosed(PayloadType payloadType) { logger.info(" media closed "); } /** * Implements method mediaEstablished from JingleMediaListener. * * @param payloadType payload used by the established media */ public void mediaEstablished(PayloadType payloadType) { logger.info("media established "); } /** * Implements method transportClosed from JingleTransportListener. * * @param transportCandidate transportCandiate with which * we were dealing */ public void transportClosed(TransportCandidate transportCandidate) { logger.info("transport closed "); } /** * Implements method transportClosedOnError from JingleTransportListener. * * @param ex the exception accompagning this error */ public void transportClosedOnError(XMPPException ex) { logger.error("transport closed on error ", ex); } /** * Implements method transportEstablished from JingleTransportListener. * * @param local local TransportCandidate for this transport link * @param remote remote TransportCandidate for this transport link */ public void transportEstablished(TransportCandidate local, TransportCandidate remote) { logger.info("transport established " + local + " -:- " + remote); } /** * Implements method beforeChange from JingleSessionStateListener. * This method is called before the change occurs in the session. * We can cancel the change by throwing a JingleException * * @param oldState old state of the session * @param newState state in which we will go * * @throws JingleException we have the ability to cancel a state change by * throwing a JingleException */ public void beforeChange(JingleNegotiator.State oldState , JingleNegotiator.State newState) throws JingleNegotiator.JingleException { if (newState instanceof IncomingJingleSession.Active) { JingleSession session = (JingleSession) newState.getNegotiator(); CallParticipantJabberImpl callParticipant = activeCallsRepository.findCallParticipant(session); if (callParticipant == null) { return; } callParticipant.setState(CallParticipantState.CONNECTED); } else if (newState instanceof OutgoingJingleSession.Inviting) { JingleSession session = (JingleSession) newState.getNegotiator(); CallParticipantJabberImpl callParticipant = activeCallsRepository.findCallParticipant(session); if (callParticipant == null) { return; } callParticipant.setState(CallParticipantState.CONNECTING); } else if (newState instanceof OutgoingJingleSession.Pending) { JingleSession session = (JingleSession) newState.getNegotiator(); CallParticipantJabberImpl callParticipant = activeCallsRepository.findCallParticipant(session); if (callParticipant == null) { return; } callParticipant.setState(CallParticipantState.ALERTING_REMOTE_SIDE); } else if (newState instanceof OutgoingJingleSession.Active) { JingleSession session = (JingleSession) newState.getNegotiator(); CallParticipantJabberImpl callParticipant = activeCallsRepository.findCallParticipant(session); if (callParticipant == null) { return; } callParticipant.setState(CallParticipantState.CONNECTED); } if ((newState == null) && (oldState != null)) { //hanging JingleSession session = (JingleSession) oldState.getNegotiator(); CallParticipantJabberImpl callParticipant = activeCallsRepository.findCallParticipant(session); if (callParticipant == null) { logger.debug("Received a stray trying response."); return; } try { hangupCallParticipant(callParticipant); } catch (Exception ex) { ex.printStackTrace(); } } } /** * Implements method afterChanged from JingleSessionStateListener. * called when we are effectivly in the newState * * @param oldState old session state * @param newState new session state */ public void afterChanged(JingleNegotiator.State oldState, JingleNegotiator.State newState) { logger.info("session state changed : " + oldState + " => " + newState); } /** * Implements sessionEstablished from JingleSessionListener * * * @param payloadType the payloadType used for media in thi session * @param remoteCandidate the remote end point of thi session * @param localCandidate the local end point of this session * @param jingleSession the session which is now fully established */ public void sessionEstablished(PayloadType payloadType, TransportCandidate remoteCandidate, TransportCandidate localCandidate, JingleSession jingleSession) { logger.info("session established "); } /** * Implements sessionDeclined from JingleSessionListener * * @param reason why the session has been declined * @param jingleSession the declined session */ public void sessionDeclined(String reason, JingleSession jingleSession) { logger.info("session declined : " + reason); } /** * Implements sessionRedirected from JingleSessionListener * * @param redirection redirection information * @param jingleSession the session which redirected */ public void sessionRedirected(String redirection, JingleSession jingleSession) { logger.info("session redirected : " + redirection); } /** * Implements sessionClosed from JingleSessionListener * * @param reason why the session has been closed * @param jingleSession the session which is closed */ public void sessionClosed(String reason, JingleSession jingleSession) { logger.info("session closed : " + reason); } /** * Implements sessionClosedOnError from JingleSessionListener * * @param ex execption which caused the error * @param jingleSession the session which is closed */ public void sessionClosedOnError(XMPPException ex, JingleSession jingleSession) { logger.error("session closed on error ", ex); } /** * Implements sessionMediaReceived from JingleSessionListener * * @param jingleSession the session where the media is established * @param participant the participant for this media session */ public void sessionMediaReceived(JingleSession jingleSession, String participant) { logger.info("session media received "); } }