/* * 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.sip; import gov.nist.javax.sip.header.HeaderFactoryImpl; import gov.nist.javax.sip.header.extensions.*; import java.net.*; import java.text.*; import java.util.*; import javax.sip.*; import javax.sip.address.*; import javax.sip.header.*; import javax.sip.message.*; import net.java.sip.communicator.service.media.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; /** * Implements all call management logic and exports basic telephony support by * implementing OperationSetBasicTelephony. * * @author Emil Ivov * @author Lubomir Marinov * @author Alan Kelly * @author Emanuel Onica */ public class OperationSetBasicTelephonySipImpl extends AbstractOperationSetBasicTelephony implements MethodProcessor, OperationSetAdvancedTelephony, OperationSetSecureTelephony { private static final Logger logger = Logger.getLogger(OperationSetBasicTelephonySipImpl.class); /** * A reference to the <tt>ProtocolProviderServiceSipImpl</tt> instance that * created us. */ private final ProtocolProviderServiceSipImpl protocolProvider; /** * Contains references for all currently active (non ended) calls. */ private final ActiveCallsRepository activeCallsRepository = new ActiveCallsRepository(this); /** * The name of the boolean property that the user could use to specify * whether incoming calls should be rejected if the user name in the * destination (to) address does not match the one that we have in our sip * address. */ private static final String FAIL_CALLS_ON_DEST_USER_MISMATCH = "net.java.sip.communicator.impl.protocol.sip." + "FAIL_CALLS_ON_DEST_USER_MISMATCH"; /** * Creates a new instance and adds itself as an <tt>INVITE</tt> method * handler in the creating protocolProvider. * * @param protocolProvider a reference to the * <tt>ProtocolProviderServiceSipImpl</tt> instance that created * us. */ public OperationSetBasicTelephonySipImpl( ProtocolProviderServiceSipImpl protocolProvider) { this.protocolProvider = protocolProvider; protocolProvider.registerMethodProcessor(Request.INVITE, this); protocolProvider.registerMethodProcessor(Request.CANCEL, this); protocolProvider.registerMethodProcessor(Request.ACK, this); protocolProvider.registerMethodProcessor(Request.BYE, this); protocolProvider.registerMethodProcessor(Request.REFER, this); protocolProvider.registerMethodProcessor(Request.NOTIFY, this); protocolProvider.registerEvent("refer"); } /** * Create a new call and invite the specified CallParticipant to it. * * @param callee the sip 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. * @throws ParseException if <tt>callee</tt> is not a valid sip address * string. */ public Call createCall(String callee) throws OperationFailedException, ParseException { Address toAddress = protocolProvider.parseAddressString(callee); return createOutgoingCall(toAddress, null); } /** * 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 { Address toAddress; try { toAddress = protocolProvider.parseAddressString(callee.getAddress()); } catch (ParseException ex) { // couldn't happen logger.error(ex.getMessage(), ex); throw new IllegalArgumentException(ex.getMessage()); } return createOutgoingCall(toAddress, null); } /** * Init and establish the specified call. * * @param calleeAddress the address of the callee that we'd like to connect * with. * @param cause the <code>Message</code>, if any, which is the cause for the * outgoing call to be placed and which carries additional * information to be included in the call initiation (e.g. a * Referred-To header and token) * @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. */ private synchronized CallSipImpl createOutgoingCall(Address calleeAddress, javax.sip.message.Message cause) throws OperationFailedException { if(!protocolProvider.isRegistered()) { throw new OperationFailedException( "The protocol provider should be registered " +"before placing an outgoing call.", OperationFailedException.PROVIDER_NOT_REGISTERED); } // create the invite request Request invite; try { invite = createInviteRequest(calleeAddress); } catch (IllegalArgumentException exc) { // encapsulate the illegal argument exception into an OpFailedExc // so that the UI would notice it. throw new OperationFailedException( exc.getMessage(), OperationFailedException.ILLEGAL_ARGUMENT, exc); } // Content ContentTypeHeader contentTypeHeader = null; try { // content type should be application/sdp (not applications) // reported by Oleg Shevchenko (Miratech) contentTypeHeader = protocolProvider.getHeaderFactory().createContentTypeHeader( "application", "sdp"); } catch (ParseException ex) { // Shouldn't happen throwOperationFailedException( "Failed to create a content type header for the INVITE request", OperationFailedException.INTERNAL_ERROR, ex); } // check whether there's a cached authorization header for this // call id and if so - attach it to the request. // add authorization header CallIdHeader call = (CallIdHeader) invite.getHeader(CallIdHeader.NAME); String callid = call.getCallId(); AuthorizationHeader authorization = protocolProvider.getSipSecurityManager() .getCachedAuthorizationHeader(callid); if (authorization != null) invite.addHeader(authorization); /* * Whatever the cause of the outgoing call is, reflect the appropriate * information from it into the INVITE request (and do it elsewhere * because this method is already long enough and difficult to grasp). */ if (cause != null) { reflectCauseOnEffect(cause, invite); } // Transaction ClientTransaction inviteTransaction = null; SipProvider jainSipProvider = protocolProvider.getDefaultJainSipProvider(); try { inviteTransaction = jainSipProvider.getNewClientTransaction(invite); } catch (TransactionUnavailableException ex) { throwOperationFailedException( "Failed to create inviteTransaction.\n" + "This is most probably a network connection error.", OperationFailedException.INTERNAL_ERROR, ex); } // create the call participant CallParticipantSipImpl callParticipant = createCallParticipantFor(inviteTransaction, jainSipProvider); // invite content try { CallSession callSession = SipActivator.getMediaService().createCallSession( callParticipant.getCall()); ((CallSipImpl) callParticipant.getCall()) .setMediaCallSession(callSession); // if possible try to indicate the address of the callee so // that the media service can choose the most proper local // address to advertise. javax.sip.address.URI calleeURI = calleeAddress.getURI(); if (calleeURI.isSipURI()) { String host = ((SipURI) calleeURI).getHost(); InetAddress intendedDestination = protocolProvider .resolveSipAddress(host).getAddress(); invite.setContent(callSession .createSdpOffer(intendedDestination), contentTypeHeader); } } catch (UnknownHostException ex) { logger.warn("Failed to obtain an InetAddress." + ex.getMessage(), ex); throw new OperationFailedException( "Failed to obtain an InetAddress for " + ex.getMessage(), OperationFailedException.NETWORK_FAILURE, ex); } catch (ParseException ex) { throwOperationFailedException( "Failed to parse sdp data while creating invite request!", OperationFailedException.INTERNAL_ERROR, ex); } catch (MediaException ex) { throwOperationFailedException("Could not access media devices!", OperationFailedException.INTERNAL_ERROR, ex); } try { inviteTransaction.sendRequest(); if (logger.isDebugEnabled()) logger.debug("sent request: " + invite); } catch (SipException ex) { throwOperationFailedException( "An error occurred while sending invite request", OperationFailedException.NETWORK_FAILURE, ex); } return (CallSipImpl) callParticipant.getCall(); } /** * Copies and possibly modifies information from a given SIP * <code>Message</code> into another SIP <code>Message</code> (the first of * which is being thought of as the cause for the existence of the second * and the second is considered the effect of the first for the sake of * clarity in the most common of use cases). * <p> * The Referred-By header and its optional token are common examples of such * information which is to be copied without modification by the referee * from the REFER <code>Request</code> into the resulting <code>Request</code> * to the refer target. * </p> * * @param cause the SIP <code>Message</code> from which the information is * to be copied * @param effect the SIP <code>Message</code> into which the information is * to be copied */ private void reflectCauseOnEffect(javax.sip.message.Message cause, javax.sip.message.Message effect) { /* * Referred-By (which comes from a referrer) should be copied to the * refer target without tampering. * * TODO Apart from Referred-By, its token should also be copied if * present. */ Header referredBy = cause.getHeader(ReferredByHeader.NAME); if (referredBy != null) { effect.setHeader(referredBy); } } /** * Returns an iterator over all currently active calls. * * @return an iterator over all currently active calls. */ public Iterator<CallSipImpl> getActiveCalls() { return activeCallsRepository.getActiveCalls(); } /** * Resumes communication with a call participant previously put on hold. * * @param participant the call participant to put on hold. * @throws OperationFailedException */ public synchronized void putOffHold(CallParticipant participant) throws OperationFailedException { putOnHold(participant, false); } /** * Puts the specified CallParticipant "on hold". * * @param participant the participant that we'd like to put on hold. * @throws OperationFailedException */ public synchronized void putOnHold(CallParticipant participant) throws OperationFailedException { putOnHold(participant, true); } /** * Puts the specified <tt>CallParticipant</tt> on or off hold. * * @param participant the <tt>CallParticipant</tt> to be put on or off hold * @param on <tt>true</tt> to have the specified <tt>CallParticipant</tt> * put on hold; <tt>false</tt>, otherwise * @throws OperationFailedException */ private void putOnHold(CallParticipant participant, boolean on) throws OperationFailedException { CallSession callSession = ((CallSipImpl) participant.getCall()).getMediaCallSession(); CallParticipantSipImpl sipParticipant = (CallParticipantSipImpl) participant; try { sendInviteRequest(sipParticipant, callSession .createSdpDescriptionForHold( sipParticipant.getSdpDescription(), on)); } catch (MediaException ex) { throwOperationFailedException( "Failed to create SDP offer to hold.", OperationFailedException.INTERNAL_ERROR, ex); } /* * Putting on hold isn't a negotiation (i.e. the issuing side takes the * decision and executes it) so we're muting now regardless of the * desire of the participant to accept the offer. */ callSession.putOnHold(on, true); CallParticipantState state = sipParticipant.getState(); if (CallParticipantState.ON_HOLD_LOCALLY.equals(state)) { if (!on) sipParticipant.setState(CallParticipantState.CONNECTED); } else if (CallParticipantState.ON_HOLD_MUTUALLY.equals(state)) { if (!on) sipParticipant.setState(CallParticipantState.ON_HOLD_REMOTELY); } else if (CallParticipantState.ON_HOLD_REMOTELY.equals(state)) { if (on) sipParticipant.setState(CallParticipantState.ON_HOLD_MUTUALLY); } else if (on) { sipParticipant.setState(CallParticipantState.ON_HOLD_LOCALLY); } } /** * Sends an invite request with a specific SDP offer (description) within * the current <tt>Dialog</tt> with a specific call participant. * * @param sipParticipant the SIP-specific call participant to send the * invite to within the current <tt>Dialog</tt> * @param sdpOffer the description of the SDP offer to be made to the * specified call participant with the sent invite * @throws OperationFailedException */ private void sendInviteRequest(CallParticipantSipImpl sipParticipant, String sdpOffer) throws OperationFailedException { Dialog dialog = sipParticipant.getDialog(); Request invite = createRequest(dialog, Request.INVITE); try { invite.setContent(sdpOffer, protocolProvider.getHeaderFactory() .createContentTypeHeader("application", "sdp")); } catch (ParseException ex) { throwOperationFailedException( "Failed to parse SDP offer for the new invite.", OperationFailedException.INTERNAL_ERROR, ex); } sendRequest(sipParticipant.getJainSipProvider(), invite, dialog); } /** * Sends a specific <code>Request</code> through a given * <code>SipProvider</code> as part of the conversation associated with a * specific <code>Dialog</code>. * * @param sipProvider the <code>SipProvider</code> to send the specified * request through * @param request the <code>Request</code> to send through * <code>sipProvider</code> * @param dialog the <code>Dialog</code> as part of which the specified * <code>request</code> is to be sent * @throws OperationFailedException */ private void sendRequest(SipProvider sipProvider, Request request, Dialog dialog) throws OperationFailedException { ClientTransaction clientTransaction = null; try { clientTransaction = sipProvider.getNewClientTransaction(request); } catch (TransactionUnavailableException ex) { throwOperationFailedException( "Failed to create a client transaction for request:\n" + request, OperationFailedException.INTERNAL_ERROR, ex); } try { dialog.sendRequest(clientTransaction); } catch (SipException ex) { throwOperationFailedException( "Failed to send request:\n" + request, OperationFailedException.NETWORK_FAILURE, ex); } logger.debug("Sent request:\n" + request); } /** * Logs a specific message and associated <tt>Throwable</tt> cause as an * error using the current <tt>Logger</tt> and then throws a new * <tt>OperationFailedException</tt> with the message, a specific error code * and the cause. * * @param message the message to be logged and then wrapped in a new * <tt>OperationFailedException</tt> * @param errorCode the error code to be assigned to the new * <tt>OperationFailedException</tt> * @param cause the <tt>Throwable</tt> that has caused the necessity to log * an error and have a new <tt>OperationFailedException</tt> * thrown * @throws OperationFailedException */ private void throwOperationFailedException(String message, int errorCode, Throwable cause) throws OperationFailedException { logger.error(message, cause); throw new OperationFailedException(message, errorCode, cause); } /** * Processes a Request received on a SipProvider upon which this SipListener * is registered. * <p> * * @param requestEvent requestEvent fired from the SipProvider to the * <tt>SipListener</tt> representing a Request received from the * network. * @return <tt>true</tt> if the specified event has been handled by this * processor and shouldn't be offered to other processors registered * for the same method; <tt>false</tt>, otherwise */ public boolean processRequest(RequestEvent requestEvent) { ServerTransaction serverTransaction = requestEvent.getServerTransaction(); SipProvider jainSipProvider = (SipProvider) requestEvent.getSource(); Request request = requestEvent.getRequest(); String requestMethod = request.getMethod(); if (serverTransaction == null) { try { serverTransaction = jainSipProvider.getNewServerTransaction(request); } catch (TransactionAlreadyExistsException ex) { // let's not scare the user and only log a message logger.error("Failed to create a new server" + "transaction for an incoming request\n" + "(Next message contains the request)", ex); return false; } catch (TransactionUnavailableException ex) { // let's not scare the user and only log a message logger.error("Failed to create a new server" + "transaction for an incoming request\n" + "(Next message contains the request)", ex); return false; } } boolean processed = false; // INVITE if (requestMethod.equals(Request.INVITE)) { logger.debug("received INVITE"); DialogState dialogState = serverTransaction.getDialog().getState(); if ((dialogState == null) || dialogState.equals(DialogState.CONFIRMED)) { if (logger.isDebugEnabled()) logger.debug("request is an INVITE. Dialog state=" + dialogState); processInvite(jainSipProvider, serverTransaction, request); processed = true; } else { logger.error("reINVITEs while the dialog is not" + "confirmed are not currently supported."); } } // ACK else if (requestMethod.equals(Request.ACK)) { processAck(serverTransaction, request); processed = true; } // BYE else if (requestMethod.equals(Request.BYE)) { processBye(serverTransaction, request); processed = true; } // CANCEL else if (requestMethod.equals(Request.CANCEL)) { processCancel(serverTransaction, request); processed = true; } // REFER else if (requestMethod.equals(Request.REFER)) { logger.debug("received REFER"); processRefer(serverTransaction, request, jainSipProvider); processed = true; } // NOTIFY else if (requestMethod.equals(Request.NOTIFY)) { logger.debug("received NOTIFY"); processed = processNotify(serverTransaction, request); } return processed; } /** * Process an asynchronously reported TransactionTerminatedEvent. * * @param transactionTerminatedEvent -- an event that indicates that the * transaction has transitioned into the terminated state. * @return <tt>true</tt> if the specified event has been handled by this * processor and shouldn't be offered to other processors registered * for the same method; <tt>false</tt>, otherwise */ public boolean processTransactionTerminated( TransactionTerminatedEvent transactionTerminatedEvent) { // nothing to do here. return false; } /** * Analyzes the incoming <tt>responseEvent</tt> and then forwards it to the * proper event handler. * * @param responseEvent the responseEvent that we received * ProtocolProviderService. * @return <tt>true</tt> if the specified event has been handled by this * processor and shouldn't be offered to other processors registered * for the same method; <tt>false</tt>, otherwise */ public boolean processResponse(ResponseEvent responseEvent) { ClientTransaction clientTransaction = responseEvent.getClientTransaction(); Response response = responseEvent.getResponse(); CSeqHeader cseq = ((CSeqHeader) response.getHeader(CSeqHeader.NAME)); if (cseq == null) { logger.error("An incoming response did not contain a CSeq header"); } String method = cseq.getMethod(); SipProvider sourceProvider = (SipProvider) responseEvent.getSource(); int responseStatusCode = response.getStatusCode(); boolean processed = false; switch (responseStatusCode) { // OK case Response.OK: if (method.equals(Request.INVITE)) { processInviteOK(clientTransaction, response); processed = true; } else if (method.equals(Request.BYE)) { // ignore } break; // Ringing case Response.RINGING: processRinging(clientTransaction, response); processed = true; break; // Session Progress case Response.SESSION_PROGRESS: processSessionProgress(clientTransaction, response); processed = true; break; // Trying case Response.TRYING: processTrying(clientTransaction, response); processed = true; break; // Busy case Response.BUSY_HERE: case Response.BUSY_EVERYWHERE: case Response.DECLINE: processBusyHere(clientTransaction, response); processed = true; break; // Accepted case Response.ACCEPTED: if (Request.REFER.equals(method)) { processReferAccepted(clientTransaction, response); processed = true; } break; // 401 UNAUTHORIZED case Response.UNAUTHORIZED: case Response.PROXY_AUTHENTICATION_REQUIRED: processAuthenticationChallenge(clientTransaction, response, sourceProvider); processed = true; break; // errors default: if ((responseStatusCode / 100 == 4) || (responseStatusCode / 100 == 5) || (responseStatusCode / 100 == 6)) { CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(clientTransaction .getDialog()); logger.error("Received error: " + response.getStatusCode() + " " + response.getReasonPhrase()); if (callParticipant != null) callParticipant.setState(CallParticipantState.FAILED); processed = true; } // ignore everything else. break; } return processed; } /** * Processes a specific <code>Response.ACCEPTED</code> response of an * earlier <code>Request.REFER</code> request. * * @param clientTransaction the <code>ClientTransaction</code> which brought * the response * @param accepted the <code>Response.ACCEPTED</code> response to an earlier * <code>Request.REFER</code> request */ private void processReferAccepted(ClientTransaction clientTransaction, Response accepted) { try { DialogUtils.addSubscription(clientTransaction.getDialog(), "refer"); } catch (SipException ex) { logger.error("Failed to make Accepted REFER response" + " keep the dialog alive after BYE:\n" + accepted, ex); } } /** * Updates the call state of the corresponding call participant. * * @param clientTransaction the transaction in which the response was * received. * @param response the trying response. */ private void processTrying(ClientTransaction clientTransaction, Response response) { Dialog dialog = clientTransaction.getDialog(); // find the call participant CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant == null) { logger.debug("Received a stray trying response."); return; } // change status CallParticipantState callParticipantState = callParticipant.getState(); if (!CallParticipantState.CONNECTED.equals(callParticipantState) && !CallParticipantState.isOnHold(callParticipantState)) callParticipant.setState(CallParticipantState.CONNECTING); } /** * Updates the call state of the corresponding call participant. We'll also * try to extract any details here that might be of use for call participant * presentation and that we didn't have when establishing the call. * * @param clientTransaction the transaction in which the response was * received. * @param response the Trying response. */ private void processRinging(ClientTransaction clientTransaction, Response response) { Dialog dialog = clientTransaction.getDialog(); // find the call participant CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant == null) { logger.debug("Received a stray trying response."); return; } // try to update the display name. ContactHeader remotePartyContactHeader = (ContactHeader) response.getHeader(ContactHeader.NAME); if (remotePartyContactHeader != null) { Address remotePartyAddress = remotePartyContactHeader.getAddress(); String displayName = remotePartyAddress.getDisplayName(); if (displayName != null && displayName.trim().length() > 0) { callParticipant.setDisplayName(displayName); } } // change status. callParticipant.setState(CallParticipantState.ALERTING_REMOTE_SIDE); } /** * Handles early media in 183 Session Progress responses. Retrieves the SDP * and makes sure that we start transmitting and playing early media that we * receive. Puts the call into a CONNECTING_WITH_EARLY_MEDIA state. * * @param clientTransaction the <tt>ClientTransaction</tt> that the response * arrived in. * @param sessionProgress the 183 <tt>Response</tt> to process */ private void processSessionProgress(ClientTransaction clientTransaction, Response sessionProgress) { Dialog dialog = clientTransaction.getDialog(); // find the call CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant.getState() == CallParticipantState.CONNECTING_WITH_EARLY_MEDIA) { // This can happen if we are receigin early media for a second time. logger.warn("Ignoring invite 183 since call participant is " + "already exchanging early media."); return; } if (sessionProgress.getContentLength().getContentLength() == 0) { logger.warn("Ignoring a 183 with no content"); return; } ContentTypeHeader contentTypeHeader = (ContentTypeHeader) sessionProgress .getHeader(ContentTypeHeader.NAME); if (!contentTypeHeader.getContentType().equalsIgnoreCase("application") || !contentTypeHeader.getContentSubType().equalsIgnoreCase("sdp")) { // This can happen if we are receiving early media for a second time. logger.warn("Ignoring invite 183 since call participant is " + "already exchanging early media."); return; } // set sdp content before setting call state as that is where // listeners get alerted and they need the sdp callParticipant.setSdpDescription(new String(sessionProgress .getRawContent())); // notify the media manager of the sdp content CallSession callSession = ((CallSipImpl) callParticipant.getCall()).getMediaCallSession(); if (callSession == null) { // unlikely to happen because it would mean we didn't send an offer // in the invite and we always send one. logger.warn("Could not find call session."); return; } try { callSession.processSdpAnswer(callParticipant, new String( sessionProgress.getRawContent())); } catch (ParseException exc) { logErrorAndFailCallParticipant( "There was an error parsing the SDP description of " + callParticipant.getDisplayName() + "(" + callParticipant.getAddress() + ")", exc, callParticipant); return; } catch (MediaException exc) { logErrorAndFailCallParticipant( "We failed to process the SDP description of " + callParticipant.getDisplayName() + "(" + callParticipant.getAddress() + ")" + ". Error was: " + exc.getMessage(), exc, callParticipant); return; } // set the call url in case there was one /** * @todo this should be done in CallSession, once we move it here. */ callParticipant.setCallInfoURL(callSession.getCallInfoURL()); // change status callParticipant .setState(CallParticipantState.CONNECTING_WITH_EARLY_MEDIA); } /** * Sets to CONNECTED that state of the corresponding call participant and * sends an ACK. * * @param clientTransaction the <tt>ClientTransaction</tt> that the response * arrived in. * @param ok the OK <tt>Response</tt> to process */ private void processInviteOK(ClientTransaction clientTransaction, Response ok) { Dialog dialog = clientTransaction.getDialog(); // find the call CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant == null) { /* * In case of forwarding a call, the dialog may have forked. If the * dialog is forked, we must end early state dialogs by replacing * the dialog with the new one. */ CallIdHeader call = (CallIdHeader) ok.getHeader(CallIdHeader.NAME); String callid = call.getCallId(); Iterator<CallSipImpl> activeCallsIter = activeCallsRepository.getActiveCalls(); while (activeCallsIter.hasNext()) { CallSipImpl activeCall = activeCallsIter.next(); Iterator<CallParticipant> callParticipantsIter = activeCall.getCallParticipants(); while (callParticipantsIter.hasNext()) { CallParticipantSipImpl cp = (CallParticipantSipImpl) callParticipantsIter.next(); Dialog callPartDialog = cp.getDialog(); // check if participant in same call // and has the same transaction if (callPartDialog != null && callPartDialog.getCallId() != null && cp.getFirstTransaction() != null && cp.getDialog().getCallId().getCallId() .equals(callid) && clientTransaction.getBranchId().equals( cp.getFirstTransaction().getBranchId())) { // change to the forked dialog callParticipant = cp; cp.setDialog(dialog); } } } if (callParticipant == null) { logger.debug("Received a stray ok response."); return; } } /* * Receiving an Invite OK is allowed even when the participant is * already connected for the purposes of call hold. */ Request ack = null; ContentTypeHeader contentTypeHeader = null; // Create ACK try { // Need to use dialog generated ACKs so that the remote UA core // sees them - Fixed by M.Ranganathan CSeqHeader cseq = ((CSeqHeader) ok.getHeader(CSeqHeader.NAME)); ack = clientTransaction.getDialog().createAck(cseq.getSeqNumber()); // Content should it be necessary. // content type should be application/sdp (not applications) // reported by Oleg Shevchenko (Miratech) contentTypeHeader = protocolProvider.getHeaderFactory().createContentTypeHeader( "application", "sdp"); } catch (ParseException ex) { // Shouldn't happen logErrorAndFailCallParticipant( "Failed to create a content type header for the ACK request", ex, callParticipant); return; } catch (InvalidArgumentException ex) { // Shouldn't happen logErrorAndFailCallParticipant( "Failed ACK request, problem with the supplied cseq", ex, callParticipant); return; } catch (SipException ex) { logErrorAndFailCallParticipant("Failed to create ACK request!", ex, callParticipant); return; } // !!! set sdp content before setting call state as that is where // listeners get alerted and they need the sdp // ignore sdp if we have already received one in early media if(!CallParticipantState.CONNECTING_WITH_EARLY_MEDIA. equals(callParticipant.getState())) callParticipant.setSdpDescription(new String(ok.getRawContent())); // notify the media manager of the sdp content CallSession callSession = ((CallSipImpl) callParticipant.getCall()).getMediaCallSession(); try { try { if (callSession == null) { // non existent call session - that means we didn't send sdp // in the invite and this is the offer so we need to create // the answer. callSession = SipActivator.getMediaService().createCallSession( callParticipant.getCall()); String sdp = callSession.processSdpOffer(callParticipant, callParticipant.getSdpDescription()); ack.setContent(sdp, contentTypeHeader); // set the call url in case there was one /** * @todo this should be done in CallSession, once we move it * here. */ callParticipant .setCallInfoURL(callSession.getCallInfoURL()); } } finally { // Send the ACK now since we got all the info we need, // and callSession.processSdpAnswer can take a few seconds. // (patch by Michael Koch) try { clientTransaction.getDialog().sendAck(ack); } catch (SipException ex) { logErrorAndFailCallParticipant( "Failed to acknowledge call!", ex, callParticipant); return; } } // ignore sdp process if we have already received one in early media CallParticipantState callParticipantState = callParticipant.getState(); if ((callParticipantState != CallParticipantState.CONNECTED) && !CallParticipantState.isOnHold(callParticipantState) && !CallParticipantState.CONNECTING_WITH_EARLY_MEDIA. equals(callParticipantState)) { callSession.processSdpAnswer(callParticipant, callParticipant .getSdpDescription()); } // set the call url in case there was one /** * @todo this should be done in CallSession, once we move it here. */ callParticipant.setCallInfoURL(callSession.getCallInfoURL()); } //at this point we have already sent our ack so in adition to logging //an error we also need to hangup the call participant. catch (Exception exc)//Media or parse exception. { logger.error("There was an error parsing the SDP description of " + callParticipant.getDisplayName() + "(" + callParticipant.getAddress() + ")", exc); try{ //we are connected from a SIP point of view (cause we sent our //ack) sp make sure we set the state accordingly or the hangup //method won't know how to end the call. callParticipant.setState(CallParticipantState.CONNECTED); hangupCallParticipant(callParticipant); } catch (Exception e){ //I don't see what more we could do. logger.error(e); callParticipant.setState(CallParticipantState.FAILED, e.getMessage()); } return; } // change status if (!CallParticipantState.isOnHold(callParticipant.getState())) callParticipant.setState(CallParticipantState.CONNECTED); } private void logErrorAndFailCallParticipant(String message, Throwable throwable, CallParticipantSipImpl participant) { logger.error(message, throwable); participant.setState(CallParticipantState.FAILED, message); } /** * Sets corresponding state to the call participant associated with this * transaction. * * @param clientTransaction the transaction in which * @param busyHere the busy here Response */ private void processBusyHere(ClientTransaction clientTransaction, Response busyHere) { Dialog dialog = clientTransaction.getDialog(); // find the call CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant == null) { logger.debug("Received a stray busyHere response."); return; } // change status callParticipant.setState(CallParticipantState.BUSY); } /** * Attempts to re-generate the corresponding request with the proper * credentials and terminates the call if it fails. * * @param clientTransaction the corresponding transaction * @param response the challenge * @param jainSipProvider the provider that received the challenge */ private void processAuthenticationChallenge( ClientTransaction clientTransaction, Response response, SipProvider jainSipProvider) { // First find the call and the call participant that this authentication // request concerns. CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(clientTransaction .getDialog()); if (callParticipant == null) { logger.debug("Received an authorization challenge for no " + "participant. authorizing anyway."); } try { logger.debug("Authenticating an INVITE request."); ClientTransaction retryTran = protocolProvider.getSipSecurityManager().handleChallenge( response, clientTransaction, jainSipProvider); if (retryTran == null) { logger.trace("No password supplied or error occured!"); return; } // There is a new dialog that will be started with this request. Get // that dialog and record it into the Call objet for later use (by // Bye-s for example). // if the request was BYE then we need to authorize it anyway even // if the call and the call participant are no longer there if (callParticipant != null) { callParticipant.setDialog(retryTran.getDialog()); callParticipant.setFirstTransaction(retryTran); callParticipant.setJainSipProvider(jainSipProvider); } retryTran.sendRequest(); } catch (Exception exc) { // tell the others we couldn't register logErrorAndFailCallParticipant( "We failed to authenticate an INVITE request.", exc, callParticipant); return; } } /** * Processes a retransmit or expiration Timeout of an underlying * {@link Transaction}handled by this SipListener. This Event notifies the * application that a retransmission or transaction Timer expired in the * SipProvider's transaction state machine. The TimeoutEvent encapsulates * the specific timeout type and the transaction identifier either client or * server upon which the timeout occurred. The type of Timeout can by * determined by: * <code>timeoutType = timeoutEvent.getTimeout().getValue();</code> * * @param timeoutEvent the timeoutEvent received indicating either the * message retransmit or transaction timed out. * @return <tt>true</tt> if the specified event has been handled by this * processor and shouldn't be offered to other processors registered * for the same method; <tt>false</tt>, otherwise */ public boolean processTimeout(TimeoutEvent timeoutEvent) { Transaction transaction; if (timeoutEvent.isServerTransaction()) { // don't care. or maybe a stack bug? return false; } else { transaction = timeoutEvent.getClientTransaction(); } CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(transaction.getDialog()); if (callParticipant == null) { logger.debug("Got a headless timeout event." + timeoutEvent); return false; } // change status callParticipant.setState(CallParticipantState.FAILED, "The remote party has not replied!" + "The call will be disconnected"); return true; } /** * Process an asynchronously reported IO Exception. Asynchronous IO * Exceptions may occur as a result of errors during retransmission of * requests. The transaction state machine requires to report IO Exceptions * to the application immediately (according to RFC 3261). This method * enables an implementation to propagate the asynchronous handling of IO * Exceptions to the application. * * @param exceptionEvent The Exception event that is reported to the * application. * @return <tt>true</tt> if the specified event has been handled by this * processor and shouldn't be offered to other processors registered * for the same method; <tt>false</tt>, otherwise */ public boolean processIOException(IOExceptionEvent exceptionEvent) { logger.error("Got an asynchronous exception event. host=" + exceptionEvent.getHost() + " port=" + exceptionEvent.getPort()); return true; } /** * Process an asynchronously reported DialogTerminatedEvent. * * @param dialogTerminatedEvent -- an event that indicates that the dialog * has transitioned into the terminated state. * @return <tt>true</tt> if the specified event has been handled by this * processor and shouldn't be offered to other processors registered * for the same method; <tt>false</tt>, otherwise */ public boolean processDialogTerminated( DialogTerminatedEvent dialogTerminatedEvent) { CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialogTerminatedEvent .getDialog()); if (callParticipant == null) { return false; } // change status callParticipant.setState(CallParticipantState.DISCONNECTED); return true; } /** * Creates an invite request destined for <tt>callee</tt>. * * @param toAddress the sip address of the callee that the request is meant * for. * @return a newly created sip <tt>Request</tt> destined for <tt>callee</tt> * . * @throws OperationFailedException with the corresponding code if creating * the request fails. * @throws IllegalArgumentException if <tt>toAddress</tt> does not appear * to be a valid destination. */ private Request createInviteRequest(Address toAddress) throws OperationFailedException, IllegalArgumentException { // Call ID CallIdHeader callIdHeader = protocolProvider.getDefaultJainSipProvider().getNewCallId(); // CSeq HeaderFactory headerFactory = protocolProvider.getHeaderFactory(); CSeqHeader cSeqHeader = null; try { cSeqHeader = headerFactory.createCSeqHeader(1l, Request.INVITE); } catch (InvalidArgumentException ex) { // Shouldn't happen throwOperationFailedException("An unexpected erro occurred while" + "constructing the CSeqHeadder", OperationFailedException.INTERNAL_ERROR, ex); } catch (ParseException exc) { // shouldn't happen throwOperationFailedException("An unexpected erro occurred while" + "constructing the CSeqHeadder", OperationFailedException.INTERNAL_ERROR, exc); } // ReplacesHeader Header replacesHeader = stripReplacesHeader(toAddress); // FromHeader String localTag = ProtocolProviderServiceSipImpl.generateLocalTag(); FromHeader fromHeader = null; ToHeader toHeader = null; try { // FromHeader fromHeader = headerFactory.createFromHeader( protocolProvider.getOurSipAddress(toAddress), localTag); // ToHeader toHeader = headerFactory.createToHeader(toAddress, null); } catch (ParseException ex) { // these two should never happen. throwOperationFailedException("An unexpected erro occurred while" + "constructing the ToHeader", OperationFailedException.INTERNAL_ERROR, ex); } // ViaHeaders ArrayList<ViaHeader> viaHeaders = protocolProvider.getLocalViaHeaders(toAddress); // MaxForwards MaxForwardsHeader maxForwards = protocolProvider.getMaxForwardsHeader(); // Contact ContactHeader contactHeader = protocolProvider.getContactHeader(toHeader.getAddress()); Request invite = null; try { invite = protocolProvider.getMessageFactory().createRequest( toHeader.getAddress().getURI(), Request.INVITE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards); } catch (ParseException ex) { // shouldn't happen throwOperationFailedException("Failed to create invite Request!", OperationFailedException.INTERNAL_ERROR, ex); } // User Agent UserAgentHeader userAgentHeader = protocolProvider.getSipCommUserAgentHeader(); if (userAgentHeader != null) invite.addHeader(userAgentHeader); // add the contact header. invite.addHeader(contactHeader); // Add the ReplacesHeader if any. if (replacesHeader != null) { invite.setHeader(replacesHeader); } return invite; } /** * Returns the <code>ReplacesHeader</code> contained, if any, in the * <code>URI</code> of a specific <code>Address</code> after removing it * from there. * * @param address the <code>Address</code> which is to have its * <code>URI</code> examined and modified * @return a <code>Header</code> which represents the Replaces header * contained in the <code>URI</code> of the specified * <code>address</code>; <code>null</code> if no such header is * present */ private Header stripReplacesHeader(Address address) throws OperationFailedException { javax.sip.address.URI uri = address.getURI(); Header replacesHeader = null; if (uri.isSipURI()) { SipURI sipURI = (SipURI) uri; String replacesHeaderValue = sipURI.getHeader(ReplacesHeader.NAME); if (replacesHeaderValue != null) { for (Iterator<String> headerNameIter = sipURI.getHeaderNames(); headerNameIter.hasNext();) { if (ReplacesHeader.NAME.equals(headerNameIter.next())) { headerNameIter.remove(); break; } } try { replacesHeader = protocolProvider.getHeaderFactory().createHeader( ReplacesHeader.NAME, replacesHeaderValue); } catch (ParseException ex) { throw new OperationFailedException( "Failed to create ReplacesHeader from " + replacesHeaderValue, OperationFailedException.INTERNAL_ERROR, ex); } } } return replacesHeader; } /** * Creates a new call and sends a RINGING response. * * @param sourceProvider the provider containing <tt>sourceTransaction</tt>. * @param serverTransaction the transaction containing the received request. * @param invite the Request that we've just received. */ private void processInvite(SipProvider sourceProvider, ServerTransaction serverTransaction, Request invite) { Dialog dialog = serverTransaction.getDialog(); CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); int statusCode; CallParticipantSipImpl callParticipantToReplace = null; if (callParticipant == null) { ReplacesHeader replacesHeader = (ReplacesHeader) invite.getHeader(ReplacesHeader.NAME); if (replacesHeader == null) { statusCode = Response.RINGING; } else { List<CallParticipantSipImpl> callParticipantsToReplace = activeCallsRepository.findCallParticipants( replacesHeader.getCallId(), replacesHeader.getToTag(), replacesHeader.getFromTag()); if (callParticipantsToReplace.size() == 1) { statusCode = Response.OK; callParticipantToReplace = callParticipantsToReplace.get(0); } else { statusCode = Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; } } logger.trace("Creating call participant."); callParticipant = createCallParticipantFor(serverTransaction, sourceProvider); logger.trace("call participant created = " + callParticipant); } else { statusCode = Response.OK; } // sdp description may be in acks - bug report Laurent Michel ContentLengthHeader cl = invite.getContentLength(); if (cl != null && cl.getContentLength() > 0) { callParticipant .setSdpDescription(new String(invite.getRawContent())); } if (!isInviteProperlyAddressed(dialog)) { callParticipant.setState(CallParticipantState.FAILED, "A call was received here while it appeared " + "destined to someone else. The call was rejected."); statusCode = Response.NOT_FOUND; } // INVITE w/ Replaces if ((statusCode == Response.OK) && (callParticipantToReplace != null)) { boolean sayBye = false; try { answerCallParticipant(callParticipant); sayBye = true; } catch (OperationFailedException ex) { logger.error( "Failed to auto-answer the referred call participant " + callParticipant, ex); /* * RFC 3891 says an appropriate error response MUST be returned * and callParticipantToReplace must be left unchanged. */ } if (sayBye) { try { hangupCallParticipant(callParticipantToReplace); } catch (OperationFailedException ex) { logger.error("Failed to hangup the referer " + callParticipantToReplace, ex); callParticipantToReplace.setState( CallParticipantState.FAILED, "Internal Error: " + ex); } } // Even if there was a failure, we cannot just send Response.OK. return; } // Send statusCode String statusCodeString; switch (statusCode) { case Response.RINGING: statusCodeString = "RINGING"; break; case Response.NOT_FOUND: statusCodeString = "NOT_FOUND"; break; case Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST: statusCodeString = "CALL_OR_TRANSACTION_DOES_NOT_EXIST"; break; default: statusCodeString = "OK"; break; } Response response = null; logger.debug("Invite seems ok, we'll say " + statusCodeString + "."); try { response = protocolProvider.getMessageFactory().createResponse(statusCode, invite); protocolProvider.attachToTag(response, dialog); response.setHeader(protocolProvider.getSipCommUserAgentHeader()); if (statusCode != Response.NOT_FOUND) { // set our display name ((ToHeader) response.getHeader(ToHeader.NAME)).getAddress() .setDisplayName(protocolProvider.getOurDisplayName()); // extract our intended destination which should be in the from Address callerAddress = ((FromHeader) response.getHeader(FromHeader.NAME)) .getAddress(); response.addHeader(protocolProvider .getContactHeader(callerAddress)); if (statusCode == Response.OK) { try { processInviteSendingResponse(callParticipant, response); } catch (OperationFailedException ex) { logger.error("Error while trying to send response " + response, ex); callParticipant.setState(CallParticipantState.FAILED, "Internal Error: " + ex.getMessage()); return; } } } } catch (ParseException ex) { logger.error("Error while trying to send a response", ex); callParticipant.setState(CallParticipantState.FAILED, "Internal Error: " + ex.getMessage()); return; } try { logger.trace("will send " + statusCodeString + " response: "); serverTransaction.sendResponse(response); logger.debug("sent a " + statusCodeString + " response: " + response); } catch (Exception ex) { logger.error("Error while trying to send a request", ex); callParticipant.setState(CallParticipantState.FAILED, "Internal Error: " + ex.getMessage()); return; } if (statusCode == Response.OK) { try { processInviteSentResponse(callParticipant, response); } catch (OperationFailedException ex) { logger.error("Error after sending response " + response, ex); } } } /** * Determines whether an INVITE request which has initiated a specific * <code>Dialog</code> is properly addressed. * * @param dialog the <code>Dialog</code> which has been initiated by the * INVITE request to be checked * @return <tt>true</tt> if the INVITE request represented by the specified * <code>Dialog</code> is properly addressed; <tt>false</tt>, * otherwise */ private boolean isInviteProperlyAddressed(Dialog dialog) { logger.trace("Will verify whether INVITE is properly addressed."); // Are we the one they are looking for? javax.sip.address.URI calleeURI = dialog.getLocalParty().getURI(); if (calleeURI.isSipURI()) { boolean assertUserMatch = Boolean.parseBoolean( SipActivator.getConfigurationService().getString( FAIL_CALLS_ON_DEST_USER_MISMATCH)); if (assertUserMatch) { // user info is case sensitive according to rfc3261 String calleeUser = ((SipURI) calleeURI).getUser(); String localUser = protocolProvider.getAccountID().getUserID(); return (calleeUser == null) || calleeUser.equals(localUser); } } return true; } /** * Provides a hook for this instance to take last configuration steps on a * specific <tt>Response</tt> before it is sent to a specific * <tt>CallParticipant</tt> as part of the execution of * {@link #processInvite(SipProvider, ServerTransaction, Request)}. * * @param participant the <tt>CallParticipant</tt> to receive a specific * <tt>Response</tt> * @param response the <tt>Response</tt> to be sent to the * <tt>participant</tt> * @throws OperationFailedException * @throws ParseException */ private void processInviteSendingResponse(CallParticipant participant, Response response) throws OperationFailedException, ParseException { /* * At the time of this writing, we're only getting called because a * response to a call-hold invite is to be sent. */ CallSession callSession = ((CallSipImpl) participant.getCall()).getMediaCallSession(); CallParticipantSipImpl sipParticipant = (CallParticipantSipImpl) participant; String sdpOffer = sipParticipant.getSdpDescription(); String sdpAnswer = null; try { sdpAnswer = callSession.createSdpDescriptionForHold(sdpOffer, callSession .isSdpOfferToHold(sdpOffer)); } catch (MediaException ex) { throwOperationFailedException( "Failed to create SDP answer to put-on/off-hold request.", OperationFailedException.INTERNAL_ERROR, ex); } response.setContent(sdpAnswer, protocolProvider.getHeaderFactory() .createContentTypeHeader("application", "sdp")); } /** * Provides a hook for this instance to take immediate steps after a * specific <tt>Response</tt> has been sent to a specific * <tt>CallParticipant</tt> as part of the execution of * {@link #processInvite(SipProvider, ServerTransaction, Request)}. * * @param participant the <tt>CallParticipant</tt> who was sent a specific * <tt>Response</tt> * @param response the <tt>Response</tt> that has just been sent to the * <tt>participant</tt> * @throws OperationFailedException * @throws ParseException */ private void processInviteSentResponse(CallParticipant participant, Response response) throws OperationFailedException { /* * At the time of this writing, we're only getting called because a * response to a call-hold invite is to be sent. */ CallSession callSession = ((CallSipImpl) participant.getCall()).getMediaCallSession(); CallParticipantSipImpl sipParticipant = (CallParticipantSipImpl) participant; boolean on = false; try { on = callSession .isSdpOfferToHold(sipParticipant.getSdpDescription()); } catch (MediaException ex) { throwOperationFailedException( "Failed to create SDP answer to put-on/off-hold request.", OperationFailedException.INTERNAL_ERROR, ex); } callSession.putOnHold(on, false); CallParticipantState state = sipParticipant.getState(); if (CallParticipantState.ON_HOLD_LOCALLY.equals(state)) { if (on) sipParticipant.setState(CallParticipantState.ON_HOLD_MUTUALLY); } else if (CallParticipantState.ON_HOLD_MUTUALLY.equals(state)) { if (!on) sipParticipant.setState(CallParticipantState.ON_HOLD_LOCALLY); } else if (CallParticipantState.ON_HOLD_REMOTELY.equals(state)) { if (!on) sipParticipant.setState(CallParticipantState.CONNECTED); } else if (on) { sipParticipant.setState(CallParticipantState.ON_HOLD_REMOTELY); } } /** * Sets the state of the corresponding call participant to DISCONNECTED and * sends an OK response. * * @param serverTransaction the ServerTransaction the the BYE request * arrived in. * @param byeRequest the BYE request to process */ private void processBye(ServerTransaction serverTransaction, Request byeRequest) { // find the call Dialog dialog = serverTransaction.getDialog(); CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant == null) { logger.debug("Received a stray bye request."); return; } // Send OK Response ok = null; try { ok = createOKResponse(byeRequest, dialog); } catch (ParseException ex) { logger.error("Error while trying to send a response to a bye", ex); // no need to let the user know about the error since it doesn't // affect them return; } try { serverTransaction.sendResponse(ok); logger.debug("sent response " + ok); } catch (Exception ex) { // This is not really a problem according to the RFC // so just dump to stdout should someone be interested logger.error("Failed to send an OK response to BYE request," + "exception was:\n", ex); } // change status boolean dialogIsAlive; try { dialogIsAlive = DialogUtils.processByeThenIsDialogAlive(dialog); } catch (SipException ex) { dialogIsAlive = false; logger .error( "Failed to determine whether the dialog should stay alive.", ex); } if (dialogIsAlive) { ((CallSipImpl) callParticipant.getCall()).getMediaCallSession() .stopStreaming(); } else { callParticipant.setState(CallParticipantState.DISCONNECTED); } } /** * Updates the session description and sends the state of the corresponding * call participant to CONNECTED. * * @param serverTransaction the transaction that the Ack was received in. * @param ackRequest Request */ private void processAck(ServerTransaction serverTransaction, Request ackRequest) { // find the call CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(serverTransaction .getDialog()); if (callParticipant == null) { // this is most probably the ack for a killed call - don't signal it logger.debug("didn't find an ack's call, returning"); return; } ContentLengthHeader cl = ackRequest.getContentLength(); if (cl != null && cl.getContentLength() > 0) { callParticipant.setSdpDescription(new String(ackRequest .getRawContent())); } // change status if (!CallParticipantState.isOnHold(callParticipant.getState())) callParticipant.setState(CallParticipantState.CONNECTED); } /** * Sets the state of the specifies call participant as DISCONNECTED. * * @param serverTransaction the transaction that the cancel was received in. * @param cancelRequest the Request that we've just received. */ void processCancel(ServerTransaction serverTransaction, Request cancelRequest) { // find the call CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(serverTransaction .getDialog()); if (callParticipant == null) { logger.debug("received a stray CANCEL req. ignoring"); return; } // Cancels should be OK-ed and the initial transaction - terminated // (report and fix by Ranga) try { Response ok = createOKResponse(cancelRequest, serverTransaction.getDialog()); serverTransaction.sendResponse(ok); logger.debug("sent an ok response to a CANCEL request:\n" + ok); } catch (ParseException ex) { logErrorAndFailCallParticipant( "Failed to create an OK Response to an CANCEL request.", ex, callParticipant); return; } catch (Exception ex) { logErrorAndFailCallParticipant( "Failed to send an OK Response to an CANCEL request.", ex, callParticipant); return; } try { // stop the invite transaction as well Transaction tran = callParticipant.getFirstTransaction(); // should be server transaction and misplaced cancels should be // filtered by the stack but it doesn't hurt checking anyway if (!(tran instanceof ServerTransaction)) { logger.error("Received a misplaced CANCEL request!"); return; } ServerTransaction inviteTran = (ServerTransaction) tran; Request invite = callParticipant.getFirstTransaction().getRequest(); Response requestTerminated = protocolProvider.getMessageFactory().createResponse( Response.REQUEST_TERMINATED, invite); requestTerminated.setHeader(protocolProvider .getSipCommUserAgentHeader()); protocolProvider.attachToTag(requestTerminated, callParticipant .getDialog()); inviteTran.sendResponse(requestTerminated); if (logger.isDebugEnabled()) logger.debug("sent request terminated response:\n" + requestTerminated); } catch (ParseException ex) { logger.error("Failed to create a REQUEST_TERMINATED Response to " + "an INVITE request.", ex); } catch (Exception ex) { logger.error("Failed to send an REQUEST_TERMINATED Response to " + "an INVITE request.", ex); } // change status callParticipant.setState(CallParticipantState.DISCONNECTED); } /** * Processes a specific REFER request i.e. attempts to transfer the * call/call participant receiving the request to a specific transfer * target. * * @param serverTransaction the <code>ServerTransaction</code> containing * the REFER request * @param referRequest the very REFER request * @param sipProvider the provider containing <code>serverTransaction</code> */ private void processRefer(ServerTransaction serverTransaction, final Request referRequest, final SipProvider sipProvider) { ReferToHeader referToHeader = (ReferToHeader) referRequest.getHeader(ReferToHeader.NAME); if (referToHeader == null) { logger.error("No Refer-To header in REFER request:\n" + referRequest); return; } Address referToAddress = referToHeader.getAddress(); if (referToAddress == null) { logger.error("No address in REFER request Refer-To header:\n" + referRequest); return; } // Accepted final Dialog dialog = serverTransaction.getDialog(); Response accepted = null; try { accepted = protocolProvider.getMessageFactory().createResponse( Response.ACCEPTED, referRequest); protocolProvider.attachToTag(accepted, dialog); accepted.setHeader(protocolProvider.getSipCommUserAgentHeader()); } catch (ParseException ex) { logger.error( "Failed to create Accepted response to REFER request:\n" + referRequest, ex); /* * TODO Should the call transfer not be attempted because the * Accepted couldn't be sent? */ } boolean removeSubscription = false; if (accepted != null) { Throwable failure = null; try { serverTransaction.sendResponse(accepted); } catch (InvalidArgumentException ex) { failure = ex; } catch (SipException ex) { failure = ex; } if (failure != null) { accepted = null; logger.error( "Failed to send Accepted response to REFER request:\n" + referRequest, failure); /* * TODO Should the call transfer not be attempted because the * Accepted couldn't be sent? */ } else { /* * The REFER request has created a subscription. Take it into * consideration in order to not disconnect on BYE but rather * when the last subscription terminates. */ try { removeSubscription = DialogUtils.addSubscription(dialog, referRequest); } catch (SipException ex) { logger.error("Failed to make the REFER request" + "keep the dialog alive after BYE:\n" + referRequest, ex); } // NOTIFY Trying try { sendReferNotifyRequest(dialog, SubscriptionStateHeader.ACTIVE, null, "SIP/2.0 100 Trying", sipProvider); } catch (OperationFailedException ex) { /* * TODO Determine whether the failure to send the Trying * refer NOTIFY should prevent the sending of the * session-terminating refer NOTIFY. */ } } } /* * Regardless of whether the Accepted, NOTIFY, etc. succeeded, try to * transfer the call because it's the most important goal. */ Call referToCall; try { referToCall = createOutgoingCall(referToAddress, referRequest); } catch (OperationFailedException ex) { referToCall = null; logger.error("Failed to create outgoing call to " + referToAddress, ex); } /* * Start monitoring the call in order to discover when the * subscription-terminating NOTIFY with the final result of the REFER is * to be sent. */ final Call referToCallListenerSource = referToCall; final boolean sendNotifyRequest = (accepted != null); final Object subscription = (removeSubscription ? referRequest : null); CallChangeListener referToCallListener = new CallChangeAdapter() { /** * The indicator which determines whether the job of this listener * has been done i.e. whether a single subscription-terminating * NOTIFY with the final result of the REFER has been sent. */ private boolean done; public synchronized void callStateChanged(CallChangeEvent evt) { if (!done && referToCallStateChanged(referToCallListenerSource, sendNotifyRequest, dialog, sipProvider, subscription)) { done = true; if (referToCallListenerSource != null) { referToCallListenerSource .removeCallChangeListener(this); } } } }; if (referToCall != null) { referToCall.addCallChangeListener(referToCallListener); } referToCallListener.callStateChanged(null); } /** * Processes a specific <code>Request.NOTIFY</code> request for the purposes * of telephony. * * @param serverTransaction the <code>ServerTransaction</code> containing * the <code>Request.NOTIFY</code> request * @param notifyRequest the <code>Request.NOTIFY</code> request to be * processed */ private boolean processNotify(ServerTransaction serverTransaction, Request notifyRequest) { /* * We're only handling NOTIFY as part of call transfer (i.e. refer) * right now. */ EventHeader eventHeader = (EventHeader) notifyRequest.getHeader(EventHeader.NAME); if ((eventHeader == null) || !"refer".equals(eventHeader.getEventType())) { return false; } SubscriptionStateHeader ssHeader = (SubscriptionStateHeader) notifyRequest .getHeader(SubscriptionStateHeader.NAME); if (ssHeader == null) { logger.error("NOTIFY of refer event type" + "with no Subscription-State header."); return false; } Dialog dialog = serverTransaction.getDialog(); CallParticipantSipImpl participant = activeCallsRepository.findCallParticipant(dialog); if (participant == null) { logger.debug("Received a stray refer NOTIFY request."); return false; } // OK Response ok; try { ok = createOKResponse(notifyRequest, dialog); } catch (ParseException ex) { String message = "Failed to create OK response to refer NOTIFY request."; logger.error(message, ex); participant.setState(CallParticipantState.DISCONNECTED, message); return false; } try { serverTransaction.sendResponse(ok); } catch (Exception ex) { String message = "Failed to send OK response to refer NOTIFY request."; logger.error(message, ex); participant.setState(CallParticipantState.DISCONNECTED, message); return false; } if (SubscriptionStateHeader.TERMINATED.equals(ssHeader.getState()) && !DialogUtils .removeSubscriptionThenIsDialogAlive(dialog, "refer")) { participant.setState(CallParticipantState.DISCONNECTED); } if (!CallParticipantState.DISCONNECTED.equals(participant.getState()) && !DialogUtils.isByeProcessed(dialog)) { boolean dialogIsAlive; try { dialogIsAlive = sayBye(participant); } catch (OperationFailedException ex) { logger.error( "Failed to send BYE in response to refer NOTIFY request.", ex); dialogIsAlive = false; } if (!dialogIsAlive) { participant.setState(CallParticipantState.DISCONNECTED); } } return true; } /** * Tracks the state changes of a specific <code>Call</code> and sends a * session-terminating NOTIFY request to the <code>Dialog</code> which * referred to the call in question as soon as the outcome of the refer is * determined. * * @param referToCall the <code>Call</code> to track and send a NOTIFY * request for * @param sendNotifyRequest <tt>true</tt> if a session-terminating NOTIFY * request should be sent to the <code>Dialog</code> which * referred to <code>referToCall</code>; <tt>false</tt> to send * no such NOTIFY request * @param dialog the <code>Dialog</code> which initiated the specified call * as part of processing a REFER request * @param sipProvider the <code>SipProvider</code> to send the NOTIFY * request through * @param subscription the subscription to be terminated when the NOTIFY * request is sent * @return <tt>true</tt> if a session-terminating NOTIFY request was sent * and the state of <code>referToCall</code> should no longer be * tracked; <tt>false</tt> if it's too early to send a * session-terminating NOTIFY request and the tracking of the state * of <code>referToCall</code> should continue */ private boolean referToCallStateChanged(Call referToCall, boolean sendNotifyRequest, Dialog dialog, SipProvider sipProvider, Object subscription) { CallState referToCallState = (referToCall == null) ? null : referToCall.getCallState(); if (CallState.CALL_INITIALIZATION.equals(referToCallState)) { return false; } /* * NOTIFY OK/Declined * * It doesn't sound like sending NOTIFY Service Unavailable is * appropriate because the REFER request has (presumably) already been * accepted. */ if (sendNotifyRequest) { String referStatus = CallState.CALL_IN_PROGRESS.equals(referToCallState) ? "SIP/2.0 200 OK" : "SIP/2.0 603 Declined"; try { sendReferNotifyRequest(dialog, SubscriptionStateHeader.TERMINATED, SubscriptionStateHeader.NO_RESOURCE, referStatus, sipProvider); } catch (OperationFailedException ex) { // The exception has already been logged. } } /* * Whatever the status of the REFER is, the subscription created by it * is terminated with the final NOTIFY. */ if (!DialogUtils.removeSubscriptionThenIsDialogAlive(dialog, subscription)) { CallParticipantSipImpl callParticipant = activeCallsRepository.findCallParticipant(dialog); if (callParticipant != null) { callParticipant.setState(CallParticipantState.DISCONNECTED); } } return true; } /** * Sends a <code>Request.NOTIFY</code> request in a specific * <code>Dialog</code> as part of the communication associated with an * earlier-received <code>Request.REFER</code> request. The sent NOTIFY has * a specific <code>Subscription-State</code> header and reason, carries a * specific body content and is sent through a specific * <code>SipProvider</code>. * * @param dialog the <code>Dialog</code> to send the NOTIFY request in * @param subscriptionState the <code>Subscription-State</code> header to be * sent with the NOTIFY request * @param reasonCode the reason for the specified * <code>subscriptionState</code> if any; <tt>null</tt> otherwise * @param content the content to be carried in the body of the sent NOTIFY * request * @param sipProvider the <code>SipProvider</code> to send the NOTIFY * request through * @throws OperationFailedException */ private void sendReferNotifyRequest(Dialog dialog, String subscriptionState, String reasonCode, Object content, SipProvider sipProvider) throws OperationFailedException { Request notify = createRequest(dialog, Request.NOTIFY); HeaderFactory headerFactory = protocolProvider.getHeaderFactory(); // Populate the request. String eventType = "refer"; try { notify.setHeader(headerFactory.createEventHeader(eventType)); } catch (ParseException ex) { throwOperationFailedException("Failed to create " + eventType + " Event header.", OperationFailedException.INTERNAL_ERROR, ex); } SubscriptionStateHeader ssHeader = null; try { ssHeader = headerFactory.createSubscriptionStateHeader(subscriptionState); if (reasonCode != null) ssHeader.setReasonCode(reasonCode); } catch (ParseException ex) { throwOperationFailedException("Failed to create " + subscriptionState + " Subscription-State header.", OperationFailedException.INTERNAL_ERROR, ex); } notify.setHeader(ssHeader); ContentTypeHeader ctHeader = null; try { ctHeader = headerFactory.createContentTypeHeader("message", "sipfrag"); } catch (ParseException ex) { throwOperationFailedException( "Failed to create Content-Type header.", OperationFailedException.INTERNAL_ERROR, ex); } try { notify.setContent(content, ctHeader); } catch (ParseException ex) { throwOperationFailedException("Failed to set NOTIFY body/content.", OperationFailedException.INTERNAL_ERROR, ex); } sendRequest(sipProvider, notify, dialog); } /** * Creates a new {@link Request} of a specific method which is to be sent in * a specific <code>Dialog</code> and populates its generally-necessary * headers such as the Authorization header. * * @param dialog the <code>Dialog</code> to create the new * <code>Request</code> in * @param method the method of the newly-created <code>Request<code> * @return a new {@link Request} of the specified <code>method</code> which * is to be sent in the specified <code>dialog</code> and populated * with its generally-necessary headers such as the Authorization * header * @throws OperationFailedException */ private Request createRequest(Dialog dialog, String method) throws OperationFailedException { Request request = null; try { request = dialog.createRequest(method); } catch (SipException ex) { throwOperationFailedException("Failed to create " + method + " request.", OperationFailedException.INTERNAL_ERROR, ex); } /* * The authorization-related headers are the responsibility of the * application (according to the Javadoc of JAIN-SIP). */ AuthorizationHeader authorization = protocolProvider.getSipSecurityManager() .getCachedAuthorizationHeader( ((CallIdHeader) request.getHeader(CallIdHeader.NAME)) .getCallId()); if (authorization != null) { request.setHeader(authorization); } return request; } /** * Indicates a user request to end a call with the specified call * participant. Depending on the state of the call the method would send a * CANCEL, BYE, or BUSY_HERE and set the new state to DISCONNECTED. * * @param participant the participant that we'd like to hang up on. * @throws ClassCastException if participant is not an instance of this * CallParticipantSipImpl. * @throws OperationFailedException if we fail to terminate the call. */ public synchronized void hangupCallParticipant(CallParticipant participant) throws ClassCastException, OperationFailedException { // do nothing if the call is already ended if (participant.getState().equals(CallParticipantState.DISCONNECTED) || participant.getState().equals(CallParticipantState.FAILED)) { logger.debug("Ignoring a request to hangup a call participant " + "that is already DISCONNECTED"); return; } CallParticipantSipImpl callParticipant = (CallParticipantSipImpl) participant; CallParticipantState participantState = callParticipant.getState(); if (participantState.equals(CallParticipantState.CONNECTED) || CallParticipantState.isOnHold(participantState)) { sayBye(callParticipant); callParticipant.setState(CallParticipantState.DISCONNECTED); } else if (callParticipant.getState().equals( CallParticipantState.CONNECTING) || callParticipant.getState().equals( CallParticipantState.CONNECTING_WITH_EARLY_MEDIA) || callParticipant.getState().equals( CallParticipantState.ALERTING_REMOTE_SIDE)) { if (callParticipant.getFirstTransaction() != null) { // Someone knows about us. Let's be polite and say we are // leaving sayCancel(callParticipant); } callParticipant.setState(CallParticipantState.DISCONNECTED); } else if (participantState.equals(CallParticipantState.INCOMING_CALL)) { callParticipant.setState(CallParticipantState.DISCONNECTED); sayBusyHere(callParticipant); } // For FAILE and BUSY we only need to update CALL_STATUS else if (participantState.equals(CallParticipantState.BUSY)) { callParticipant.setState(CallParticipantState.DISCONNECTED); } else if (participantState.equals(CallParticipantState.FAILED)) { callParticipant.setState(CallParticipantState.DISCONNECTED); } else { callParticipant.setState(CallParticipantState.DISCONNECTED); logger.error("Could not determine call participant state!"); } } // end call /** * Sends an Internal Error response to <tt>callParticipant</tt>. * * @param callParticipant the call participant that we need to say bye to. * * @throws OperationFailedException if we failed constructing or sending a * SIP Message. */ public void sayInternalError(CallParticipantSipImpl callParticipant) throws OperationFailedException { sayError(callParticipant, Response.SERVER_INTERNAL_ERROR); } /** * Send an error response with the <tt>errorCode</tt> code to * <tt>callParticipant</tt>. * * @param callParticipant the call participant that we need to say bye to. * @param errorCode the code that the response should have. * * @throws OperationFailedException if we failed constructing or sending a * SIP Message. */ public void sayError(CallParticipantSipImpl callParticipant, int errorCode) throws OperationFailedException { Dialog dialog = callParticipant.getDialog(); callParticipant.setState(CallParticipantState.FAILED); if (dialog == null) { logger.error("Failed to extract participant's associated dialog! " + "Ending Call!"); throw new OperationFailedException( "Failed to extract participant's associated dialog! " + "Ending Call!", OperationFailedException.INTERNAL_ERROR); } Transaction transaction = callParticipant.getFirstTransaction(); if (transaction == null || !dialog.isServer()) { logger.error("Failed to extract a transaction" + " from the call's associated dialog!"); throw new OperationFailedException( "Failed to extract a transaction from the participant's " + "associated dialog!", OperationFailedException.INTERNAL_ERROR); } ServerTransaction serverTransaction = (ServerTransaction) transaction; Response errorResponse = null; try { errorResponse = protocolProvider.getMessageFactory().createResponse(errorCode, callParticipant.getFirstTransaction().getRequest()); protocolProvider.attachToTag(errorResponse, dialog); } catch (ParseException ex) { throwOperationFailedException( "Failed to construct an OK response to an INVITE request", OperationFailedException.INTERNAL_ERROR, ex); } ContactHeader contactHeader = protocolProvider .getContactHeader(dialog.getRemoteTarget()); errorResponse.addHeader(contactHeader); try { serverTransaction.sendResponse(errorResponse); if (logger.isDebugEnabled()) logger.debug("sent response: " + errorResponse); } catch (Exception ex) { throwOperationFailedException( "Failed to send an OK response to an INVITE request", OperationFailedException.INTERNAL_ERROR, ex); } } // internal error /** * Sends a BYE request to <tt>callParticipant</tt>. * * @param callParticipant the call participant that we need to say bye to. * @return <tt>true</tt> if the <code>Dialog</code> should be considered * alive after sending the BYE request (e.g. when there're still * active subscriptions); <tt>false</tt>, otherwise * @throws OperationFailedException if we failed constructing or sending a * SIP Message. */ private boolean sayBye(CallParticipantSipImpl callParticipant) throws OperationFailedException { Dialog dialog = callParticipant.getDialog(); Request bye = null; try { bye = dialog.createRequest(Request.BYE); // we have to set the via headers our selves because otherwise // jain sip would send them with a 0.0.0.0 address SipURI destination = (SipURI) bye.getRequestURI(); ArrayList<ViaHeader> viaHeaders = protocolProvider.getLocalViaHeaders(destination); bye.setHeader(viaHeaders.get(0)); bye.addHeader(protocolProvider.getSipCommUserAgentHeader()); } catch (SipException ex) { throwOperationFailedException("Failed to create bye request!", OperationFailedException.INTERNAL_ERROR, ex); } sendRequest(callParticipant.getJainSipProvider(), bye, dialog); /* * Let subscriptions such as the ones associated with REFER requests * keep the dialog alive and correctly delete it when they are * terminated. */ try { return DialogUtils.processByeThenIsDialogAlive(dialog); } catch (SipException ex) { throwOperationFailedException( "Failed to determine whether the dialog should stay alive.", OperationFailedException.INTERNAL_ERROR, ex); return false; } } // bye /** * Sends a Cancel request to <tt>callParticipant</tt>. * * @param callParticipant the call participant that we need to cancel. * * @throws OperationFailedException we failed to construct or send the * CANCEL request. */ private void sayCancel(CallParticipantSipImpl callParticipant) throws OperationFailedException { if (callParticipant.getDialog().isServer()) { logger.error("Cannot cancel a server transaction"); throw new OperationFailedException( "Cannot cancel a server transaction", OperationFailedException.INTERNAL_ERROR); } ClientTransaction clientTransaction = (ClientTransaction) callParticipant.getFirstTransaction(); try { Request cancel = clientTransaction.createCancel(); ClientTransaction cancelTransaction = callParticipant.getJainSipProvider().getNewClientTransaction( cancel); cancelTransaction.sendRequest(); logger.debug("sent request:\n" + cancel); } catch (SipException ex) { throwOperationFailedException("Failed to send the CANCEL request", OperationFailedException.NETWORK_FAILURE, ex); } } // cancel /** * Sends a BUSY_HERE response to <tt>callParticipant</tt>. * * @param callParticipant the call participant that we need to send busy * tone to. * @throws OperationFailedException if we fail to create or send the * response */ private void sayBusyHere(CallParticipantSipImpl callParticipant) throws OperationFailedException { Request request = callParticipant.getFirstTransaction().getRequest(); Response busyHere = null; try { busyHere = protocolProvider.getMessageFactory().createResponse( Response.BUSY_HERE, request); busyHere.setHeader(protocolProvider.getSipCommUserAgentHeader()); protocolProvider.attachToTag(busyHere, callParticipant.getDialog()); } catch (ParseException ex) { throwOperationFailedException( "Failed to create the BUSY_HERE response!", OperationFailedException.INTERNAL_ERROR, ex); } if (!callParticipant.getDialog().isServer()) { logger.error("Cannot send BUSY_HERE in a client transaction"); throw new OperationFailedException( "Cannot send BUSY_HERE in a client transaction", OperationFailedException.INTERNAL_ERROR); } ServerTransaction serverTransaction = (ServerTransaction) callParticipant.getFirstTransaction(); try { serverTransaction.sendResponse(busyHere); logger.debug("sent response:\n" + busyHere); } catch (Exception ex) { throwOperationFailedException( "Failed to send the BUSY_HERE response", OperationFailedException.NETWORK_FAILURE, ex); } } // busy here /** * Indicates a user request to answer an incoming call from the specified * CallParticipant. * * Sends an OK response to <tt>callParticipant</tt>. Make sure that the call * participant contains an sdp description when you call this method. * * @param participant the call participant that we need to send the ok to. * @throws OperationFailedException if we fail to create or send the * response. */ public synchronized void answerCallParticipant(CallParticipant participant) throws OperationFailedException { CallParticipantSipImpl callParticipant = (CallParticipantSipImpl) participant; Transaction transaction = callParticipant.getFirstTransaction(); Dialog dialog = callParticipant.getDialog(); if (transaction == null || !dialog.isServer()) { callParticipant.setState(CallParticipantState.DISCONNECTED); throw new OperationFailedException( "Failed to extract a ServerTransaction " + "from the call's associated dialog!", OperationFailedException.INTERNAL_ERROR); } CallParticipantState participantState = participant.getState(); if (participantState.equals(CallParticipantState.CONNECTED) || CallParticipantState.isOnHold(participantState)) { logger.info("Ignoring user request to answer a CallParticipant " + "that is already connected. CP:" + participant); return; } ServerTransaction serverTransaction = (ServerTransaction) transaction; Response ok = null; try { ok = createOKResponse(callParticipant.getFirstTransaction() .getRequest(), dialog); } catch (ParseException ex) { callParticipant.setState(CallParticipantState.DISCONNECTED); throwOperationFailedException( "Failed to construct an OK response to an INVITE request", OperationFailedException.INTERNAL_ERROR, ex); } // Content ContentTypeHeader contentTypeHeader = null; try { // content type should be application/sdp (not applications) // reported by Oleg Shevchenko (Miratech) contentTypeHeader = protocolProvider.getHeaderFactory().createContentTypeHeader( "application", "sdp"); } catch (ParseException ex) { // Shouldn't happen callParticipant.setState(CallParticipantState.DISCONNECTED); throwOperationFailedException( "Failed to create a content type header for the OK response", OperationFailedException.INTERNAL_ERROR, ex); } try { CallSession callSession = SipActivator.getMediaService().createCallSession( callParticipant.getCall()); ((CallSipImpl) callParticipant.getCall()) .setMediaCallSession(callSession); String sdpOffer = callParticipant.getSdpDescription(); String sdp; // if the offer was in the invite create an sdp answer if ((sdpOffer != null) && (sdpOffer.length() > 0)) { sdp = callSession.processSdpOffer(callParticipant, sdpOffer); // set the call url in case there was one /** * @todo this should be done in CallSession, once we move it * here. */ callParticipant.setCallInfoURL(callSession.getCallInfoURL()); } // if there was no offer in the invite - create an offer else { sdp = callSession.createSdpOffer(); } ok.setContent(sdp, contentTypeHeader); } catch (MediaException ex) { //log, the error and tell the remote party. do not throw an //exception as it would go to the stack and there's nothing it could //do with it. logger.error( "Failed to created an SDP description for an ok response " + "to an INVITE request!", ex); this.sayError((CallParticipantSipImpl) participant, Response.NOT_ACCEPTABLE_HERE); } catch (ParseException ex) { //log, the error and tell the remote party. do not throw an //exception as it would go to the stack and there's nothing it could //do with it. logger.error( "Failed to parse sdp data while creating invite request!", ex); this.sayError((CallParticipantSipImpl) participant, Response.NOT_ACCEPTABLE_HERE); } ContactHeader contactHeader = protocolProvider.getContactHeader( dialog.getRemoteTarget()); ok.addHeader(contactHeader); try { serverTransaction.sendResponse(ok); if (logger.isDebugEnabled()) logger.debug("sent response\n" + ok); } catch (Exception ex) { callParticipant.setState(CallParticipantState.DISCONNECTED); throwOperationFailedException( "Failed to send an OK response to an INVITE request", OperationFailedException.NETWORK_FAILURE, ex); } } // answer call /** * Creates a new {@link Response#OK} response to a specific {@link Request} * which is to be sent as part of a specific {@link Dialog}. * * @param request the <code>Request</code> to create the OK response for * @param containingDialog the <code>Dialog</code> to send the response in * @return a new <code>Response.OK</code> response to the specified * <code>request</code> to be sent as part of the specified * <code>containingDialog</code> * @throws ParseException */ private Response createOKResponse(Request request, Dialog containingDialog) throws ParseException { Response ok = protocolProvider.getMessageFactory().createResponse(Response.OK, request); protocolProvider.attachToTag(ok, containingDialog); ok.setHeader(protocolProvider.getSipCommUserAgentHeader()); return ok; } /** * Creates a new call and call participant associated with * <tt>containingTransaction</tt> * * @param containingTransaction the transaction that created the call. * @param sourceProvider the provider that the containingTransaction belongs * to. * * @return a new instance of a <tt>CallParticipantSipImpl</tt> corresponding * to the <tt>containingTransaction</tt>. */ private CallParticipantSipImpl createCallParticipantFor( Transaction containingTransaction, SipProvider sourceProvider) { CallSipImpl call = new CallSipImpl(protocolProvider); CallParticipantSipImpl callParticipant = new CallParticipantSipImpl( containingTransaction.getDialog().getRemoteParty(), call); boolean incomingCall = (containingTransaction instanceof ServerTransaction); callParticipant.setState( incomingCall ? CallParticipantState.INCOMING_CALL : CallParticipantState.INITIATING_CALL); callParticipant.setDialog(containingTransaction.getDialog()); callParticipant.setFirstTransaction(containingTransaction); callParticipant.setJainSipProvider(sourceProvider); activeCallsRepository.addCall(call); // notify everyone fireCallEvent( incomingCall ? CallEvent.CALL_RECEIVED : CallEvent.CALL_INITIATED, call); return callParticipant; } /** * Returns a string representation of this OperationSetBasicTelephony * instance including information that would permit to distinguish it among * other instances when reading a log file. * * @return a string representation of this operation set. */ public String toString() { return getClass().getSimpleName() + "-[dn=" + protocolProvider.getOurDisplayName() + " addr=[" + protocolProvider.getRegistrarConnection().getAddressOfRecord() + "]"; } /** * Closes all active calls. And releases resources. */ public synchronized void shutdown() { logger.trace("Ending all active calls."); Iterator<CallSipImpl> activeCalls = this.activeCallsRepository.getActiveCalls(); // go through all active calls. while (activeCalls.hasNext()) { CallSipImpl call = activeCalls.next(); Iterator<CallParticipant> callParticipants = call.getCallParticipants(); // go through all call participants and say bye to every one. while (callParticipants.hasNext()) { CallParticipant participant = callParticipants.next(); try { this.hangupCallParticipant(participant); } catch (Exception ex) { logger.warn("Failed to properly hangup particpant " + participant, ex); } } } } /** * Sets the mute state of the audio stream being sent to a specific * <tt>CallParticipant</tt>. * <p> * The implementation sends silence through the audio stream. * </p> * * @param participant the <tt>CallParticipant</tt> who receives the audio * stream to have its mute state set * @param mute <tt>true</tt> to mute the audio stream being sent to * <tt>participant</tt>; otherwise, <tt>false</tt> */ public void setMute(CallParticipant participant, boolean mute) { CallParticipantSipImpl sipParticipant = (CallParticipantSipImpl) participant; ((CallSipImpl) sipParticipant.getCall()) .getMediaCallSession().setMute(mute); sipParticipant.setMute(mute); } /** * Sets the secure communication status of the call associated to the given * participant. * * @param participant the call participant, for which we're setting the * state. * @param secure indicates the security status - enabled ot disabled. * @param source indicates the source of this change - local change, remote * change or revert of the status. */ public void setSecure( CallParticipant participant, boolean secure, SecureStatusChangeSource source) { CallSession cs = ((CallSipImpl) participant.getCall()).getMediaCallSession(); if (cs != null) cs.setSecureCommunicationStatus(secure, source); } /** * Returns <code>true</code> to indicate that the call associated with the * given participant is secured, otherwise returns <code>false</code>. * * @return <code>true</code> to indicate that the call associated with the * given participant is secured, otherwise returns <code>false</code>. */ public boolean isSecure(CallParticipant participant) { CallSession cs = ((CallSipImpl) participant.getCall()).getMediaCallSession(); return (cs != null) && cs.getSecureCommunicationStatus(); } /** * Sets the SAS verification property value for the given call participant. * * @param participant the call participant, for which we set the * @param isVerified indicates whether the SAS string is verified or not * for the given participant. */ public boolean setSasVerified( CallParticipant participant, boolean isVerified) { CallSession cs = ((CallSipImpl) participant.getCall()).getMediaCallSession(); return (cs != null) && cs.setZrtpSASVerification(isVerified); } /** * Transfers (in the sense of call transfer) a specific * <code>CallParticipant</code> to a specific callee address. * * @param participant the <code>CallParticipant</code> to be transfered to * the specified callee address * @param target the <code>Address</code> the callee to transfer * <code>participant</code> to * @throws OperationFailedException */ private void transfer(CallParticipant participant, Address target) throws OperationFailedException { CallParticipantSipImpl sipParticipant = (CallParticipantSipImpl) participant; Dialog dialog = sipParticipant.getDialog(); Request refer = createRequest(dialog, Request.REFER); HeaderFactory headerFactory = protocolProvider.getHeaderFactory(); // Refer-To is required. refer.setHeader(headerFactory.createReferToHeader(target)); /* * Referred-By is optional but only to the extent that the refer target * may choose to require a valid Referred-By token. */ refer.addHeader( ((HeaderFactoryImpl) headerFactory) .createReferredByHeader(sipParticipant.getJainSipAddress())); sendRequest(sipParticipant.getJainSipProvider(), refer, dialog); } /* * (non-Javadoc) * * @see * net.java.sip.communicator.service.protocol.OperationSetAdvancedTelephony * #transfer(net.java.sip.communicator.service.protocol.CallParticipant, * net.java.sip.communicator.service.protocol.CallParticipant) */ public void transfer(CallParticipant participant, CallParticipant target) throws OperationFailedException { Address targetAddress = parseAddressString(target.getAddress()); Dialog targetDialog = ((CallParticipantSipImpl) target).getDialog(); String remoteTag = targetDialog.getRemoteTag(); String localTag = targetDialog.getLocalTag(); Replaces replacesHeader = null; SipURI sipURI = (SipURI) targetAddress.getURI(); try { replacesHeader = (Replaces) ((HeaderFactoryImpl) protocolProvider.getHeaderFactory()) .createReplacesHeader( targetDialog.getCallId().getCallId(), (remoteTag == null) ? "0" : remoteTag, (localTag == null) ? "0" : localTag); } catch (ParseException ex) { throwOperationFailedException( "Failed to create Replaces header for target dialog " + targetDialog, OperationFailedException.ILLEGAL_ARGUMENT, ex); } try { sipURI.setHeader(ReplacesHeader.NAME, replacesHeader.encodeBody()); } catch (ParseException ex) { throwOperationFailedException("Failed to set Replaces header " + replacesHeader + " to SipURI " + sipURI, OperationFailedException.INTERNAL_ERROR, ex); } putOnHold(participant); putOnHold(target); transfer(participant, targetAddress); } /* * (non-Javadoc) * * @see * net.java.sip.communicator.service.protocol.OperationSetAdvancedTelephony * #transfer(net.java.sip.communicator.service.protocol.CallParticipant, * String) */ public void transfer(CallParticipant participant, String target) throws OperationFailedException { transfer(participant, parseAddressString(target)); } /** * Parses a specific string into a JAIN SIP <code>Address</code>. * * @param addressString the <code>String</code> to be parsed into an * <code>Address</code> * @return the <code>Address</code> representation of * <code>addressString</code> * @throws OperationFailedException if <code>addressString</code> is not * properly formatted */ private Address parseAddressString(String addressString) throws OperationFailedException { Address address = null; try { address = protocolProvider.parseAddressString(addressString); } catch (ParseException ex) { throwOperationFailedException("Failed to parse address string " + addressString, OperationFailedException.ILLEGAL_ARGUMENT, ex); } return address; } }