/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2015, Telestax Inc and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* For questions related to commercial use licensing, please contact sales@telestax.com.
*
*/
package org.restcomm.android.sdk.SignalingClient.JainSipClient;
import android.gov.nist.javax.sip.ResponseEventExt;
import android.gov.nist.javax.sip.message.SIPMessage;
import android.javax.sip.ClientTransaction;
import android.javax.sip.Dialog;
import android.javax.sip.DialogState;
import android.javax.sip.RequestEvent;
import android.javax.sip.ResponseEvent;
import android.javax.sip.ServerTransaction;
import android.javax.sip.SipException;
import android.javax.sip.TimeoutEvent;
import android.javax.sip.Transaction;
import android.javax.sip.header.CSeqHeader;
import android.javax.sip.header.ToHeader;
import android.javax.sip.message.Request;
import android.javax.sip.message.Response;
import org.restcomm.android.sdk.RCClient;
import org.restcomm.android.sdk.util.RCLogger;
import java.util.HashMap;
// Represents a call
public class JainSipCall {
// Interface the JainSipCall listener needs to implement, to get events from us
public interface JainSipCallListener {
void onCallOutgoingPeerRingingEvent(String jobId);
void onCallOutgoingConnectedEvent(String jobId, String sdpAnswer, HashMap<String, String> customHeaders);
void onCallIncomingConnectedEvent(String jobId);
void onCallLocalDisconnectedEvent(String jobId);
void onCallPeerDisconnectedEvent(String jobId);
// cancel was received and answered
void onCallIncomingCanceledEvent(String jobId);
// TODO: for when we implement call ignore functionality
// we ignored the call
void onCallIgnoredEvent(String jobId);
void onCallErrorEvent(String jobId, RCClient.ErrorCodes status, String text);
void onCallArrivedEvent(String jobId, String peer, String sdpOffer, HashMap<String, String> customHeaders);
void onCallDigitsEvent(String jobId, RCClient.ErrorCodes status, String text);
}
JainSipClient jainSipClient;
JainSipCallListener listener;
//String jobId;
static final String TAG = "JainSipCall";
JainSipCall(JainSipClient jainSipClient, JainSipCallListener listener)
{
this.jainSipClient = jainSipClient;
this.listener = listener;
}
// make a call with the given jobId, using given parameters
public void open(String jobId, HashMap<String, Object> parameters)
{
RCLogger.i(TAG, "open(): id: " + jobId + ", parameters: " + parameters.toString());
try {
Transaction transaction = jainSipCallInvite(parameters);
jainSipClient.jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_CALL, transaction, parameters, this);
}
catch (JainSipException e) {
e.printStackTrace();
listener.onCallErrorEvent(jobId, e.errorCode, e.errorText);
jainSipClient.jainSipJobManager.remove(jobId);
}
}
// accept a call with the given jobId, using given parameters
public void accept(JainSipJob jainSipJob, HashMap<String, Object> parameters)
{
RCLogger.i(TAG, "accept(): jobId: " + jainSipJob.jobId + ", parameters: " + parameters.toString());
try {
jainSipCallAccept(jainSipJob, parameters);
}
catch (JainSipException e) {
e.printStackTrace();
listener.onCallErrorEvent(jainSipJob.jobId, e.errorCode, e.errorText);
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
}
// Send DTMF digits over this call
public void sendDigits(JainSipJob jainSipJob, String digits)
{
RCLogger.i(TAG, "sendDigits(): jobId: " + jainSipJob.jobId + ", digits: " + digits);
if (!jainSipClient.jainSipNotificationManager.haveConnectivity()) {
listener.onCallDigitsEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY));
return;
}
try {
jainSipCallSendDigits(jainSipJob, digits);
}
catch (JainSipException e) {
e.printStackTrace();
listener.onCallDigitsEvent(jainSipJob.jobId, e.errorCode, e.errorText);
}
}
// Close an existing call. The actual SIP request emitted depends on current state: a. If its an early incoming call we Decline, b. If its an early outgoing
// call we Cancel and c. On any other case we Bye
public void disconnect(JainSipJob jainSipJob, String reason)
{
RCLogger.i(TAG, "close(): jobId: " + jainSipJob.jobId);
try {
if (jainSipJob.transaction.getDialog().getState() == null ||
jainSipJob.transaction.getDialog().getState() == DialogState.EARLY) {
if (jainSipJob.transaction.getDialog().isServer()) {
// server transaction (i.e. incoming call)
RCLogger.v(TAG, "close(): jobId " + jainSipJob.jobId + " - Early dialog state for incoming call, sending Decline");
jainSipCallDecline(jainSipJob);
listener.onCallLocalDisconnectedEvent(jainSipJob.jobId);
// we are done with this call, let's remove job
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
else {
// client transaction (i.e. outgoing call)
RCLogger.v(TAG, "close(): jobId " + jainSipJob.jobId + " - Early dialog state for outgoing call, sending Cancel");
// if we haven't received 200 OK to our invite yet, we need to cancel
jainSipCallCancel(jainSipJob);
}
}
else {
RCLogger.v(TAG, "close(): jobId " + jainSipJob.jobId + " - Confirmed dialog state, sending Bye");
jainSipCallHangup(jainSipJob, jainSipClient.configuration, reason);
}
}
catch (JainSipException e) {
e.printStackTrace();
listener.onCallErrorEvent(jainSipJob.jobId, e.errorCode, e.errorText);
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
}
// ------ Internal APIs
public ClientTransaction jainSipCallInvite(final HashMap<String, Object> parameters) throws JainSipException
{
RCLogger.v(TAG, "jainSipCallInvite()");
ClientTransaction transaction = null;
try {
Request inviteRequest = jainSipClient.jainSipMessageBuilder.buildInviteRequest(jainSipClient.jainSipListeningPoint, parameters, jainSipClient.configuration, jainSipClient.jainSipClientContext);
RCLogger.i(TAG, "Sending SIP request: \n" + inviteRequest.toString());
transaction = jainSipClient.jainSipProvider.getNewClientTransaction(inviteRequest);
transaction.sendRequest();
}
catch (JainSipException e) {
throw e;
}
catch (Exception e) {
// DNS error (error resolving registrar URI)
throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_COULD_NOT_CONNECT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_COULD_NOT_CONNECT), e);
}
return transaction;
}
public void jainSipCallAccept(JainSipJob jainSipJob, HashMap<String, Object> parameters) throws JainSipException
{
RCLogger.v(TAG, "jainSipCallAccept(): jobId: " + jainSipJob.jobId);
try {
ServerTransaction transaction = (ServerTransaction) jainSipJob.transaction;
Response response = jainSipClient.jainSipMessageBuilder.buildInvite200OKResponse(transaction, (String) parameters.get("sdp"), jainSipClient.jainSipListeningPoint,
jainSipClient.jainSipClientContext);
RCLogger.i(TAG, "Sending SIP response: \n" + response.toString());
transaction.sendResponse(response);
}
catch (JainSipException e) {
throw e;
}
catch (Exception e) {
throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_ACCEPT_FAILED,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_ACCEPT_FAILED), e);
}
}
public ClientTransaction jainSipCallHangup(JainSipJob jainSipJob, HashMap<String, Object> clientConfiguration, String reason) throws JainSipException
{
RCLogger.v(TAG, "jainSipCallHangup(): jobId: " + jainSipJob.jobId);
Request byeRequest = null;
try {
byeRequest = jainSipClient.jainSipMessageBuilder.buildByeRequest(jainSipJob.transaction.getDialog(), reason, clientConfiguration);
RCLogger.i(TAG, "Sending SIP request: \n" + byeRequest.toString());
ClientTransaction transaction = jainSipClient.jainSipProvider.getNewClientTransaction(byeRequest);
jainSipJob.transaction.getDialog().sendRequest(transaction);
// update transaction in the job to contain the latest transaction
jainSipJob.updateTransaction(transaction);
return transaction;
}
catch (SipException e) {
throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_FAILED,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_FAILED), e);
}
catch (Exception e) {
throw new RuntimeException("SIP transaction error", e);
}
}
public ClientTransaction jainSipCallCancel(JainSipJob jainSipJob) throws JainSipException
{
RCLogger.v(TAG, "jainSipCallCancel(): jobId: " + jainSipJob.jobId);
try {
final Request request = ((ClientTransaction) jainSipJob.transaction).createCancel();
RCLogger.i(TAG, "Sending SIP response: \n" + request.toString());
ClientTransaction cancelTransaction = jainSipClient.jainSipProvider.getNewClientTransaction(request);
//jainSipJob.updateTransaction(cancelTransaction);
cancelTransaction.sendRequest();
return cancelTransaction;
}
catch (SipException e) {
throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_FAILED,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_FAILED), e);
}
}
public void jainSipCallDecline(JainSipJob jainSipJob) throws JainSipException
{
RCLogger.v(TAG, "jainSipCallReject(): jobId: " + jainSipJob.jobId);
try {
Response responseDecline = jainSipClient.jainSipMessageBuilder.buildResponse(Response.DECLINE, jainSipJob.transaction.getRequest());
RCLogger.i(TAG, "Sending SIP response: \n" + responseDecline.toString());
((ServerTransaction) jainSipJob.transaction).sendResponse(responseDecline);
}
catch (Exception e) {
throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_DECLINE_FAILED,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DECLINE_FAILED), e);
}
}
public ClientTransaction jainSipCallSendDigits(JainSipJob jainSipJob, String digits) throws JainSipException
{
RCLogger.v(TAG, "jainSipCallSendDigits()");
try {
Dialog dialog = jainSipJob.transaction.getDialog();
Request request = jainSipClient.jainSipMessageBuilder.buildDtmfInfoRequest(dialog, digits);
RCLogger.i(TAG, "Sending SIP request: \n" + request.toString());
ClientTransaction transaction = jainSipClient.jainSipProvider.getNewClientTransaction(request);
dialog.sendRequest(transaction);
return transaction;
}
catch (Exception e) {
throw new JainSipException(RCClient.ErrorCodes.ERROR_CONNECTION_DTMF_DIGITS_FAILED,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DTMF_DIGITS_FAILED), e);
}
}
// Let's follow the naming conventions of SipListener, even though JainSipCall doesn't implement it, to keep it easier to follow
public void processRequest(JainSipJob jainSipJob, final RequestEvent requestEvent)
{
ServerTransaction serverTransaction = requestEvent.getServerTransaction();
Request request = requestEvent.getRequest();
String method = request.getMethod();
if (method.equals(Request.BYE)) {
try {
Response response = jainSipClient.jainSipMessageBuilder.buildResponse(Response.OK, request);
RCLogger.i(TAG, "Sending SIP response: \n" + response.toString());
serverTransaction.sendResponse(response);
listener.onCallPeerDisconnectedEvent(jainSipJob.jobId);
// we are done with this call, let's remove job
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
catch (Exception e) {
// TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then
// we can adjust and only do a e.printStackTrace()
throw new RuntimeException("Failed to respond to Bye request", e);
}
}
else if (method.equals(Request.CANCEL)) {
try {
Response response = jainSipClient.jainSipMessageBuilder.buildResponse(Response.OK, request);
RCLogger.i(TAG, "Sending SIP response: \n" + response.toString());
serverTransaction.sendResponse(response);
if (jainSipJob.transaction != null) {
// also send a 487 Request Terminated response to the original INVITE request
Request originalInviteRequest = jainSipJob.transaction.getRequest();
Response originalInviteResponse = jainSipClient.jainSipMessageBuilder.buildResponse(Response.REQUEST_TERMINATED, originalInviteRequest);
RCLogger.i(TAG, "Sending SIP response: \n" + originalInviteResponse.toString());
((ServerTransaction) jainSipJob.transaction).sendResponse(originalInviteResponse);
}
listener.onCallIncomingCanceledEvent(jainSipJob.jobId);
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
catch (Exception e) {
// TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then
// we can adjust and only do a e.printStackTrace()
throw new RuntimeException("Failed to respond to Cancel request", e);
}
}
else if (method.equals(Request.INVITE)) {
try {
// Remember that requestEvent ServerTransaction is null for new Dialogs
if (serverTransaction == null) {
serverTransaction = jainSipClient.jainSipProvider.getNewServerTransaction(request);
}
jainSipJob.updateTransaction(serverTransaction);
Response response = jainSipClient.jainSipMessageBuilder.buildResponse(Response.RINGING, request);
// Important: we need set the 'tag' for the 'To' (once that happens Dialog transitions to EARLY)
ToHeader toHeader = (ToHeader)request.getHeader(ToHeader.NAME);
toHeader.setTag(Long.toString(System.currentTimeMillis()));
response.setHeader(toHeader);
RCLogger.i(TAG, "Sending SIP response: \n" + response.toString());
serverTransaction.sendResponse(response);
String sdpOffer = new String(request.getRawContent(), "UTF-8");
listener.onCallArrivedEvent(jainSipJob.jobId, ((SIPMessage) request).getFrom().getAddress().toString(), sdpOffer, JainSipMessageBuilder.parseCustomHeaders(request));
}
catch (Exception e) {
// TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then
// we can adjust and only do a e.printStackTrace()
throw new RuntimeException("Failed to send Ringing to incoming Invite", e);
}
}
else if (method.equals(Request.ACK)) {
// A dialog transitions to the "confirmed" state when a 2xx final response is received to the INVITE Request
if (serverTransaction.getDialog().getState() == DialogState.CONFIRMED) {
listener.onCallIncomingConnectedEvent(jainSipJob.jobId);
}
else {
RCLogger.e(TAG, "Received ACK for dialog not in Confirmed state: \n" + serverTransaction.getDialog().getState());
}
}
}
public void processResponse(JainSipJob jainSipJob, final ResponseEvent responseEvent)
{
ResponseEventExt responseEventExt = (ResponseEventExt) responseEvent;
Response response = responseEvent.getResponse();
CSeqHeader cseq = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
String method = cseq.getMethod();
if (response.getStatusCode() == Response.OK) {
if (method.equals(Request.INVITE)) {
try {
// create and send out ACK
Dialog dialog = jainSipJob.transaction.getDialog();
Request ackRequest = dialog.createAck(((CSeqHeader) response.getHeader(CSeqHeader.NAME)).getSeqNumber());
RCLogger.i(TAG, "Sending SIP request: \n" + ackRequest.toString());
dialog.sendAck(ackRequest);
// filter out SDP to return to UI thread
String sdpAnswer = new String(response.getRawContent(), "UTF-8");
// Let's leave this around in case we want to parse the non-webrtc media port in the future
//SDPAnnounceParser parser = new SDPAnnounceParser(sdpContent);
//SessionDescriptionImpl sessiondescription = parser.parse();
//MediaDescription incomingMediaDescriptor = (MediaDescription) sessiondescription.getMediaDescriptions(false).get(0);
//int rtpPort = incomingMediaDescriptor.getMedia().getMediaPort();
// if its a webrtc call we need to send back the full SDP
listener.onCallOutgoingConnectedEvent(jainSipJob.jobId, sdpAnswer, JainSipMessageBuilder.parseCustomHeaders(response));
}
catch (SipException e) {
listener.onCallErrorEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_CONNECTION_COULD_NOT_CONNECT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_COULD_NOT_CONNECT));
}
catch (Exception e) {
// TODO: let's emit a RuntimeException for now so that we get a loud and clear indication of issues involved in the field and then
// we can adjust and only do a e.printStackTrace()
throw new RuntimeException("Failed to Ack the 200 Ok out outgoing Invite", e);
}
}
else if (method.equals(Request.BYE)) {
listener.onCallLocalDisconnectedEvent(jainSipJob.jobId);
// we are done with this call, let's remove job
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
else if (method.equals(Request.CANCEL)) {
if (responseEvent.getClientTransaction().getDialog().getState() == DialogState.CONFIRMED) {
RCLogger.w(TAG, "processResponse(): Cancel reached peer too late, need to send Bye");
try {
jainSipCallHangup(jainSipJob, jainSipClient.configuration, null);
}
catch (JainSipException e) {
listener.onCallErrorEvent(jainSipJob.jobId, e.errorCode, e.errorText);
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
}
}
else if (method.equals(Request.INFO)) {
listener.onCallDigitsEvent(jainSipJob.jobId, RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS));
}
}
else if (response.getStatusCode() == Response.RINGING) {
listener.onCallOutgoingPeerRingingEvent(jainSipJob.jobId);
}
if (response.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED || response.getStatusCode() == Response.UNAUTHORIZED) {
try {
// important we pass the jainSipClient params in jainSipAuthenticate (instead of jainSipJob.parameters that has the call parameters), because this is where usename/pass reside
jainSipClient.jainSipAuthenticate(jainSipJob, jainSipClient.configuration, responseEventExt);
}
catch (JainSipException e) {
listener.onCallErrorEvent(jainSipJob.jobId, e.errorCode, e.errorText);
}
}
else if (response.getStatusCode() == Response.FORBIDDEN) {
listener.onCallErrorEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_CONNECTION_AUTHENTICATION_FORBIDDEN,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_AUTHENTICATION_FORBIDDEN));
}
else if (response.getStatusCode() == Response.DECLINE || response.getStatusCode() == Response.TEMPORARILY_UNAVAILABLE ||
(response.getStatusCode() == Response.BUSY_HERE)) {
listener.onCallPeerDisconnectedEvent(jainSipJob.jobId);
}
else if (response.getStatusCode() == Response.NOT_FOUND) {
listener.onCallErrorEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_CONNECTION_PEER_NOT_FOUND,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_PEER_NOT_FOUND));
// we don't remove job because right now the flow is such that the client disconnects after this event
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
else if (response.getStatusCode() == Response.SERVICE_UNAVAILABLE) {
listener.onCallErrorEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_CONNECTION_SERVICE_UNAVAILABLE,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_SERVICE_UNAVAILABLE));
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
else if (response.getStatusCode() == Response.REQUEST_TERMINATED) {
if (method.equals(Request.INVITE)) {
// INVITE was terminated by Cancel
listener.onCallLocalDisconnectedEvent(jainSipJob.jobId);
// we are done with this call, let's remove job
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
}
else if (response.getStatusCode() == Response.SERVER_INTERNAL_ERROR) {
if (method.equals(Request.BYE)) {
listener.onCallErrorEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_CONNECTION_SERVICE_INTERNAL_ERROR,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_SERVICE_INTERNAL_ERROR));
// we are done with this call, let's remove job
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
}
else {
RCLogger.e(TAG, "processResponse(): unhandled SIP response: " + response.getStatusCode());
}
/*
else if (response.getStatusCode() == Response.REQUEST_TERMINATED) {
// INVITE was terminated by Cancel
listener.onCallLocalDisconnectedEvent(jainSipJob.jobId);
// we are done with this call, let's remove job
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
*/
// Notice that we 're not handling '200 Canceling' response as it doesn't add any value to the SDK, at least for now
}
public void processTimeout(JainSipJob jainSipJob, final TimeoutEvent timeoutEvent)
{
listener.onCallErrorEvent(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_CONNECTION_SIGNALING_TIMEOUT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_SIGNALING_TIMEOUT));
jainSipClient.jainSipJobManager.remove(jainSipJob.jobId);
}
}