/* * 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.content.Context; import android.gov.nist.javax.sip.ResponseEventExt; import android.gov.nist.javax.sip.SipStackExt; import android.gov.nist.javax.sip.clientauthutils.AuthenticationHelper; import android.gov.nist.javax.sip.message.SIPMessage; import android.javax.sip.ClientTransaction; import android.javax.sip.DialogTerminatedEvent; import android.javax.sip.IOExceptionEvent; import android.javax.sip.ListeningPoint; import android.javax.sip.ObjectInUseException; import android.javax.sip.RequestEvent; import android.javax.sip.ResponseEvent; import android.javax.sip.ServerTransaction; import android.javax.sip.SipException; import android.javax.sip.SipFactory; import android.javax.sip.SipListener; import android.javax.sip.SipProvider; import android.javax.sip.SipStack; import android.javax.sip.TimeoutEvent; import android.javax.sip.Transaction; import android.javax.sip.TransactionTerminatedEvent; import android.javax.sip.header.CSeqHeader; import android.javax.sip.header.CallIdHeader; import android.javax.sip.header.ViaHeader; import android.javax.sip.message.Request; import android.javax.sip.message.Response; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.SystemClock; import android.text.format.Formatter; //import org.apache.http.conn.util.InetAddressUtils; import org.restcomm.android.sdk.RCClient; import org.restcomm.android.sdk.RCConnection; import org.restcomm.android.sdk.RCDevice; import org.restcomm.android.sdk.RCDeviceListener; import org.restcomm.android.sdk.util.RCLogger; import java.io.File; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Properties; /** * JainSipClient hides the JAIN SIP complexity and offers an easy to use API to implement SIP signaling. JainSipClient is typically used by Signaling Handler * that encapsulates the separate, signaling thread. JainSipClient creates SIP requests via methods like open(), close(), call(), etc and replies or events * are sent back to the 'user' via listener callbacks. * * Although all signaling functionality is accessed via the JainSipClient entity, internally we have introduced JainSipCall that handles call related functionalities * to better organize things. This means that call related APIs like call(), accept(), disconnect(), etc are forwarded to JainSipCall. In the reverse direction when * a response/event comes in from JAIN SIP it is firstly received by JainSipClient (since its the SipListener for JAIN SIP), but then if it identifies that it is call * related, it is once again forwarded to JainSipCall. * * With each of the following signaling actions: open(), reconfigure(), close(), sendMessage() and call() a JainSipJob is created that carries around its context until * it is either finished or an error occurs. The rest of the signaling actions like accept(), sendDtmf(), don't create new jobs. Instead they act on existing ones. * Main pieces of the job are the current transaction. Remember that a single job can consist of more than one SIP transaction. For example the TYPE_RECONFIGURE job consists * of an unregister transaction, followed by an authentication transaction, followed by register transaction, followed by an authentication transaction. Some jobs are also * associated with a JainSipFsm object that implements a simple state machine to be able to properly address invoking the same functionalities in different job contexts * without losing track and at the same time notifying the correct UI entities of job status. Right now JainSipFsm is only used in complex jobs that need to batch multiple * transactions together, like: TYPE_OPEN, TYPE_REGISTER_REFRESH, TYPE_CLOSE, TYPE_RECONFIGURE, TYPE_RECONFIGURE_RELOAD_NETWORKING, TYPE_RELOAD_NETWORKING, TYPE_START_NETWORKING, * TYPE_CALL. Jobs are managed by JainSipJobManager * * JAIN SIP requests and responses are typically built by JainSipMessageBuilder */ public class JainSipClient implements SipListener, JainSipNotificationManager.NotificationManagerListener { // Interface the JainSipClient listener needs to implement, to get events from us public interface JainSipClientListener { void onClientOpenedReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text); // on successful/unsuccessful register, onPrivateClientConnectorOpenedEvent void onClientErrorReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text); // mostly on unsuccessful register, onPrivateClientConnectorOpenErrorEvent void onClientClosedEvent(String jobId, RCClient.ErrorCodes status, String text); // on successful unregister, onPrivateClientConnectorClosedEvent void onClientReconfigureReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text); // on successful register, onPrivateClientConnectorOpenedEvent void onClientConnectivityEvent(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus); void onClientMessageArrivedEvent(String jobId, String peer, String messageText); void onClientMessageReply(String jobId, RCClient.ErrorCodes status, String text); // Event to convey trying to Register, so that UI can convey that to user void onClientRegisteringEvent(String jobId); } public JainSipClientListener listener; JainSipMessageBuilder jainSipMessageBuilder; JainSipJobManager jainSipJobManager; JainSipNotificationManager jainSipNotificationManager; private Context androidContext; HashMap<String, Object> configuration; // any client context that is not configuration related, like the rport HashMap<String, Object> jainSipClientContext; //boolean clientConnected = false; private static boolean clientOpened = false; private static final String TAG = "JainSipClient"; // android handler token to identify registration refresh posts private final int REGISTER_REFRESH_HANDLER_TOKEN = 1; Handler signalingHandler; // Register expiry in seconds private final int DEFAULT_REGISTER_EXPIRY_PERIOD = 3600; private final int DEFAULT_LOCAL_SIP_PORT = 5090; // the registration refresh needs to happen sooner than expiry to make sure that the client has a registration at all times. Let's // set it to EXPIRY - 50 seconds. TODO: in the future we could randomize this so that for example it is between half the expiry // and full expiry (in this example, a random between [30, 60] seconds) to avoid having all android clients refreshing all at // the same time and stressing Restcomm. Actually this is how Sofia SIP in restcomm-ios-sdk does it by default. Value is in seconds final int REGISTER_REFRESH_MINUS_INTERVAL = 50; // how long after we force close the client if it takes too long to process JainSipClient.close() static final int FORCE_CLOSE_INTERVAL = 3000; // JAIN SIP entities public SipFactory jainSipFactory; public SipStack jainSipStack; public ListeningPoint jainSipListeningPoint; public SipProvider jainSipProvider; public JainSipClient(Handler signalingHandler) { this.signalingHandler = signalingHandler; } // -- Published API public void open(String jobId, Context androidContext, HashMap<String, Object> configuration, JainSipClientListener listener) { RCLogger.i(TAG, "open(): " + configuration.toString()); if (JainSipClient.clientOpened) { listener.onClientOpenedReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, RCClient.ErrorCodes.ERROR_DEVICE_ALREADY_OPEN, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_ALREADY_OPEN)); return; } this.listener = listener; this.androidContext = androidContext; this.configuration = configuration; jainSipMessageBuilder = new JainSipMessageBuilder(); jainSipJobManager = new JainSipJobManager(this); jainSipNotificationManager = new JainSipNotificationManager(androidContext, signalingHandler, this); jainSipClientContext = new HashMap<String, Object>(); jainSipFactory = SipFactory.getInstance(); jainSipFactory.resetFactory(); jainSipFactory.setPathName("android.gov.nist"); Properties properties = new Properties(); properties.setProperty("android.javax.sip.STACK_NAME", "androidSip"); properties.setProperty("android.gov.nist.javax.sip.MESSAGE_PROCESSOR_FACTORY", "android.gov.nist.javax.sip.stack.NioMessageProcessorFactory"); // Setup TLS even if currently we aren't using it, so that if user changes the setting later // the SIP stack is ready to support it String keystoreFilename = "restcomm-android.keystore"; HashMap<String, String> securityParameters = JainSipSecurityHelper.generateKeystore(androidContext, keystoreFilename); JainSipSecurityHelper.setProperties(properties, securityParameters.get("keystore-path"), securityParameters.get("keystore-password"), (Boolean)configuration.get(RCDevice.ParameterKeys.DEBUG_JAIN_DISABLE_CERTIFICATE_VERIFICATION)); if (configuration.containsKey(RCDevice.ParameterKeys.DEBUG_JAIN_SIP_LOGGING_ENABLED) && (Boolean)configuration.get(RCDevice.ParameterKeys.DEBUG_JAIN_SIP_LOGGING_ENABLED)) { // You need 16 for logging traces. 32 for debug + traces. // Your code will limp at 32 but it is best for debugging. properties.setProperty("android.gov.nist.javax.sip.TRACE_LEVEL", "32"); File downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); properties.setProperty("android.gov.nist.javax.sip.DEBUG_LOG", downloadPath.getAbsolutePath() + "/debug-jain.log"); properties.setProperty("android.gov.nist.javax.sip.SERVER_LOG", downloadPath.getAbsolutePath() + "/server-jain.log"); } try { jainSipStack = jainSipFactory.createSipStack(properties); jainSipMessageBuilder.normalizeDomain(configuration); jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_OPEN, configuration); } catch (SipException e) { throw new RuntimeException("Failed to bootstrap the signaling stack", e); /* listener.onClientOpenedReply(jobId, jainSipNotificationManager.getConnectivityStatus(), RCClient.ErrorCodes.ERROR_SIGNALING_SIP_STACK_BOOTSTRAP, RCClient.errorText(RCClient.ErrorCodes.ERROR_SIGNALING_SIP_STACK_BOOTSTRAP)); */ } } public void close(final String jobId) { RCLogger.i(TAG, "close(): " + jobId); if (JainSipClient.clientOpened) { // cancel any pending scheduled registrations //signalingHandler.removeCallbacksAndMessages(REGISTER_REFRESH_HANDLER_TOKEN); // TODO: close any active calls // jainSipNotificationManager.close(); jainSipJobManager.removeAll(); if (configuration.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) && !configuration.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) { // non registrar-less, we need to unregister and when done shutdown jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_CLOSE, this.configuration); } else { // registrar-less, just shutdown and notify UI thread try { jainSipClientUnbind(); jainSipClientStopStack(); listener.onClientClosedEvent(jobId, RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS)); } catch (JainSipException e) { e.printStackTrace(); listener.onClientClosedEvent(jobId, e.errorCode, e.errorText); } } } else { throw new RuntimeException("JainSipClient already closed, bailing"); /* RCLogger.w(TAG, "close(): JAIN SIP client already closed, bailing"); listener.onClientClosedEvent(jobId, RCClient.ErrorCodes.ERROR_SIGNALING_SIP_STACK_BOOTSTRAP, RCClient.errorText(RCClient.ErrorCodes.ERROR_SIGNALING_SIP_STACK_BOOTSTRAP)); */ } } public void reconfigure(String jobId, HashMap<String, Object> parameters, JainSipClientListener listener) { RCLogger.i(TAG, "reconfigure(): " + parameters.toString()); // normalize before checking which parameters changed jainSipMessageBuilder.normalizeDomain(parameters); // check which parameters actually changed by comparing this.configuration with parameters HashMap<String, Object> modifiedParameters = JainSipConfiguration.modifiedParameters(this.configuration, parameters); if (modifiedParameters.size() == 0) { listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipNotificationManager.getNetworkStatus()), RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS)); return; } HashMap<String, Object> oldParameters = new HashMap<String, Object>(); oldParameters.putAll(configuration); // remember that the new parameters can be just a subset of the currently stored in configuration, so to update the current parameters we need // to merge them with the new (i.e. keep the old and replace any new keys with new values) configuration = JainSipConfiguration.mergeParameters(configuration, parameters); // Set the media parameters right away, since they are irrelevant to signaling if (modifiedParameters.containsKey(RCDevice.ParameterKeys.MEDIA_TURN_ENABLED)) { this.configuration.put(RCDevice.ParameterKeys.MEDIA_TURN_ENABLED, modifiedParameters.get(RCDevice.ParameterKeys.MEDIA_TURN_ENABLED)); } if (modifiedParameters.containsKey(RCDevice.ParameterKeys.MEDIA_ICE_URL)) { this.configuration.put(RCDevice.ParameterKeys.MEDIA_ICE_URL, modifiedParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_URL)); } if (modifiedParameters.containsKey(RCDevice.ParameterKeys.MEDIA_ICE_USERNAME)) { this.configuration.put(RCDevice.ParameterKeys.MEDIA_ICE_USERNAME, modifiedParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_USERNAME)); } if (modifiedParameters.containsKey(RCDevice.ParameterKeys.MEDIA_ICE_PASSWORD)) { this.configuration.put(RCDevice.ParameterKeys.MEDIA_ICE_PASSWORD, modifiedParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_PASSWORD)); } if (modifiedParameters.containsKey(RCDevice.ParameterKeys.MEDIA_ICE_DOMAIN)) { this.configuration.put(RCDevice.ParameterKeys.MEDIA_ICE_DOMAIN, modifiedParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_DOMAIN)); } HashMap<String, Object> multipleParameters = new HashMap<String, Object>(); multipleParameters.put("old-parameters", oldParameters); multipleParameters.put("new-parameters", configuration); if (modifiedParameters.containsKey(RCDevice.ParameterKeys.SIGNALING_SECURE_ENABLED)) { // if signaling secure has changed we need to a. unregister from old using old creds, b. unbind, c. bind, d. register with new using new creds // start FSM and pass it both previous and current parameters jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_RECONFIGURE_RELOAD_NETWORKING, multipleParameters); } else if (modifiedParameters.containsKey(RCDevice.ParameterKeys.SIGNALING_USERNAME) || modifiedParameters.containsKey(RCDevice.ParameterKeys.SIGNALING_PASSWORD) || modifiedParameters.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN)) { // if username, password or domain has changed we need to a. unregister from old using old creds and b. register with new using new creds // start FSM and pass it both previous and current parameters jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_RECONFIGURE, multipleParameters); } else { listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipNotificationManager.getNetworkStatus()), RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS)); } } // ------ Call-related methods public void call(String jobId, HashMap<String, Object> parameters, JainSipCall.JainSipCallListener listener) { RCLogger.i(TAG, "call(): jobId: " + jobId + ", username: " + parameters.toString()); if (!jainSipNotificationManager.haveConnectivity()) { listener.onCallErrorEvent(jobId, RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); return; } try { jainSipMessageBuilder.normalizePeer(parameters, configuration); JainSipCall jainSipCall = new JainSipCall(this, listener); jainSipCall.open(jobId, parameters); } catch (JainSipException e) { listener.onCallErrorEvent(jobId, e.errorCode, e.errorText); } } public void accept(String jobId, HashMap<String, Object> parameters, JainSipCall.JainSipCallListener listener) { RCLogger.i(TAG, "accept(): jobId: " + jobId + ", parameters: " + parameters.toString()); if (!jainSipNotificationManager.haveConnectivity()) { listener.onCallErrorEvent(jobId, RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); return; } JainSipJob jainSipJob = jainSipJobManager.get(jobId); if (jainSipJob == null) { throw new RuntimeException("Error accepting a call that doesn't exist in job manager, jobId: " + jobId); } jainSipJob.jainSipCall.accept(jainSipJob, parameters); } public void disconnect(String jobId, String reason, JainSipCall.JainSipCallListener listener) { RCLogger.i(TAG, "disconnect(): jobId: " + jobId + ", reason: " + reason); if (!jainSipNotificationManager.haveConnectivity()) { listener.onCallErrorEvent(jobId, RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); return; } JainSipJob jainSipJob = jainSipJobManager.get(jobId); // There are cases where a call legitimately fails before a job is created (an example is when invalid SIP URI is used, where RCConnection will return error // to the User but the user still needs to disconnect() manually), hence this will return null and // in those cases we don't need to do anything if (jainSipJob != null) { jainSipJob.jainSipCall.disconnect(jainSipJob, reason); } else { // let's emit a warning just in case we hit an actual error case with this RCLogger.w(TAG, "disconnect(): job doesn't exist for the call; this can be a valid scenario"); } } public void sendDigits(String jobId, String digits) { RCLogger.i(TAG, "sendDigits(): jobId: " + jobId + ", digits: " + digits); JainSipJob jainSipJob = jainSipJobManager.get(jobId); jainSipJob.jainSipCall.sendDigits(jainSipJob, digits); } // ------ Message-related methods public void sendMessage(String jobId, HashMap<String, Object> parameters) { RCLogger.i(TAG, "sendMessage(): jobId: " + jobId + ", parameters: " + parameters.toString()); if (!jainSipNotificationManager.haveConnectivity()) { listener.onClientMessageReply(jobId, RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); return; } try { jainSipMessageBuilder.normalizePeer(parameters, configuration); Transaction transaction = jainSipClientSendMessage(parameters); jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_MESSAGE, transaction, parameters, null); } catch (JainSipException e) { listener.onClientMessageReply(jobId, e.errorCode, e.errorText); } } // ------ Internal APIs // Setup JAIN networking facilities public void jainSipClientBind(HashMap<String, Object> parameters) throws JainSipException { RCLogger.v(TAG, "bind()"); if (jainSipListeningPoint == null) { if (!jainSipNotificationManager.haveConnectivity()) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); } // new network interface is up, let's retrieve its ip address String transport = "tcp"; if (JainSipConfiguration.getBoolean(configuration, RCDevice.ParameterKeys.SIGNALING_SECURE_ENABLED)) { transport = "tls"; //transport = "wss"; } Integer port = DEFAULT_LOCAL_SIP_PORT; if (parameters.containsKey(RCDevice.ParameterKeys.SIGNALING_LOCAL_PORT)) { port = (Integer) parameters.get(RCDevice.ParameterKeys.SIGNALING_LOCAL_PORT); } try { jainSipListeningPoint = jainSipStack.createListeningPoint(getIPAddress(true), port, transport); jainSipProvider = jainSipStack.createSipProvider(jainSipListeningPoint); jainSipProvider.addSipListener(this); jainSipMessageBuilder.initialize(jainSipFactory, jainSipProvider); } /* catch (SocketException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_SIGNALING_NETWORK_INTERFACE, RCClient.errorText(RCClient.ErrorCodes.ERROR_SIGNALING_NETWORK_INTERFACE), e); } */ catch (Exception e) { // not much the user can do, probably programming error throw new RuntimeException("Failed to set up networking facilities", e); /* throw new JainSipException(RCClient.ErrorCodes.ERROR_SIGNALING_NETWORK_BINDING, RCClient.errorText(RCClient.ErrorCodes.ERROR_SIGNALING_NETWORK_BINDING), e); */ } } else { //RCLogger.e(TAG, "jainSipBind(): Error: Listening point already instantiated"); throw new RuntimeException("Error: listening point already created"); } } // Release JAIN networking facilities public void jainSipClientUnbind() throws JainSipException { RCLogger.v(TAG, "unbind()"); if (jainSipListeningPoint != null) { try { jainSipProvider.removeSipListener(this); if (jainSipProvider.getListeningPoints().length > 1) { RCLogger.e(TAG, "unbind(): Listening Point count > 1: " + jainSipProvider.getListeningPoints().length); } jainSipStack.deleteSipProvider(jainSipProvider); jainSipStack.deleteListeningPoint(jainSipListeningPoint); jainSipListeningPoint = null; } catch (ObjectInUseException e) { throw new RuntimeException("Failed to tear down networking facilities", e); } } } void jainSipClientStartStack() { try { jainSipStack.start(); JainSipClient.clientOpened = true; } catch (SipException e) { throw new RuntimeException("Failed to start the signaling stack", e); } } void jainSipClientStopStack() { jainSipStack.stop(); jainSipMessageBuilder.shutdown(); jainSipFactory.resetFactory(); /* configuration = null; androidContext = null; listener = null; jainSipFsm = null; */ JainSipClient.clientOpened = false; } public ClientTransaction jainSipClientRegister(JainSipJob jainSipJob, final HashMap<String, Object> parameters) throws JainSipException { RCLogger.v(TAG, "jainSipRegister()"); // Debug purposes to track the JainSipJob objects RCLogger.v(TAG, "jainSipRegister(), jobs status: " + jainSipJobManager.getPrintableJobs()); if (!jainSipNotificationManager.haveConnectivity()) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); } int expiry = DEFAULT_REGISTER_EXPIRY_PERIOD; if (parameters.containsKey("signaling-register-expiry") && !parameters.get("signaling-register-expiry").equals("")) { expiry = (Integer) parameters.get("signaling-register-expiry"); if (expiry <= REGISTER_REFRESH_MINUS_INTERVAL) { RCLogger.w(TAG, "jainSipRegister(): Register expiry period too small, using default: " + DEFAULT_REGISTER_EXPIRY_PERIOD); expiry = DEFAULT_REGISTER_EXPIRY_PERIOD; } } ClientTransaction transaction; try { Request registerRequest = jainSipMessageBuilder.buildRegisterRequest(jainSipListeningPoint, expiry, parameters); RCLogger.i(TAG, "Sending SIP request: \n" + registerRequest.toString()); // only notify on registering on specific types of jobs, otherwise we would swamp the App with notifications if (jainSipJob.type == JainSipJob.Type.TYPE_RECONFIGURE || jainSipJob.type == JainSipJob.Type.TYPE_RECONFIGURE_RELOAD_NETWORKING || jainSipJob.type == JainSipJob.Type.TYPE_START_NETWORKING || jainSipJob.type == JainSipJob.Type.TYPE_RELOAD_NETWORKING) { listener.onClientRegisteringEvent(jainSipJob.jobId); } // Remember that this might block waiting for DNS server transaction = this.jainSipProvider.getNewClientTransaction(registerRequest); transaction.sendRequest(); } catch (SipException e) { if (e.getMessage().contains("Trust anchor for certification path not found")) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_UNTRUSTED_SERVER, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_UNTRUSTED_SERVER), e); } else { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_COULD_NOT_CONNECT, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_COULD_NOT_CONNECT), e); } } // cancel any pending scheduled registrations (in case this is an on-demand registration and we end up posting to handler on top of the old) signalingHandler.removeCallbacksAndMessages(REGISTER_REFRESH_HANDLER_TOKEN); // schedule a registration update after 'registrationRefresh' seconds Runnable runnable = new Runnable() { @Override public void run() { jainSipJobManager.add(Long.toString(System.currentTimeMillis()), JainSipJob.Type.TYPE_REGISTER_REFRESH, parameters); } }; signalingHandler.postAtTime(runnable, REGISTER_REFRESH_HANDLER_TOKEN, SystemClock.uptimeMillis() + (expiry - REGISTER_REFRESH_MINUS_INTERVAL) * 1000); return transaction; } public ClientTransaction jainSipClientUnregister(final HashMap<String, Object> parameters) throws JainSipException { RCLogger.v(TAG, "jainSipUnregister()"); if (!jainSipNotificationManager.haveConnectivity()) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY)); } ClientTransaction transaction = null; try { Request registerRequest = jainSipMessageBuilder.buildRegisterRequest(jainSipListeningPoint, 0, parameters); RCLogger.i(TAG, "Sending SIP request: \n" + registerRequest.toString()); // Remember that this might block waiting for DNS server transaction = this.jainSipProvider.getNewClientTransaction(registerRequest); transaction.sendRequest(); } catch (SipException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_COULD_NOT_CONNECT, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_COULD_NOT_CONNECT), e); } // cancel any pending scheduled registrations signalingHandler.removeCallbacksAndMessages(REGISTER_REFRESH_HANDLER_TOKEN); return transaction; } public Transaction jainSipClientSendMessage(final HashMap<String, Object> parameters) throws JainSipException { RCLogger.v(TAG, "jainSipClientSendMessage()"); try { Request request = jainSipMessageBuilder.buildMessageRequest((String) parameters.get(RCConnection.ParameterKeys.CONNECTION_PEER), (String) parameters.get("text-message"), jainSipListeningPoint, configuration); RCLogger.i(TAG, "Sending SIP request: \n" + request.toString()); ClientTransaction transaction = this.jainSipProvider.getNewClientTransaction(request); transaction.sendRequest(); return transaction; } catch (SipException e) { throw new JainSipException(RCClient.ErrorCodes.ERROR_MESSAGE_COULD_NOT_CONNECT, RCClient.errorText(RCClient.ErrorCodes.ERROR_MESSAGE_COULD_NOT_CONNECT), e); } } // Notice that this is used both for registrations and calls public void jainSipAuthenticate(JainSipJob jainSipJob, HashMap<String, Object> parameters, ResponseEventExt responseEventExt) throws JainSipException { try { String password = (String) parameters.get(RCDevice.ParameterKeys.SIGNALING_PASSWORD); if (password == null) { password = ""; } AuthenticationHelper authenticationHelper = ((SipStackExt) jainSipStack).getAuthenticationHelper( new JainSipAccountManagerImpl((String) parameters.get(RCDevice.ParameterKeys.SIGNALING_USERNAME), responseEventExt.getRemoteIpAddress(), password), jainSipMessageBuilder.getHeaderFactory()); // we 're subtracting one since the first attempt has already taken place // (that way we are enforcing MAX_AUTH_ATTEMPTS at most) if (jainSipJob.shouldRetry()) { ClientTransaction authenticationTransaction = authenticationHelper.handleChallenge(responseEventExt.getResponse(), (ClientTransaction) jainSipJob.transaction, jainSipProvider, 5, true); // update previous transaction with authenticationTransaction (remember that previous ended with 407 final response) jainSipJob.updateTransaction(authenticationTransaction); RCLogger.i(TAG, "Sending SIP request: \n" + authenticationTransaction.getRequest().toString()); authenticationTransaction.sendRequest(); jainSipJob.increaseAuthAttempts(); } else { // actually this should not happen. Restcomm should return forbidden if the credentials are wrong and not challenge again throw new RuntimeException("Failed to authenticate after max attempts"); } } catch (SipException e) { // Got exception when trying to register with a non existing domain (for some reason it didn't break in REGISTER, but in AUTH) throw new JainSipException(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_COULD_NOT_CONNECT, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_COULD_NOT_CONNECT), e); } /* 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 authenticate", e); } */ } // ------ SipListener events // Remember that SipListener events run in a separate thread created by JAIN SIP, which makes sharing of resources between our signaling thread and this // JAIN SIP thread a bit difficult. To avoid that let's do the actual handling of these events in the signaling thread. public void processRequest(final RequestEvent requestEvent) { Runnable runnable = new Runnable() { @Override public void run() { Request request = requestEvent.getRequest(); RCLogger.i(TAG, "Received SIP request: \n" + request.toString()); String callId = ((CallIdHeader)request.getHeader("Call-ID")).getCallId(); // create a new jobId for the new job String jobId = Long.toString(System.currentTimeMillis()); ServerTransaction serverTransaction = requestEvent.getServerTransaction(); String method = request.getMethod(); if (method.equals(Request.INVITE)) { // New INVITE, need to create new job JainSipCall jainSipCall = new JainSipCall(JainSipClient.this, (JainSipCall.JainSipCallListener)listener); // Remember, this is new dialog and hence serverTransaction is null JainSipJob jainSipJob = jainSipJobManager.add(jobId, JainSipJob.Type.TYPE_CALL, null, null, jainSipCall); jainSipCall.processRequest(jainSipJob, requestEvent); } else if (method.equals(Request.MESSAGE)) { try { if (serverTransaction == null) { // no server transaction yet serverTransaction = jainSipProvider.getNewServerTransaction(request); } Response response = jainSipMessageBuilder.buildResponse(Response.OK, request); RCLogger.i(TAG, "Sending SIP response: \n" + response.toString()); serverTransaction.sendResponse(response); String messageText = ((SIPMessage)request).getMessageContent(); listener.onClientMessageArrivedEvent(jobId, ((SIPMessage)request).getFrom().getAddress().toString(), messageText); } catch (Exception e) { e.printStackTrace(); } } else if (method.equals(Request.OPTIONS)) { try { if (serverTransaction == null) { // no server transaction yet serverTransaction = jainSipProvider.getNewServerTransaction(request); } Response response = jainSipMessageBuilder.buildOptions200OKResponse(request, jainSipListeningPoint); RCLogger.i(TAG, "Sending SIP response: \n" + response.toString()); serverTransaction.sendResponse(response); } catch (Exception e) { e.printStackTrace(); } } else if (method.equals(Request.BYE) || method.equals(Request.CANCEL) || method.equals(Request.ACK)) { JainSipJob jainSipJob = jainSipJobManager.getByCallId(callId); if (jainSipJob == null) { // no need to notify UI thread RCLogger.e(TAG, "processRequest(): error, got request for unknown transaction job. Method: " + method); return; } // forward to JainSipCall for processing jainSipJob.jainSipCall.processRequest(jainSipJob, requestEvent); } } }; signalingHandler.post(runnable); } public void processResponse(final ResponseEvent responseEvent) { Runnable runnable = new Runnable() { @Override public void run() { ResponseEventExt responseEventExt = (ResponseEventExt) responseEvent; Response response = responseEvent.getResponse(); RCLogger.i(TAG, "Received SIP response: \n" + response.toString()); //JainSipJob jainSipJob = jainSipJobManager.getByBranchId(responseEvent.getClientTransaction().getBranchId()); JainSipJob jainSipJob = jainSipJobManager.getByCallId(((CallIdHeader)response.getHeader("Call-ID")).getCallId()); if (jainSipJob == null) { RCLogger.e(TAG, "processResponse(): error, got response for unknown job"); return; } CSeqHeader cseq = (CSeqHeader) response.getHeader(CSeqHeader.NAME); String method = cseq.getMethod(); if (method.equals(Request.REGISTER)) { if (response.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED || response.getStatusCode() == Response.UNAUTHORIZED) { jainSipJob.processFsm(jainSipJob.jobId, JainSipJob.FsmEvents.AUTH_REQUIRED, responseEventExt, null, null); } else if (response.getStatusCode() == Response.FORBIDDEN) { jainSipJob.processFsm(jainSipJob.jobId, JainSipJob.FsmEvents.REGISTER_FAILURE, null, RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_AUTHENTICATION_FORBIDDEN, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_AUTHENTICATION_FORBIDDEN)); } else if (response.getStatusCode() == Response.SERVICE_UNAVAILABLE) { jainSipJob.processFsm(jainSipJob.jobId, JainSipJob.FsmEvents.REGISTER_FAILURE, null, RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_SERVICE_UNAVAILABLE, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_SERVICE_UNAVAILABLE)); } else if (response.getStatusCode() == Response.OK) { // register succeeded //ViaHeader viaHeader = (ViaHeader) response.getHeader(ViaHeader.NAME); updateViaReceivedAndRport((ViaHeader)response.getHeader(ViaHeader.NAME)); jainSipJob.processFsm(jainSipJob.jobId, JainSipJob.FsmEvents.REGISTER_SUCCESS, null, RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS)); } } else if (method.equals(Request.INVITE) || method.equals(Request.BYE) || method.equals(Request.CANCEL) || method.equals(Request.INFO)) { // forward to JainSipCall for processing jainSipJob.jainSipCall.processResponse(jainSipJob, responseEvent); } else if (method.equals(Request.MESSAGE)) { if (response.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED || response.getStatusCode() == Response.UNAUTHORIZED) { try { jainSipAuthenticate(jainSipJob, configuration, responseEventExt); } catch (JainSipException e) { listener.onClientMessageReply(jainSipJob.jobId, e.errorCode, e.errorText); } } else if (response.getStatusCode() == Response.OK) { listener.onClientMessageReply(jainSipJob.jobId, RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS)); } else if (response.getStatusCode() == Response.FORBIDDEN) { listener.onClientMessageReply(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_MESSAGE_AUTHENTICATION_FORBIDDEN, RCClient.errorText(RCClient.ErrorCodes.ERROR_MESSAGE_AUTHENTICATION_FORBIDDEN)); } else if (response.getStatusCode() == Response.SERVICE_UNAVAILABLE) { listener.onClientMessageReply(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_MESSAGE_SERVICE_UNAVAILABLE, RCClient.errorText(RCClient.ErrorCodes.ERROR_MESSAGE_SERVICE_UNAVAILABLE)); } } } }; signalingHandler.post(runnable); } public void processDialogTerminated(final DialogTerminatedEvent dialogTerminatedEvent) { Runnable runnable = new Runnable() { @Override public void run() { RCLogger.v(TAG, "SipManager.processDialogTerminated: " + dialogTerminatedEvent.toString() + "\n" + "\tdialog: " + dialogTerminatedEvent.getDialog().toString()); } }; signalingHandler.post(runnable); } public void processIOException(final IOExceptionEvent exceptionEvent) { Runnable runnable = new Runnable() { @Override public void run() { RCLogger.e(TAG, "SipManager.processIOException: " + exceptionEvent.toString() + "\n" + "\thost: " + exceptionEvent.getHost() + "\n" + "\tport: " + exceptionEvent.getPort()); } }; signalingHandler.post(runnable); } public void processTransactionTerminated(final TransactionTerminatedEvent transactionTerminatedEvent) { Runnable runnable = new Runnable() { @Override public void run() { RCLogger.v(TAG, "processTransactionTerminated: " + transactionTerminatedEvent.toString() + "\n" + "\tclient transaction: " + transactionTerminatedEvent.getClientTransaction() + "\n" + "\tserver transaction: " + transactionTerminatedEvent.getServerTransaction() + "\n" + "\tisServerTransaction: " + transactionTerminatedEvent.isServerTransaction()); } }; signalingHandler.post(runnable); } public void processTimeout(final TimeoutEvent timeoutEvent) { Runnable runnable = new Runnable() { @Override public void run() { Request request; Transaction transaction; if (timeoutEvent.isServerTransaction()) { request = timeoutEvent.getServerTransaction().getRequest(); //transaction = timeoutEvent.getServerTransaction(); } else { request = timeoutEvent.getClientTransaction().getRequest(); //transaction = timeoutEvent.getClientTransaction(); } RCLogger.w(TAG, "processTimeout(): method: " + request.getMethod() + " URI: " + request.getRequestURI()); JainSipJob jainSipJob = jainSipJobManager.getByCallId(((CallIdHeader) request.getHeader("Call-ID")).getCallId()); if (jainSipJob == null) { // transaction is not identified, just emit a log error; don't notify UI thread RCLogger.e(TAG, "processTimeout(): transaction not identified"); return; } if (jainSipJob.type == JainSipJob.Type.TYPE_CALL) { jainSipJob.jainSipCall.processTimeout(jainSipJob, timeoutEvent); } else if (jainSipJob.type == JainSipJob.Type.TYPE_MESSAGE) { listener.onClientMessageReply(jainSipJob.jobId, RCClient.ErrorCodes.ERROR_MESSAGE_TIMEOUT, RCClient.errorText(RCClient.ErrorCodes.ERROR_MESSAGE_TIMEOUT)); jainSipJobManager.remove(jainSipJob.jobId); } else { // register, register refresh, reconfigure, etc jainSipJob.processFsm(jainSipJob.jobId, JainSipJob.FsmEvents.TIMEOUT, null, RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT, RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT)); } } }; signalingHandler.post(runnable); } // ------ NotificationManagerListener events public void onConnectivityChange(JainSipNotificationManager.ConnectivityChange connectivityChange) { // No matter the connectivity change, cancel any pending scheduled registrations signalingHandler.removeCallbacksAndMessages(REGISTER_REFRESH_HANDLER_TOKEN); if (connectivityChange == JainSipNotificationManager.ConnectivityChange.OFFLINE) { try { jainSipClientUnbind(); listener.onClientConnectivityEvent(Long.toString(System.currentTimeMillis()), RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone); } catch (JainSipException e) { // let's notify the App regardless of the exception since we no longer have connectivity e.printStackTrace(); listener.onClientConnectivityEvent(Long.toString(System.currentTimeMillis()), RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone); } } else if (connectivityChange == JainSipNotificationManager.ConnectivityChange.OFFLINE_TO_WIFI || connectivityChange == JainSipNotificationManager.ConnectivityChange.OFFLINE_TO_CELLULAR_DATA || connectivityChange == JainSipNotificationManager.ConnectivityChange.OFFLINE_TO_ETHERNET) { HashMap<String, Object> parameters = new HashMap<>(this.configuration); if (connectivityChange == JainSipNotificationManager.ConnectivityChange.OFFLINE_TO_WIFI) { parameters.put("connectivity-status", RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusWiFi); } else if (connectivityChange == JainSipNotificationManager.ConnectivityChange.OFFLINE_TO_CELLULAR_DATA) { parameters.put("connectivity-status", RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusCellular); } else { parameters.put("connectivity-status", RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusEthernet); } jainSipJobManager.add(Long.toString(System.currentTimeMillis()), JainSipJob.Type.TYPE_START_NETWORKING, parameters); } else if (connectivityChange == JainSipNotificationManager.ConnectivityChange.HANDOVER_TO_WIFI || connectivityChange == JainSipNotificationManager.ConnectivityChange.HANDOVER_TO_CELLULAR_DATA || connectivityChange == JainSipNotificationManager.ConnectivityChange.HANDOVER_TO_ETHERNET) { HashMap<String, Object> parameters = new HashMap<>(this.configuration); if (connectivityChange == JainSipNotificationManager.ConnectivityChange.HANDOVER_TO_WIFI) { parameters.put("connectivity-status", RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusWiFi); } else if (connectivityChange == JainSipNotificationManager.ConnectivityChange.HANDOVER_TO_CELLULAR_DATA) { parameters.put("connectivity-status", RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusCellular); } else { // connectivityChange == JainSipNotificationManager.ConnectivityChange.HANDOVER_TO_ETHERNET parameters.put("connectivity-status", RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusEthernet); } jainSipJobManager.add(Long.toString(System.currentTimeMillis()), JainSipJob.Type.TYPE_RELOAD_NETWORKING, parameters); } } // Search through the interfaces to find the one who's name is starting with 'networkInterfacePrefix' and return // the ip address corresponding to it private String interface2Address(boolean useIPv4, String networkInterfacePrefix) throws SocketException { String stringAddress = ""; List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); for (NetworkInterface intf : interfaces) { RCLogger.i(TAG, "interface2Address(): Current interface: " + intf.toString()); if (intf.isUp() && intf.getName().matches(networkInterfacePrefix + ".*")) { List<InetAddress> addrs = Collections.list(intf.getInetAddresses()); for (InetAddress addr : addrs) { if (!addr.isLoopbackAddress()) { String sAddr = addr.getHostAddress().toUpperCase(); boolean isIPv4 = addr instanceof Inet4Address; //InetAddressUtils.isIPv4Address(sAddr); if (useIPv4) { if (isIPv4) { stringAddress = sAddr; break; } } else { if (!isIPv4) { int delim = sAddr.indexOf('%'); // drop ip6 port // suffix stringAddress = delim < 0 ? sAddr : sAddr.substring(0, delim); break; } } } } } else { RCLogger.i(TAG, "interface2Address(): Interface not matching or down: " + intf.toString() + " isUp: " + intf.isUp()); } } // One issue that isn't 100% from resources around the web is whether the interface names are standard across Android flavours. We assume that cellular data will always be // rmnet* and ethernet will always be eth*. To that end let's print out all the interfaces in case we cannot find an ip address for current network type so that we can troubleshoot // right away in that unlikely event. RCLogger.v(TAG, "interface2Address(): stringAddress: " + stringAddress + ", for currently active network: " + networkInterfacePrefix + ", interfaces: " + interfaces.toString()); if (stringAddress.isEmpty()) { RCLogger.e(TAG, "Couldn't retrieve IP address for currently active network"); } return stringAddress; } // -- Helpers // TODO: Improve this, try to not depend on such low level facilities public String getIPAddress(boolean useIPv4) throws SocketException { String stringAddress = ""; if (jainSipNotificationManager.getNetworkStatus() == JainSipNotificationManager.NetworkStatus.NetworkStatusWiFi) { WifiManager wifiMgr = (WifiManager) androidContext.getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiMgr.getConnectionInfo(); int ip = wifiInfo.getIpAddress(); stringAddress = Formatter.formatIpAddress(ip); } if (jainSipNotificationManager.getNetworkStatus() == JainSipNotificationManager.NetworkStatus.NetworkStatusCellular) { if (Build.FINGERPRINT.contains("generic")) { // Emulator; when using emulator, network access is provided via Cellular interface (no idea why this happens instead of ConnectivityManager.TYPE_ETHERNET) // but the actual interface name is usually 'eth0', so let's pass that as well in the network interface prefix argument stringAddress = interface2Address(useIPv4, "(rmnet|eth)"); } else { // Real device stringAddress = interface2Address(useIPv4, "rmnet"); } } if (jainSipNotificationManager.getNetworkStatus() == JainSipNotificationManager.NetworkStatus.NetworkStatusEthernet) { stringAddress = interface2Address(useIPv4, "eth"); } RCLogger.v(TAG, "getIPAddress(): " + stringAddress); return stringAddress; } private void updateViaReceivedAndRport(ViaHeader viaHeader) { // keep around the Via received and rport parms so that we can populate the contact properly if (viaHeader.getReceived() != null) { jainSipClientContext.put("via-received", viaHeader.getReceived()); } else { jainSipClientContext.remove("via-received"); } if (viaHeader.getRPort() != -1) { jainSipClientContext.put("via-rport", viaHeader.getRPort()); } else { jainSipClientContext.remove("via-rport"); } } }