/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.protocol.sip.dtmf; import gov.nist.javax.sip.header.*; import java.io.*; import java.text.*; import java.util.*; import javax.sip.*; import javax.sip.header.*; import javax.sip.message.*; import net.java.sip.communicator.impl.protocol.sip.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.OperationFailedException; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.jitsi.service.neomedia.*; /** * Sending DTMFs with SIP INFO. * * @author Damian Minkov * @author Lyubomir Marinov */ public class DTMFInfo extends MethodProcessorAdapter { /** * The <tt>Logger</tt> used by the <tt>DTMFInfo</tt> class and its instances * for logging output. */ private static final Logger logger = Logger.getLogger(DTMFInfo.class); /** * The sub-type of the content of the <tt>Request</tt>s being sent by * <tt>DTMFInfo</tt>. */ private static final String CONTENT_SUB_TYPE = "dtmf-relay"; /** * The type of the content of the <tt>Request</tt>s being sent by * <tt>DTMFInfo</tt>. */ private static final String CONTENT_TYPE = "application"; /** * Maps call peers and tone and its start time, so we can compute duration. */ private Hashtable<CallPeer, Object[]> currentlyTransmittingTones = new Hashtable<CallPeer, Object[]>(); /** * Involved protocol provider service. */ private final ProtocolProviderServiceSipImpl pps; /** * A list of listeners registered for dtmf tone events. */ private final List<DTMFListener> dtmfListeners = new LinkedList<DTMFListener>(); /** * Constructor * * @param pps the SIP Protocol provider service */ public DTMFInfo(ProtocolProviderServiceSipImpl pps) { this.pps = pps; this.pps.registerMethodProcessor(Request.INFO, this); } /** * Saves the tone we need to send and its start time. With start time we * can compute the duration later when we need to send the DTMF. * * @param callPeer the call peer. * @param tone the tone to transmit. * @throws OperationFailedException * @throws NullPointerException * @throws IllegalArgumentException */ public void startSendingDTMF(CallPeerSipImpl callPeer, DTMFTone tone) throws OperationFailedException, NullPointerException, IllegalArgumentException { if(currentlyTransmittingTones.contains(callPeer)) throw new IllegalStateException( "Error starting dtmf tone, already started"); currentlyTransmittingTones.put(callPeer, new Object[]{tone, System.currentTimeMillis()}); } /** * Sending of the currently saved tone. * @param callPeer */ public void stopSendingDTMF(CallPeerSipImpl callPeer) { Object[] toneInfo = currentlyTransmittingTones.remove(callPeer); if(toneInfo != null) { try { long startTime = (Long)toneInfo[1]; sayInfo(callPeer, (DTMFTone) toneInfo[0], System.currentTimeMillis() - startTime); } catch (OperationFailedException ex) { logger.error("Error stoping dtmf "); } } } /** * This is just a copy of the bye method from the OpSetBasicTelephony, * which was enhanced with a body in order to send the DTMF tone * * @param callPeer destination of the DTMF tone * @param dtmftone DTMF tone to send * @param duration the duration of the tone * @throws OperationFailedException */ private void sayInfo(CallPeerSipImpl callPeer, DTMFTone dtmftone, long duration) throws OperationFailedException { Request info = pps.getMessageFactory().createRequest( callPeer.getDialog(), Request.INFO); //here we add the body ContentType ct = new ContentType(CONTENT_TYPE, CONTENT_SUB_TYPE); String content = "Signal=" + dtmftone.getValue() + "\r\nDuration=" + duration + "\r\n"; ContentLength cl = new ContentLength(content.length()); info.setContentLength(cl); try { info.setContent(content.getBytes(), ct); } catch (ParseException ex) { logger.error("Failed to construct the INFO request", ex); throw new OperationFailedException( "Failed to construct a client the INFO request" , OperationFailedException.INTERNAL_ERROR , ex); } //body ended ClientTransaction clientTransaction = null; try { clientTransaction = callPeer.getJainSipProvider() .getNewClientTransaction(info); } catch (TransactionUnavailableException ex) { logger.error( "Failed to construct a client transaction from the INFO request" , ex); throw new OperationFailedException( "Failed to construct a client transaction from the INFO request" , OperationFailedException.INTERNAL_ERROR , ex); } try { if (callPeer.getDialog().getState() == DialogState.TERMINATED) { //this is probably because the call has just ended, so don't //throw an exception. simply log and get lost. logger.warn("Trying to send a dtmf tone inside a " +"TERMINATED dialog."); return; } callPeer.getDialog().sendRequest(clientTransaction); if (logger.isDebugEnabled()) logger.debug("sent request:\n" + info); } catch (SipException ex) { throw new OperationFailedException( "Failed to send the INFO request" , OperationFailedException.NETWORK_FAILURE , ex); } } /** * Just look if the DTMF signal was well received, and log it * * @param responseEvent the response event * @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 */ @Override public boolean processResponse(ResponseEvent responseEvent) { boolean processed = false; if (responseEvent == null) { if (logger.isDebugEnabled()) logger.debug("null responseEvent"); } else { Response response = responseEvent.getResponse(); if (response == null) { if (logger.isDebugEnabled()) logger.debug("null response"); } else { // Is it even for us? ClientTransaction clientTransaction = responseEvent.getClientTransaction(); if (clientTransaction == null) { if (logger.isDebugEnabled()) logger.debug("null clientTransaction"); } else { Request request = clientTransaction.getRequest(); if (request == null) { if (logger.isDebugEnabled()) logger.debug("null request"); } else { ContentTypeHeader contentTypeHeader = (ContentTypeHeader) request.getHeader(ContentTypeHeader.NAME); if ((contentTypeHeader != null) && CONTENT_TYPE.equalsIgnoreCase( contentTypeHeader.getContentType()) && CONTENT_SUB_TYPE.equalsIgnoreCase( contentTypeHeader.getContentSubType())) { processed = true; int statusCode = response.getStatusCode(); if (statusCode == 200) { if (logger.isDebugEnabled()) logger.debug( "DTMF send succeeded: " + statusCode); } else logger.error("DTMF send failed: " + statusCode); } } } } } return processed; } /** * Receives dtmf info requests. */ @Override public boolean processRequest(RequestEvent requestEvent) { Request request = requestEvent.getRequest(); ContentTypeHeader contentTypeHeader = (ContentTypeHeader) request.getHeader(ContentTypeHeader.NAME); if ((contentTypeHeader != null) && CONTENT_TYPE.equalsIgnoreCase( contentTypeHeader.getContentType()) && CONTENT_SUB_TYPE.equalsIgnoreCase( contentTypeHeader.getContentSubType())) { try { byte[] value; Object valueObj = request.getContent(); if(valueObj instanceof String) value = ((String)valueObj).getBytes("UTF-8"); else if(valueObj instanceof byte[]) value = (byte[])valueObj; else { logger.error("Unknown content type"); return false; } Properties prop = new Properties(); prop.load(new ByteArrayInputStream(value)); String signal = prop.getProperty("Signal"); String durationStr = prop.getProperty("Duration"); DTMFTone tone = DTMFTone.getDTMFTone(signal); if(tone == null) { logger.warn("Unknown tone received: " + tone); return false; } long duration = 0; try { duration = Long.parseLong(durationStr); } catch(NumberFormatException ex) { logger.warn("Error parsing duration:" + durationStr, ex); } // fire event fireToneEvent(tone, duration); } catch(IOException ioe) {} Response responseOK; try { responseOK = pps.getMessageFactory().createResponse( Response.OK, requestEvent.getRequest()); } catch (ParseException ex) { //What else could we do apart from logging? logger.warn("Failed to create OK for incoming INFO request", ex); return false; } try { SipStackSharing.getOrCreateServerTransaction(requestEvent). sendResponse(responseOK); } catch(TransactionUnavailableException ex) { if (logger.isInfoEnabled()) logger.info("Failed to respond to an incoming " +"transactionless INFO request"); if (logger.isTraceEnabled()) logger.trace("Exception was:", ex); return false; } catch (InvalidArgumentException ex) { //What else could we do apart from logging? logger.warn("Failed to send OK for incoming INFO request", ex); return false; } catch (SipException ex) { //What else could we do apart from logging? logger.warn("Failed to send OK for incoming INFO request", ex); return false; } return true; } return false; } /** * Fire event to interested listeners. * @param tone to go into event. * @param duration of the tone. */ private void fireToneEvent(DTMFTone tone, long duration) { Collection<DTMFListener> listeners; synchronized (this.dtmfListeners) { listeners = new ArrayList<DTMFListener>(this.dtmfListeners); } DTMFReceivedEvent evt = new DTMFReceivedEvent(pps, tone, duration); if (logger.isDebugEnabled()) logger.debug("Dispatching DTMFTone Listeners=" + listeners.size() + " evt=" + evt); try { for (DTMFListener listener : listeners) { listener.toneReceived(evt); } } catch (Throwable e) { logger.error("Error delivering dtmf tone", e); } } /** * Registers the specified DTMFListener with this provider so that it could * be notified when incoming DTMF tone is received. * @param listener the listener to register with this provider. * */ public void addDTMFListener(DTMFListener listener) { synchronized (dtmfListeners) { if (!dtmfListeners.contains(listener)) { dtmfListeners.add(listener); } } } /** * Removes the specified listener from the list of DTMF listeners. * @param listener the listener to unregister. */ public void removeDTMFListener(DTMFListener listener) { synchronized (dtmfListeners) { dtmfListeners.remove(listener); } } }