/** * Copyright (C) 2010-2015 Regis Montoya (aka r3gis - www.r3gis.fr) * Copyright (C) 2012-2013 Dennis Guse (http://dennisguse.de) * Copyright (C) 2015 Antonio Eugenio Burriel * This file is part of CSipSimple. * * CSipSimple is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * If you own a pjsip commercial license you can also redistribute it * and/or modify it under the terms of the GNU Lesser General Public License * as an android library. * * CSipSimple 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with CSipSimple. If not, see <http://www.gnu.org/licenses/>. */ package com.csipsimple.pjsip; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.media.AudioManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.SparseArray; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.SurfaceView; import com.csipsimple.R; import com.csipsimple.api.SipCallSession; import com.csipsimple.api.SipConfigManager; import com.csipsimple.api.SipManager; import com.csipsimple.api.SipManager.PresenceStatus; import com.csipsimple.api.SipProfile; import com.csipsimple.api.SipProfileState; import com.csipsimple.api.SipUri.ParsedSipContactInfos; import com.csipsimple.pjsip.earlylock.EarlyLockModule; import com.csipsimple.pjsip.player.IPlayerHandler; import com.csipsimple.pjsip.player.impl.SimpleWavPlayerHandler; import com.csipsimple.pjsip.recorder.IRecorderHandler; import com.csipsimple.pjsip.recorder.impl.SimpleWavRecorderHandler; import com.csipsimple.pjsip.reghandler.RegHandlerModule; import com.csipsimple.pjsip.sipclf.SipClfModule; import com.csipsimple.service.MediaManager; import com.csipsimple.service.SipService; import com.csipsimple.service.SipService.SameThreadException; import com.csipsimple.service.SipService.SipRunnable; import com.csipsimple.service.SipService.ToCall; import com.csipsimple.utils.ExtraPlugins; import com.csipsimple.utils.ExtraPlugins.DynCodecInfos; import com.csipsimple.utils.Log; import com.csipsimple.utils.PreferencesProviderWrapper; import com.csipsimple.utils.PreferencesWrapper; import com.csipsimple.utils.TimerWrapper; import com.csipsimple.utils.video.VideoUtilsWrapper; import com.csipsimple.utils.video.VideoUtilsWrapper.VideoCaptureCapability; import com.csipsimple.utils.video.VideoUtilsWrapper.VideoCaptureDeviceInfo; import com.csipsimple.wizards.WizardUtils; import org.pjsip.pjsua.SWIGTYPE_p_pj_stun_auth_cred; import org.pjsip.pjsua.csipsimple_config; import org.pjsip.pjsua.dynamic_factory; import org.pjsip.pjsua.pj_ice_sess_options; import org.pjsip.pjsua.pj_pool_t; import org.pjsip.pjsua.pj_qos_params; import org.pjsip.pjsua.pj_str_t; import org.pjsip.pjsua.pj_turn_tp_type; import org.pjsip.pjsua.pjmedia_srtp_use; import org.pjsip.pjsua.pjsip_ssl_method; import org.pjsip.pjsua.pjsip_timer_setting; import org.pjsip.pjsua.pjsip_tls_setting; import org.pjsip.pjsua.pjsip_transport_type_e; import org.pjsip.pjsua.pjsua; import org.pjsip.pjsua.pjsuaConstants; import org.pjsip.pjsua.pjsua_acc_info; import org.pjsip.pjsua.pjsua_buddy_config; import org.pjsip.pjsua.pjsua_call_flag; import org.pjsip.pjsua.pjsua_call_setting; import org.pjsip.pjsua.pjsua_call_vid_strm_op; import org.pjsip.pjsua.pjsua_config; import org.pjsip.pjsua.pjsua_logging_config; import org.pjsip.pjsua.pjsua_media_config; import org.pjsip.pjsua.pjsua_msg_data; import org.pjsip.pjsua.pjsua_transport_config; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Timer; import java.util.TimerTask; public class PjSipService { private static final String THIS_FILE = "PjService"; private static int DTMF_TONE_PAUSE_LENGTH = 300; private static int DTMF_TONE_WAIT_LENGTH = 2000; public SipService service; private boolean created = false; private boolean hasSipStack = false; private boolean sipStackIsCorrupted = false; private Integer localUdpAccPjId, localUdp6AccPjId, localTcpAccPjId, localTcp6AccPjId, localTlsAccPjId, localTls6AccPjId; public PreferencesProviderWrapper prefsWrapper; private Integer hasBeenHoldByGSM = null; private Integer hasBeenChangedRingerMode = null; public UAStateReceiver userAgentReceiver; public ZrtpStateReceiver zrtpReceiver; public MediaManager mediaManager; private Timer tasksTimer; private SparseArray<String> dtmfToAutoSend = new SparseArray<String>(5); private SparseArray<TimerTask> dtmfTasks = new SparseArray<TimerTask>(5); private SparseArray<PjStreamDialtoneGenerator> dtmfDialtoneGenerators = new SparseArray<PjStreamDialtoneGenerator>(5); private SparseArray<PjStreamDialtoneGenerator> waittoneGenerators = new SparseArray<PjStreamDialtoneGenerator>(5); private String mNatDetected = ""; // ------- // Locks // ------- public PjSipService() { } public void setService(SipService aService) { service = aService; prefsWrapper = service.getPrefs(); } public boolean isCreated() { return created; } public boolean tryToLoadStack() { if (hasSipStack) { return true; } // File stackFile = NativeLibManager.getStackLibFile(service); if (!sipStackIsCorrupted) { try { // Try to load the stack // System.load(NativeLibManager.getBundledStackLibFile(service, // "libcrypto.so").getAbsolutePath()); // System.load(NativeLibManager.getBundledStackLibFile(service, // "libssl.so").getAbsolutePath()); // System.loadLibrary("crypto"); // System.loadLibrary("ssl"); System.loadLibrary(NativeLibManager.STD_LIB_NAME); System.loadLibrary(NativeLibManager.STACK_NAME); hasSipStack = true; return true; } catch (UnsatisfiedLinkError e) { // If it fails we probably are running on a special hardware Log.e(THIS_FILE, "We have a problem with the current stack.... NOT YET Implemented", e); hasSipStack = false; sipStackIsCorrupted = true; service.notifyUserOfMessage("Can't load native library. CPU arch invalid for this build"); return false; } catch (Exception e) { Log.e(THIS_FILE, "We have a problem with the current stack....", e); } } return false; } // Start the sip stack according to current settings /** * Start the sip stack Thread safing of this method must be ensured by upper * layer Every calls from pjsip that require start/stop/getInfos from the * underlying stack must be done on the same thread */ public boolean sipStart() throws SameThreadException { Log.setLogLevel(prefsWrapper.getLogLevel()); if (!hasSipStack) { Log.e(THIS_FILE, "We have no sip stack, we can't start"); return false; } // Ensure the stack is not already created or is being created if (!created) { Log.d(THIS_FILE, "Starting sip stack"); // Pj timer TimerWrapper.create(service); int status; status = pjsua.create(); Log.i(THIS_FILE, "Created " + status); // General config { pj_str_t[] stunServers = null; int stunServersCount = 0; pjsua_config cfg = new pjsua_config(); pjsua_logging_config logCfg = new pjsua_logging_config(); pjsua_media_config mediaCfg = new pjsua_media_config(); csipsimple_config cssCfg = new csipsimple_config(); // SERVICE CONFIG if (userAgentReceiver == null) { Log.d(THIS_FILE, "create ua receiver"); userAgentReceiver = new UAStateReceiver(); userAgentReceiver.initService(this); } userAgentReceiver.reconfigure(service); if (zrtpReceiver == null) { Log.d(THIS_FILE, "create zrtp receiver"); zrtpReceiver = new ZrtpStateReceiver(this); } if (mediaManager == null) { mediaManager = new MediaManager(service); } mediaManager.startService(); initModules(); DTMF_TONE_PAUSE_LENGTH = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.DTMF_PAUSE_TIME); DTMF_TONE_WAIT_LENGTH = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.DTMF_WAIT_TIME); pjsua.setCallbackObject(userAgentReceiver); pjsua.setZrtpCallbackObject(zrtpReceiver); Log.d(THIS_FILE, "Attach is done to callback"); // CSS CONFIG pjsua.csipsimple_config_default(cssCfg); cssCfg.setUse_compact_form_headers(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.USE_COMPACT_FORM) ? pjsua.PJ_TRUE : pjsua.PJ_FALSE); cssCfg.setUse_compact_form_sdp(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.USE_COMPACT_FORM) ? pjsua.PJ_TRUE : pjsua.PJ_FALSE); cssCfg.setUse_no_update(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.FORCE_NO_UPDATE) ? pjsua.PJ_TRUE : pjsua.PJ_FALSE); cssCfg.setUse_noise_suppressor(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.ENABLE_NOISE_SUPPRESSION) ? pjsua.PJ_TRUE : pjsua.PJ_FALSE); cssCfg.setTcp_keep_alive_interval(prefsWrapper.getTcpKeepAliveInterval()); cssCfg.setTls_keep_alive_interval(prefsWrapper.getTlsKeepAliveInterval()); cssCfg.setDisable_tcp_switch(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.DISABLE_TCP_SWITCH) ? pjsuaConstants.PJ_TRUE : pjsuaConstants.PJ_FALSE); cssCfg.setDisable_rport(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.DISABLE_RPORT) ? pjsuaConstants.PJ_TRUE : pjsuaConstants.PJ_FALSE); cssCfg.setAdd_bandwidth_tias_in_sdp(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.ADD_BANDWIDTH_TIAS_IN_SDP) ? pjsuaConstants.PJ_TRUE : pjsuaConstants.PJ_FALSE); // Transaction timeouts int tsx_to = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.TSX_T1_TIMEOUT); if (tsx_to > 0) { cssCfg.setTsx_t1_timeout(tsx_to); } tsx_to = prefsWrapper.getPreferenceIntegerValue(SipConfigManager.TSX_T2_TIMEOUT); if (tsx_to > 0) { cssCfg.setTsx_t2_timeout(tsx_to); } tsx_to = prefsWrapper.getPreferenceIntegerValue(SipConfigManager.TSX_T4_TIMEOUT); if (tsx_to > 0) { cssCfg.setTsx_t4_timeout(tsx_to); } tsx_to = prefsWrapper.getPreferenceIntegerValue(SipConfigManager.TSX_TD_TIMEOUT); if (tsx_to > 0) { cssCfg.setTsx_td_timeout(tsx_to); } // -- USE_ZRTP 1 is no_zrtp, 2 is create_zrtp File zrtpFolder = PreferencesWrapper.getZrtpFolder(service); if (zrtpFolder != null) { cssCfg.setUse_zrtp((prefsWrapper .getPreferenceIntegerValue(SipConfigManager.USE_ZRTP) > 1) ? pjsua.PJ_TRUE : pjsua.PJ_FALSE); cssCfg.setStorage_folder(pjsua.pj_str_copy(zrtpFolder.getAbsolutePath())); } else { cssCfg.setUse_zrtp(pjsua.PJ_FALSE); cssCfg.setStorage_folder(pjsua.pj_str_copy("")); } Map<String, DynCodecInfos> availableCodecs = ExtraPlugins.getDynCodecPlugins( service, SipManager.ACTION_GET_EXTRA_CODECS); dynamic_factory[] cssCodecs = cssCfg.getExtra_aud_codecs(); int i = 0; for (Entry<String, DynCodecInfos> availableCodec : availableCodecs.entrySet()) { DynCodecInfos dyn = availableCodec.getValue(); if (!TextUtils.isEmpty(dyn.libraryPath)) { cssCodecs[i].setShared_lib_path(pjsua.pj_str_copy(dyn.libraryPath)); cssCodecs[i++].setInit_factory_name(pjsua .pj_str_copy(dyn.factoryInitFunction)); } } cssCfg.setExtra_aud_codecs_cnt(i); // Audio implementation int implementation = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.AUDIO_IMPLEMENTATION); if (implementation == SipConfigManager.AUDIO_IMPLEMENTATION_OPENSLES) { dynamic_factory audImp = cssCfg.getAudio_implementation(); audImp.setInit_factory_name(pjsua.pj_str_copy("pjmedia_opensl_factory")); File openslLib = NativeLibManager.getBundledStackLibFile(service, "libpj_opensl_dev.so"); audImp.setShared_lib_path(pjsua.pj_str_copy(openslLib.getAbsolutePath())); cssCfg.setAudio_implementation(audImp); Log.d(THIS_FILE, "Use OpenSL-ES implementation"); } // Video implementation if (prefsWrapper.getPreferenceBooleanValue(SipConfigManager.USE_VIDEO)) { // TODO :: Have plugins per capture / render / video codec / // converter Map<String, DynCodecInfos> videoPlugins = ExtraPlugins.getDynCodecPlugins( service, SipManager.ACTION_GET_VIDEO_PLUGIN); if (videoPlugins.size() > 0) { DynCodecInfos videoPlugin = videoPlugins.values().iterator().next(); pj_str_t pjVideoFile = pjsua.pj_str_copy(videoPlugin.libraryPath); Log.d(THIS_FILE, "Load video plugin at " + videoPlugin.libraryPath); // Render { dynamic_factory vidImpl = cssCfg.getVideo_render_implementation(); vidImpl.setInit_factory_name(pjsua .pj_str_copy("pjmedia_webrtc_vid_render_factory")); vidImpl.setShared_lib_path(pjVideoFile); } // Capture { dynamic_factory vidImpl = cssCfg.getVideo_capture_implementation(); vidImpl.setInit_factory_name(pjsua .pj_str_copy("pjmedia_webrtc_vid_capture_factory")); vidImpl.setShared_lib_path(pjVideoFile); /* * -- For testing video screen -- Not yet released * try { ComponentName cmp = new * ComponentName("com.csipsimple.plugins.video", * "com.csipsimple.plugins.video.CaptureReceiver"); * DynCodecInfos screenCapt = new * ExtraPlugins.DynCodecInfos(service, cmp); * vidImpl.setInit_factory_name(pjsua * .pj_str_copy(screenCapt.factoryInitFunction)); * vidImpl.setShared_lib_path(pjsua * .pj_str_copy(screenCapt.libraryPath)); } catch * (NameNotFoundException e) { Log.e(THIS_FILE, * "Not found capture plugin"); } */ } // Video codecs availableCodecs = ExtraPlugins.getDynCodecPlugins(service, SipManager.ACTION_GET_EXTRA_VIDEO_CODECS); cssCodecs = cssCfg.getExtra_vid_codecs(); dynamic_factory[] cssCodecsDestroy = cssCfg.getExtra_vid_codecs_destroy(); i = 0; for (Entry<String, DynCodecInfos> availableCodec : availableCodecs .entrySet()) { DynCodecInfos dyn = availableCodec.getValue(); if (!TextUtils.isEmpty(dyn.libraryPath)) { // Create cssCodecs[i].setShared_lib_path(pjsua.pj_str_copy(dyn.libraryPath)); cssCodecs[i].setInit_factory_name(pjsua .pj_str_copy(dyn.factoryInitFunction)); // Destroy cssCodecsDestroy[i].setShared_lib_path(pjsua .pj_str_copy(dyn.libraryPath)); cssCodecsDestroy[i].setInit_factory_name(pjsua .pj_str_copy(dyn.factoryDeinitFunction)); } i++; } cssCfg.setExtra_vid_codecs_cnt(i); // Converter dynamic_factory convertImpl = cssCfg.getVid_converter(); convertImpl.setShared_lib_path(pjVideoFile); convertImpl.setInit_factory_name(pjsua .pj_str_copy("pjmedia_libswscale_converter_init")); } } // MAIN CONFIG pjsua.config_default(cfg); cfg.setCb(pjsuaConstants.WRAPPER_CALLBACK_STRUCT); cfg.setUser_agent(pjsua.pj_str_copy(prefsWrapper.getUserAgent(service))); // We need at least one thread int threadCount = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.THREAD_COUNT); if (threadCount <= 0) { threadCount = 1; } cfg.setThread_cnt(threadCount); cfg.setUse_srtp(getUseSrtp()); cfg.setSrtp_secure_signaling(0); cfg.setNat_type_in_sdp(0); pjsip_timer_setting timerSetting = cfg.getTimer_setting(); int minSe = prefsWrapper.getPreferenceIntegerValue(SipConfigManager.TIMER_MIN_SE); int sessExp = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.TIMER_SESS_EXPIRES); if (minSe <= sessExp && minSe >= 90) { timerSetting.setMin_se(minSe); timerSetting.setSess_expires(sessExp); cfg.setTimer_setting(timerSetting); } // DNS if (prefsWrapper.enableDNSSRV() && !prefsWrapper.useIPv6()) { pj_str_t[] nameservers = getNameservers(); if (nameservers != null) { cfg.setNameserver_count(nameservers.length); cfg.setNameserver(nameservers); } else { cfg.setNameserver_count(0); } } // STUN boolean isStunEnabled = prefsWrapper.getPreferenceBooleanValue(SipConfigManager.ENABLE_STUN); if (isStunEnabled) { String[] servers = prefsWrapper.getPreferenceStringValue( SipConfigManager.STUN_SERVER).split(","); cfg.setStun_srv_cnt(servers.length); stunServers = cfg.getStun_srv(); for (String server : servers) { Log.d(THIS_FILE, "add server " + server.trim()); stunServers[stunServersCount] = pjsua.pj_str_copy(server.trim()); stunServersCount++; } cfg.setStun_srv(stunServers); cfg.setStun_map_use_stun2(boolToPjsuaConstant(prefsWrapper .getPreferenceBooleanValue(SipConfigManager.ENABLE_STUN2))); } // LOGGING CONFIG pjsua.logging_config_default(logCfg); logCfg.setConsole_level(prefsWrapper.getLogLevel()); logCfg.setLevel(prefsWrapper.getLogLevel()); logCfg.setMsg_logging(pjsuaConstants.PJ_TRUE); if (prefsWrapper.getPreferenceBooleanValue(SipConfigManager.LOG_USE_DIRECT_FILE, false)) { File outFile = PreferencesWrapper.getLogsFile(service, true); if (outFile != null) { logCfg.setLog_filename(pjsua.pj_str_copy(outFile.getAbsolutePath())); logCfg.setLog_file_flags(0x1108 /* PJ_O_APPEND */); } } // MEDIA CONFIG pjsua.media_config_default(mediaCfg); // For now only this cfg is supported mediaCfg.setChannel_count(1); mediaCfg.setSnd_auto_close_time(prefsWrapper.getAutoCloseTime()); // Echo cancellation mediaCfg.setEc_tail_len(prefsWrapper.getEchoCancellationTail()); int echoMode = prefsWrapper.getPreferenceIntegerValue(SipConfigManager.ECHO_MODE); long clockRate = prefsWrapper.getClockRate(mediaManager); if (clockRate > 16000 && echoMode == SipConfigManager.ECHO_MODE_WEBRTC_M) { // WebRTC mobile does not allow higher that 16kHz for now // TODO : warn user about this point echoMode = SipConfigManager.ECHO_MODE_SIMPLE; } mediaCfg.setEc_options(echoMode); mediaCfg.setNo_vad(boolToPjsuaConstant(!prefsWrapper .getPreferenceBooleanValue(SipConfigManager.ENABLE_VAD))); mediaCfg.setQuality(prefsWrapper.getMediaQuality()); mediaCfg.setClock_rate(clockRate); mediaCfg.setAudio_frame_ptime(prefsWrapper .getPreferenceIntegerValue(SipConfigManager.SND_PTIME)); // Disabled ? because only one thread enabled now for battery // perfs on normal state int mediaThreadCount = prefsWrapper .getPreferenceIntegerValue(SipConfigManager.MEDIA_THREAD_COUNT); mediaCfg.setThread_cnt(mediaThreadCount); boolean hasOwnIoQueue = prefsWrapper .getPreferenceBooleanValue(SipConfigManager.HAS_IO_QUEUE); if (threadCount <= 0) { // Global thread count is 0, so don't use sip one anyway hasOwnIoQueue = false; } mediaCfg.setHas_ioqueue(boolToPjsuaConstant(hasOwnIoQueue)); // ICE boolean iceEnabled = prefsWrapper.getPreferenceBooleanValue(SipConfigManager.ENABLE_ICE); mediaCfg.setEnable_ice(boolToPjsuaConstant(iceEnabled)); if(iceEnabled) { pj_ice_sess_options iceOpts = mediaCfg.getIce_opt(); boolean aggressiveIce = prefsWrapper.getPreferenceBooleanValue(SipConfigManager.ICE_AGGRESSIVE); iceOpts.setAggressive(boolToPjsuaConstant(aggressiveIce)); } // TURN boolean isTurnEnabled = prefsWrapper.getPreferenceBooleanValue(SipConfigManager.ENABLE_TURN); if (isTurnEnabled) { SWIGTYPE_p_pj_stun_auth_cred creds = mediaCfg.getTurn_auth_cred(); mediaCfg.setEnable_turn(boolToPjsuaConstant(isTurnEnabled)); mediaCfg.setTurn_server(pjsua.pj_str_copy(prefsWrapper.getTurnServer())); pjsua.set_turn_credentials( pjsua.pj_str_copy(prefsWrapper .getPreferenceStringValue(SipConfigManager.TURN_USERNAME)), pjsua.pj_str_copy(prefsWrapper .getPreferenceStringValue(SipConfigManager.TURN_PASSWORD)), pjsua.pj_str_copy("*"), creds); // Normally this step is useless as manipulating a pointer in C memory at this point, but in case this changes reassign mediaCfg.setTurn_auth_cred(creds); int turnTransport = prefsWrapper.getPreferenceIntegerValue(SipConfigManager.TURN_TRANSPORT); if(turnTransport != 0) { switch (turnTransport) { case 1: mediaCfg.setTurn_conn_type(pj_turn_tp_type.PJ_TURN_TP_UDP); break; case 2: mediaCfg.setTurn_conn_type(pj_turn_tp_type.PJ_TURN_TP_TCP); break; case 3: mediaCfg.setTurn_conn_type(pj_turn_tp_type.PJ_TURN_TP_TLS); break; default: break; } } //mediaCfg.setTurn_conn_type(value); } else { mediaCfg.setEnable_turn(pjsua.PJ_FALSE); } // INITIALIZE status = pjsua.csipsimple_init(cfg, logCfg, mediaCfg, cssCfg, service); if (status != pjsuaConstants.PJ_SUCCESS) { String msg = "Fail to init pjsua " + pjStrToString(pjsua.get_error_message(status)); Log.e(THIS_FILE, msg); service.notifyUserOfMessage(msg); cleanPjsua(); return false; } } // Add transports { // TODO : allow to configure local accounts. // We need a local account for each transport // to not have the // application lost when direct call to the IP // UDP if (prefsWrapper.isUDPEnabled()) { int udpPort = prefsWrapper.getUDPTransportPort(); localUdpAccPjId = createLocalTransportAndAccount( pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udpPort); if (localUdpAccPjId == null) { cleanPjsua(); return false; } // UDP v6 if (prefsWrapper.useIPv6()) { localUdp6AccPjId = createLocalTransportAndAccount( pjsip_transport_type_e.PJSIP_TRANSPORT_UDP6, udpPort == 0 ? udpPort : udpPort + 10); } } // TCP if (prefsWrapper.isTCPEnabled()) { int tcpPort = prefsWrapper.getTCPTransportPort(); localTcpAccPjId = createLocalTransportAndAccount( pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcpPort); if (localTcpAccPjId == null) { cleanPjsua(); return false; } // TCP v6 if (prefsWrapper.useIPv6()) { localTcp6AccPjId = createLocalTransportAndAccount( pjsip_transport_type_e.PJSIP_TRANSPORT_TCP6, tcpPort == 0 ? tcpPort : tcpPort + 10); } } // TLS if (prefsWrapper.isTLSEnabled()) { int tlsPort = prefsWrapper.getTLSTransportPort(); localTlsAccPjId = createLocalTransportAndAccount( pjsip_transport_type_e.PJSIP_TRANSPORT_TLS, tlsPort); if (localTlsAccPjId == null) { cleanPjsua(); return false; } // TLS v6 if (prefsWrapper.useIPv6()) { localTls6AccPjId = createLocalTransportAndAccount( pjsip_transport_type_e.PJSIP_TRANSPORT_TLS6, tlsPort == 0 ? tlsPort : tlsPort + 10); } } } // Add pjsip modules for (PjsipModule mod : pjsipModules.values()) { mod.onBeforeStartPjsip(); } // Initialization is done, now start pjsua status = pjsua.start(); if (status != pjsua.PJ_SUCCESS) { String msg = "Fail to start pjsip " + pjStrToString(pjsua.get_error_message(status)); Log.e(THIS_FILE, msg); service.notifyUserOfMessage(msg); cleanPjsua(); return false; } // Init media codecs initCodecs(); setCodecsPriorities(); created = true; return true; } return false; } /** * Stop sip service * * @return true if stop has been performed */ public boolean sipStop() throws SameThreadException { Log.d(THIS_FILE, ">> SIP STOP <<"); if (getActiveCallInProgress() != null) { Log.e(THIS_FILE, "We have a call in progress... DO NOT STOP !!!"); // TODO : queue quit on end call; return false; } if (service.notificationManager != null) { service.notificationManager.cancelRegisters(); } if (created) { cleanPjsua(); } if (tasksTimer != null) { tasksTimer.cancel(); tasksTimer.purge(); tasksTimer = null; } return true; } private void cleanPjsua() throws SameThreadException { Log.d(THIS_FILE, "Detroying..."); // This will destroy all accounts so synchronize with accounts // management lock // long flags = 1; /*< Lazy disconnect : only RX */ // Try with TX & RX if network is considered as available long flags = 0; if (!prefsWrapper.isValidConnectionForOutgoing(false)) { // If we are current not valid for outgoing, // it means that we don't want the network for SIP now // so don't use RX | TX to not consume data at all flags = 3; } pjsua.csipsimple_destroy(flags); service.getContentResolver().delete(SipProfile.ACCOUNT_STATUS_URI, null, null); if (userAgentReceiver != null) { userAgentReceiver.stopService(); userAgentReceiver = null; } if (mediaManager != null) { mediaManager.stopService(); mediaManager = null; } TimerWrapper.destroy(); created = false; } /** * Utility to create a transport * * @return transport id or -1 if failed */ private Integer createTransport(pjsip_transport_type_e type, int port) throws SameThreadException { pjsua_transport_config cfg = new pjsua_transport_config(); int[] tId = new int[1]; int status; pjsua.transport_config_default(cfg); cfg.setPort(port); if (type.equals(pjsip_transport_type_e.PJSIP_TRANSPORT_TLS)) { pjsip_tls_setting tlsSetting = cfg.getTls_setting(); /* * TODO : THIS IS OBSOLETE -- remove from UI String serverName = * prefsWrapper * .getPreferenceStringValue(SipConfigManager.TLS_SERVER_NAME); if * (!TextUtils.isEmpty(serverName)) { * tlsSetting.setServer_name(pjsua.pj_str_copy(serverName)); } */ String caListFile = prefsWrapper .getPreferenceStringValue(SipConfigManager.CA_LIST_FILE); if (!TextUtils.isEmpty(caListFile)) { tlsSetting.setCa_list_file(pjsua.pj_str_copy(caListFile)); } String certFile = prefsWrapper.getPreferenceStringValue(SipConfigManager.CERT_FILE); if (!TextUtils.isEmpty(certFile)) { tlsSetting.setCert_file(pjsua.pj_str_copy(certFile)); } String privKey = prefsWrapper.getPreferenceStringValue(SipConfigManager.PRIVKEY_FILE); if (!TextUtils.isEmpty(privKey)) { tlsSetting.setPrivkey_file(pjsua.pj_str_copy(privKey)); } String tlsPwd = prefsWrapper.getPreferenceStringValue(SipConfigManager.TLS_PASSWORD); if (!TextUtils.isEmpty(tlsPwd)) { tlsSetting.setPassword(pjsua.pj_str_copy(tlsPwd)); } boolean checkClient = prefsWrapper .getPreferenceBooleanValue(SipConfigManager.TLS_VERIFY_CLIENT); tlsSetting.setVerify_client(checkClient ? 1 : 0); tlsSetting.setMethod(pjsip_ssl_method.swigToEnum(prefsWrapper.getTLSMethod())); tlsSetting.setProto(0); boolean checkServer = prefsWrapper .getPreferenceBooleanValue(SipConfigManager.TLS_VERIFY_SERVER); tlsSetting.setVerify_server(checkServer ? 1 : 0); cfg.setTls_setting(tlsSetting); } if (prefsWrapper.getPreferenceBooleanValue(SipConfigManager.ENABLE_QOS)) { Log.d(THIS_FILE, "Activate qos for this transport"); pj_qos_params qosParam = cfg.getQos_params(); qosParam.setDscp_val((short) prefsWrapper .getPreferenceIntegerValue(SipConfigManager.DSCP_VAL)); qosParam.setFlags((short) 1); // DSCP cfg.setQos_params(qosParam); } status = pjsua.transport_create(type, cfg, tId); if (status != pjsuaConstants.PJ_SUCCESS) { String errorMsg = pjStrToString(pjsua.get_error_message(status)); String msg = "Fail to create transport " + errorMsg + " (" + status + ")"; Log.e(THIS_FILE, msg); if (status == 120098) { /* Already binded */ msg = service.getString(R.string.another_application_use_sip_port); } service.notifyUserOfMessage(msg); return null; } return tId[0]; } private Integer createLocalAccount(Integer transportId) throws SameThreadException { if (transportId == null) { return null; } int[] p_acc_id = new int[1]; pjsua.acc_add_local(transportId, pjsua.PJ_FALSE, p_acc_id); return p_acc_id[0]; } private Integer createLocalTransportAndAccount(pjsip_transport_type_e type, int port) throws SameThreadException { Integer transportId = createTransport(type, port); return createLocalAccount(transportId); } public boolean addAccount(SipProfile profile) throws SameThreadException { int status = pjsuaConstants.PJ_FALSE; if (!created) { Log.e(THIS_FILE, "PJSIP is not started here, nothing can be done"); return status == pjsuaConstants.PJ_SUCCESS; } PjSipAccount account = new PjSipAccount(profile); account.applyExtraParams(service); // Force the use of a transport /* * switch (account.transport) { case SipProfile.TRANSPORT_UDP: if * (udpTranportId != null) { * //account.cfg.setTransport_id(udpTranportId); } break; case * SipProfile.TRANSPORT_TCP: if (tcpTranportId != null) { // * account.cfg.setTransport_id(tcpTranportId); } break; case * SipProfile.TRANSPORT_TLS: if (tlsTransportId != null) { // * account.cfg.setTransport_id(tlsTransportId); } break; default: break; * } */ SipProfileState currentAccountStatus = getProfileState(profile); account.cfg.setRegister_on_acc_add(pjsuaConstants.PJ_FALSE); if (currentAccountStatus.isAddedToStack()) { pjsua.csipsimple_set_acc_user_data(currentAccountStatus.getPjsuaId(), account.css_cfg); status = pjsua.acc_modify(currentAccountStatus.getPjsuaId(), account.cfg); beforeAccountRegistration(currentAccountStatus.getPjsuaId(), profile); ContentValues cv = new ContentValues(); cv.put(SipProfileState.ADDED_STATUS, status); service.getContentResolver().update( ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_ID_URI_BASE, profile.id), cv, null, null); if (!account.wizard.equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) { // Re register if (status == pjsuaConstants.PJ_SUCCESS) { status = pjsua.acc_set_registration(currentAccountStatus.getPjsuaId(), 1); if (status == pjsuaConstants.PJ_SUCCESS) { pjsua.acc_set_online_status(currentAccountStatus.getPjsuaId(), 1); } } } } else { int[] accId = new int[1]; if (account.wizard.equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) { // We already have local account by default // For now consider we are talking about UDP one // In the future local account should be set per transport switch (account.transport) { case SipProfile.TRANSPORT_UDP: accId[0] = prefsWrapper.useIPv6() ? localUdp6AccPjId : localUdpAccPjId; break; case SipProfile.TRANSPORT_TCP: accId[0] = prefsWrapper.useIPv6() ? localTcp6AccPjId : localTcpAccPjId; break; case SipProfile.TRANSPORT_TLS: accId[0] = prefsWrapper.useIPv6() ? localTls6AccPjId : localTlsAccPjId; break; default: // By default use UDP accId[0] = localUdpAccPjId; break; } pjsua.csipsimple_set_acc_user_data(accId[0], account.css_cfg); // TODO : use video cfg here // nCfg.setVid_in_auto_show(pjsuaConstants.PJ_TRUE); // nCfg.setVid_out_auto_transmit(pjsuaConstants.PJ_TRUE); // status = pjsua.acc_modify(accId[0], nCfg); } else { // Cause of standard account different from local account :) status = pjsua.acc_add(account.cfg, pjsuaConstants.PJ_FALSE, accId); pjsua.csipsimple_set_acc_user_data(accId[0], account.css_cfg); beforeAccountRegistration(accId[0], profile); pjsua.acc_set_registration(accId[0], 1); } if (status == pjsuaConstants.PJ_SUCCESS) { SipProfileState ps = new SipProfileState(profile); ps.setAddedStatus(status); ps.setPjsuaId(accId[0]); service.getContentResolver().insert( ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_ID_URI_BASE, account.id), ps.getAsContentValue()); pjsua.acc_set_online_status(accId[0], 1); } } return status == pjsuaConstants.PJ_SUCCESS; } void beforeAccountRegistration(int pjId, SipProfile profile) { for (PjsipModule mod : pjsipModules.values()) { mod.onBeforeAccountStartRegistration(pjId, profile); } } /** * Synchronize content provider backend from pjsip stack * * @param pjsuaId the pjsua id of the account to synchronize * @throws SameThreadException */ public void updateProfileStateFromService(int pjsuaId) throws SameThreadException { if (!created) { return; } long accId = getAccountIdForPjsipId(service, pjsuaId); Log.d(THIS_FILE, "Update profile from service for " + pjsuaId + " aka in db " + accId); if (accId != SipProfile.INVALID_ID) { int success = pjsuaConstants.PJ_FALSE; pjsua_acc_info pjAccountInfo; pjAccountInfo = new pjsua_acc_info(); success = pjsua.acc_get_info(pjsuaId, pjAccountInfo); if (success == pjsuaConstants.PJ_SUCCESS && pjAccountInfo != null) { ContentValues cv = new ContentValues(); try { // Should be fine : status code are coherent with RFC // status codes cv.put(SipProfileState.STATUS_CODE, pjAccountInfo.getStatus().swigValue()); } catch (IllegalArgumentException e) { cv.put(SipProfileState.STATUS_CODE, SipCallSession.StatusCode.INTERNAL_SERVER_ERROR); } cv.put(SipProfileState.STATUS_TEXT, pjStrToString(pjAccountInfo.getStatus_text())); cv.put(SipProfileState.EXPIRES, pjAccountInfo.getExpires()); service.getContentResolver().update( ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_ID_URI_BASE, accId), cv, null, null); Log.d(THIS_FILE, "Profile state UP : " + cv); } } else { Log.e(THIS_FILE, "Trying to update not added account " + pjsuaId); } } /** * Get the dynamic state of the profile * * @param account the sip profile from database. Important field is id. * @return the dynamic sip profile state */ public SipProfileState getProfileState(SipProfile account) { if (!created || account == null) { return null; } if (account.id == SipProfile.INVALID_ID) { return null; } SipProfileState accountInfo = new SipProfileState(account); Cursor c = service.getContentResolver().query( ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_ID_URI_BASE, account.id), null, null, null, null); if (c != null) { try { if (c.getCount() > 0) { c.moveToFirst(); accountInfo.createFromDb(c); } } catch (Exception e) { Log.e(THIS_FILE, "Error on looping over sip profiles states", e); } finally { c.close(); } } return accountInfo; } private static ArrayList<String> codecs = new ArrayList<String>(); private static ArrayList<String> video_codecs = new ArrayList<String>(); private static boolean codecs_initialized = false; /** * Reset the list of codecs stored */ public static void resetCodecs() { synchronized (codecs) { if (codecs_initialized) { codecs.clear(); video_codecs.clear(); codecs_initialized = false; } } } /** * Retrieve codecs from pjsip stack and store it inside preference storage * so that it can be retrieved in the interface view * * @throws SameThreadException */ private void initCodecs() throws SameThreadException { synchronized (codecs) { if (!codecs_initialized) { int nbrCodecs, i; // Audio codecs nbrCodecs = pjsua.codecs_get_nbr(); for (i = 0; i < nbrCodecs; i++) { String codecId = pjStrToString(pjsua.codecs_get_id(i)); codecs.add(codecId); // Log.d(THIS_FILE, "Added codec " + codecId); } // Set it in prefs if not already set correctly prefsWrapper.setCodecList(codecs); // Video codecs nbrCodecs = pjsua.codecs_vid_get_nbr(); for (i = 0; i < nbrCodecs; i++) { String codecId = pjStrToString(pjsua.codecs_vid_get_id(i)); video_codecs.add(codecId); Log.d(THIS_FILE, "Added video codec " + codecId); } // Set it in prefs if not already set correctly prefsWrapper.setVideoCodecList(video_codecs); codecs_initialized = true; // We are now always capable of tls and srtp ! prefsWrapper.setLibCapability(PreferencesProviderWrapper.LIB_CAP_TLS, true); prefsWrapper.setLibCapability(PreferencesProviderWrapper.LIB_CAP_SRTP, true); } } } /** * Append log for the codec in String builder * * @param sb the buffer to be appended with the codec info * @param codec the codec name * @param prio the priority of the codec */ private void buffCodecLog(StringBuilder sb, String codec, short prio) { if (prio > 0 && Log.getLogLevel() >= 4) { sb.append(codec); sb.append(" ("); sb.append(prio); sb.append(") - "); } } /** * Set the codec priority in pjsip stack layer based on preference store * * @throws SameThreadException */ private void setCodecsPriorities() throws SameThreadException { ConnectivityManager cm = ((ConnectivityManager) service .getSystemService(Context.CONNECTIVITY_SERVICE)); synchronized (codecs) { if (codecs_initialized) { NetworkInfo ni = cm.getActiveNetworkInfo(); if (ni != null) { StringBuilder audioSb = new StringBuilder(); StringBuilder videoSb = new StringBuilder(); audioSb.append("Audio codecs : "); videoSb.append("Video codecs : "); String currentBandType = prefsWrapper.getPreferenceStringValue( SipConfigManager.getBandTypeKey(ni.getType(), ni.getSubtype()), SipConfigManager.CODEC_WB); synchronized (codecs) { for (String codec : codecs) { short aPrio = prefsWrapper.getCodecPriority(codec, currentBandType, "-1"); buffCodecLog(audioSb, codec, aPrio); pj_str_t codecStr = pjsua.pj_str_copy(codec); if (aPrio >= 0) { pjsua.codec_set_priority(codecStr, aPrio); } String codecKey = SipConfigManager.getCodecKey(codec, SipConfigManager.FRAMES_PER_PACKET_SUFFIX); Integer frmPerPacket = SipConfigManager.getPreferenceIntegerValue( service, codecKey); if (frmPerPacket != null && frmPerPacket > 0) { Log.v(THIS_FILE, "Set codec " + codec + " fpp : " + frmPerPacket); pjsua.codec_set_frames_per_packet(codecStr, frmPerPacket); } } for (String codec : video_codecs) { short aPrio = prefsWrapper.getCodecPriority(codec, currentBandType, "-1"); buffCodecLog(videoSb, codec, aPrio); if (aPrio >= 0) { pjsua.vid_codec_set_priority(pjsua.pj_str_copy(codec), aPrio); } String videoSize = SipConfigManager.getPreferenceStringValue(service, SipConfigManager.VIDEO_CAPTURE_SIZE, ""); if (TextUtils.isEmpty(videoSize) || videoSize.equalsIgnoreCase("0x0@0")) { List<VideoCaptureDeviceInfo> cps = VideoUtilsWrapper.getInstance() .getVideoCaptureDevices(service); if (cps.size() > 0) { videoSize = cps.get(cps.size() - 1).bestCapability .toPreferenceValue(); } } VideoCaptureCapability videoCap = new VideoUtilsWrapper.VideoCaptureCapability( videoSize); if (codec.startsWith("H264")) { int h264profile = SipConfigManager.getPreferenceIntegerValue( service, SipConfigManager.H264_PROFILE, 66); int h264level = SipConfigManager.getPreferenceIntegerValue(service, SipConfigManager.H264_LEVEL, 30); int h264bitrate = SipConfigManager.getPreferenceIntegerValue( service, SipConfigManager.H264_BITRATE, 0); if (h264profile > 0) { pjsua.codec_h264_set_profile(h264profile, h264level, videoCap.width, videoCap.height, videoCap.fps, h264bitrate, 0); // pjsua.codec_h264_set_profile(h264profile, // h264level, 352, 480, 15, h264bitrate, 0); // // 352×480 Log.d(THIS_FILE, "Set h264 profile : " + h264profile + ", " + h264level + ", " + h264bitrate); } } } } Log.d(THIS_FILE, audioSb.toString()); Log.d(THIS_FILE, videoSb.toString()); } } } } // Call related /** * Answer a call * * @param callId the id of the call to answer to * @param code the status code to send in the response * @return */ public int callAnswer(int callId, int code) throws SameThreadException { if (created) { pjsua_call_setting cs = new pjsua_call_setting(); pjsua.call_setting_default(cs); cs.setAud_cnt(1); cs.setVid_cnt(prefsWrapper.getPreferenceBooleanValue(SipConfigManager.USE_VIDEO) ? 1 : 0); cs.setFlag(0); return pjsua.call_answer2(callId, cs, code, null, null); // return pjsua.call_answer(callId, code, null, null); } return -1; } /** * Hangup a call * * @param callId the id of the call to hangup * @param code the status code to send in the response * @return */ public int callHangup(int callId, int code) throws SameThreadException { if (created) { return pjsua.call_hangup(callId, code, null, null); } return -1; } public int callXfer(int callId, String callee) throws SameThreadException { if (created) { return pjsua.call_xfer(callId, pjsua.pj_str_copy(callee), null); } return -1; } public int callXferReplace(int callId, int otherCallId, int options) throws SameThreadException { if (created) { return pjsua.call_xfer_replaces(callId, otherCallId, options, null); } return -1; } /** * Make a call * * @param callee remote contact ot call If not well formated we try to add * domain name of the default account */ public int makeCall(String callee, int accountId, Bundle b) throws SameThreadException { if (!created) { return -1; } final ToCall toCall = sanitizeSipUri(callee, accountId); if (toCall != null) { pj_str_t uri = pjsua.pj_str_copy(toCall.getCallee()); // Nothing to do with this values byte[] userData = new byte[1]; int[] callId = new int[1]; pjsua_call_setting cs = new pjsua_call_setting(); pjsua_msg_data msgData = new pjsua_msg_data(); int pjsuaAccId = toCall.getPjsipAccountId(); // Call settings to add video pjsua.call_setting_default(cs); cs.setAud_cnt(1); cs.setVid_cnt(0); if (b != null && b.getBoolean(SipCallSession.OPT_CALL_VIDEO, false)) { cs.setVid_cnt(1); } cs.setFlag(0); pj_pool_t pool = pjsua.pool_create("call_tmp", 512, 512); // Msg data to add headers pjsua.msg_data_init(msgData); pjsua.csipsimple_init_acc_msg_data(pool, pjsuaAccId, msgData); if (b != null) { Bundle extraHeaders = b.getBundle(SipCallSession.OPT_CALL_EXTRA_HEADERS); if (extraHeaders != null) { for (String key : extraHeaders.keySet()) { try { String value = extraHeaders.getString(key); if (!TextUtils.isEmpty(value)) { int res = pjsua.csipsimple_msg_data_add_string_hdr(pool, msgData, pjsua.pj_str_copy(key), pjsua.pj_str_copy(value)); if (res == pjsuaConstants.PJ_SUCCESS) { Log.e(THIS_FILE, "Failed to add Xtra hdr (" + key + " : " + value + ") probably not X- header"); } } } catch (Exception e) { Log.e(THIS_FILE, "Invalid header value for key : " + key); } } } } int status = pjsua.call_make_call(pjsuaAccId, uri, cs, userData, msgData, callId); if (status == pjsuaConstants.PJ_SUCCESS) { dtmfToAutoSend.put(callId[0], toCall.getDtmf()); Log.d(THIS_FILE, "DTMF - Store for " + callId[0] + " - " + toCall.getDtmf()); } pjsua.pj_pool_release(pool); return status; } else { service.notifyUserOfMessage(service.getString(R.string.invalid_sip_uri) + " : " + callee); } return -1; } public int updateCallOptions(int callId, Bundle options) { // TODO : if more options we should redesign this part. if (options.containsKey(SipCallSession.OPT_CALL_VIDEO)) { boolean add = options.getBoolean(SipCallSession.OPT_CALL_VIDEO); SipCallSession ci = getCallInfo(callId); if (add && ci.mediaHasVideo()) { // We already have one video running -- refuse to send another return -1; } else if (!add && !ci.mediaHasVideo()) { // We have no current video, no way to remove. return -1; } pjsua_call_vid_strm_op op = add ? pjsua_call_vid_strm_op.PJSUA_CALL_VID_STRM_ADD : pjsua_call_vid_strm_op.PJSUA_CALL_VID_STRM_REMOVE; if (!add) { // TODO : manage remove case } return pjsua.call_set_vid_strm(callId, op, null); } return -1; } /** * Send a dtmf signal to a call * * @param callId the call to send the signal * @param keyCode the keyCode to send (android style) * @return */ public int sendDtmf(int callId, int keyCode) throws SameThreadException { if (!created) { return -1; } String keyPressed = ""; // Since some device (xoom...) are apparently buggy with key character // map loading... // we have to do crappy thing here if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { keyPressed = Integer.toString(keyCode - KeyEvent.KEYCODE_0); } else if (keyCode == KeyEvent.KEYCODE_POUND) { keyPressed = "#"; } else if (keyCode == KeyEvent.KEYCODE_STAR) { keyPressed = "*"; } else { // Fallback... should never be there if using visible dialpad, but // possible using keyboard KeyCharacterMap km = KeyCharacterMap.load(KeyCharacterMap.NUMERIC); keyPressed = Integer.toString(km.getNumber(keyCode)); } return sendDtmf(callId, keyPressed); } private int sendDtmf(final int callId, String keyPressed) throws SameThreadException { if (TextUtils.isEmpty(keyPressed)) { return pjsua.PJ_SUCCESS; } if (pjsua.call_is_active(callId) != pjsuaConstants.PJ_TRUE) { return -1; } if(pjsua.call_has_media(callId) != pjsuaConstants.PJ_TRUE) { return -1; } String dtmfToDial = keyPressed; String remainingDtmf = ""; int pauseBeforeRemaining = 0; boolean foundSeparator = false; if (keyPressed.contains(",") || keyPressed.contains(";")) { dtmfToDial = ""; for (int i = 0; i < keyPressed.length(); i++) { char c = keyPressed.charAt(i); if (!foundSeparator) { if (c == ',' || c == ';') { pauseBeforeRemaining += (c == ',') ? DTMF_TONE_PAUSE_LENGTH : DTMF_TONE_WAIT_LENGTH; foundSeparator = true; } else { dtmfToDial += c; } } else { if ((c == ',' || c == ';') && TextUtils.isEmpty(remainingDtmf)) { pauseBeforeRemaining += (c == ',') ? DTMF_TONE_PAUSE_LENGTH : DTMF_TONE_WAIT_LENGTH; } else { remainingDtmf += c; } } } } int res = 0; if (!TextUtils.isEmpty(dtmfToDial)) { pj_str_t pjKeyPressed = pjsua.pj_str_copy(dtmfToDial); res = -1; if (prefsWrapper.useSipInfoDtmf()) { res = pjsua.send_dtmf_info(callId, pjKeyPressed); Log.d(THIS_FILE, "Has been sent DTMF INFO : " + res); } else { if (!prefsWrapper.forceDtmfInBand()) { // Generate using RTP res = pjsua.call_dial_dtmf(callId, pjKeyPressed); Log.d(THIS_FILE, "Has been sent in RTP DTMF : " + res); } if (res != pjsua.PJ_SUCCESS && !prefsWrapper.forceDtmfRTP()) { // Generate using analogic inband if (dtmfDialtoneGenerators.get(callId) == null) { dtmfDialtoneGenerators.put(callId, new PjStreamDialtoneGenerator(callId)); } res = dtmfDialtoneGenerators.get(callId).sendPjMediaDialTone(dtmfToDial); Log.d(THIS_FILE, "Has been sent DTMF analogic : " + res); } } } // Finally, push remaining DTMF in the future if (!TextUtils.isEmpty(remainingDtmf)) { dtmfToAutoSend.put(callId, remainingDtmf); if (tasksTimer == null) { tasksTimer = new Timer("com.csipsimple.PjSipServiceTasks"); } TimerTask tt = new TimerTask() { @Override public void run() { service.getExecutor().execute(new SipRunnable() { @Override protected void doRun() throws SameThreadException { Log.d(THIS_FILE, "Running pending DTMF send"); sendPendingDtmf(callId); } }); } }; dtmfTasks.put(callId, tt); Log.d(THIS_FILE, "Schedule DTMF " + remainingDtmf + " in " + pauseBeforeRemaining); tasksTimer.schedule(tt, pauseBeforeRemaining); } else { if (dtmfToAutoSend.get(callId) != null) { dtmfToAutoSend.put(callId, null); } if (dtmfTasks.get(callId) != null) { dtmfTasks.put(callId, null); } } return res; } /** * Send sms/message using SIP server */ public ToCall sendMessage(String callee, String message, long accountId) throws SameThreadException { if (!created) { return null; } ToCall toCall = sanitizeSipUri(callee, accountId); if (toCall != null) { pj_str_t uri = pjsua.pj_str_copy(toCall.getCallee()); pj_str_t text = pjsua.pj_str_copy(message); /* * Log.d(THIS_FILE, "get for outgoing"); int finalAccountId = * accountId; if (accountId == -1) { finalAccountId = * pjsua.acc_find_for_outgoing(uri); } */ // Nothing to do with this values byte[] userData = new byte[1]; int status = pjsua.im_send(toCall.getPjsipAccountId(), uri, null, text, null, userData); return (status == pjsuaConstants.PJ_SUCCESS) ? toCall : null; } return toCall; } /** * Add a buddy to buddies list * * @param buddyUri the uri to register to * @throws SameThreadException */ public int addBuddy(String buddyUri) throws SameThreadException { if (!created) { return -1; } int[] p_buddy_id = new int[1]; pjsua_buddy_config buddy_cfg = new pjsua_buddy_config(); pjsua.buddy_config_default(buddy_cfg); buddy_cfg.setSubscribe(1); buddy_cfg.setUri(pjsua.pj_str_copy(buddyUri)); pjsua.buddy_add(buddy_cfg, p_buddy_id); return p_buddy_id[0]; } /** * Remove one buddy from the buddy list managed by pjsip * * @param buddyUri he uri to unregister * @throws SameThreadException */ public void removeBuddy(String buddyUri) throws SameThreadException { if (!created) { return; } int buddyId = pjsua.buddy_find(pjsua.pj_str_copy(buddyUri)); if (buddyId >= 0) { pjsua.buddy_del(buddyId); } } public void sendPendingDtmf(int callId) throws SameThreadException { if (dtmfToAutoSend.get(callId) != null) { Log.d(THIS_FILE, "DTMF - Send pending dtmf " + dtmfToAutoSend.get(callId) + " for " + callId); sendDtmf(callId, dtmfToAutoSend.get(callId)); } } public void stopDialtoneGenerator(int callId) { if (dtmfDialtoneGenerators.get(callId) != null) { dtmfDialtoneGenerators.get(callId).stopDialtoneGenerator(); dtmfDialtoneGenerators.put(callId, null); } if (dtmfToAutoSend.get(callId) != null) { dtmfToAutoSend.put(callId, null); } if (dtmfTasks.get(callId) != null) { dtmfTasks.get(callId).cancel(); dtmfTasks.put(callId, null); } } public void startWaittoneGenerator(int callId) { if (waittoneGenerators.get(callId) == null) { waittoneGenerators.put(callId, new PjStreamDialtoneGenerator(callId, false)); } waittoneGenerators.get(callId).startPjMediaWaitingTone(); } public void stopWaittoneGenerator(int callId) { if (waittoneGenerators.get(callId) != null) { waittoneGenerators.get(callId).stopDialtoneGenerator(); waittoneGenerators.put(callId, null); } } public int callHold(int callId) throws SameThreadException { if (created) { return pjsua.call_set_hold(callId, null); } return -1; } public int callReinvite(int callId, boolean unhold) throws SameThreadException { if (created) { return pjsua.call_reinvite(callId, unhold ? pjsua_call_flag.PJSUA_CALL_UNHOLD.swigValue() : 0, null); } return -1; } public SipCallSession getCallInfo(int callId) { if (created/* && !creating */&& userAgentReceiver != null) { SipCallSession callInfo = userAgentReceiver.getCallInfo(callId); return callInfo; } return null; } public SipCallSession getPublicCallInfo(int callId) { SipCallSession internalCallSession = getCallInfo(callId); if( internalCallSession == null) { return null; } return new SipCallSession(internalCallSession); } public void setBluetoothOn(boolean on) throws SameThreadException { if (created && mediaManager != null) { mediaManager.setBluetoothOn(on); } } /** * Mute microphone * * @param on true if microphone has to be muted * @throws SameThreadException */ public void setMicrophoneMute(boolean on) throws SameThreadException { if (created && mediaManager != null) { mediaManager.setMicrophoneMute(on); } } /** * Change speaker phone mode * * @param on true if the speaker mode has to be on. * @throws SameThreadException */ public void setSpeakerphoneOn(boolean on) throws SameThreadException { if (created && mediaManager != null) { mediaManager.setSpeakerphoneOn(on); } } public SipCallSession[] getCalls() { if (created && userAgentReceiver != null) { SipCallSession[] callsInfo = userAgentReceiver.getCalls(); return callsInfo; } return new SipCallSession[0]; } public void confAdjustTxLevel(int port, float value) throws SameThreadException { if (created && userAgentReceiver != null) { pjsua.conf_adjust_tx_level(port, value); } } public void confAdjustRxLevel(int port, float value) throws SameThreadException { if (created && userAgentReceiver != null) { pjsua.conf_adjust_rx_level(port, value); } } public void setEchoCancellation(boolean on) throws SameThreadException { if (created && userAgentReceiver != null) { Log.d(THIS_FILE, "set echo cancelation " + on); pjsua.set_ec(on ? prefsWrapper.getEchoCancellationTail() : 0, prefsWrapper.getPreferenceIntegerValue(SipConfigManager.ECHO_MODE)); } } public void adjustStreamVolume(int stream, int direction, int flags) { if (mediaManager != null) { mediaManager.adjustStreamVolume(stream, direction, AudioManager.FLAG_SHOW_UI); } } public void silenceRinger() { if (mediaManager != null) { mediaManager.stopRingAndUnfocus(); } } /** * Change account registration / adding state * * @param account The account to modify registration * @param renew if 0 we ask for deletion of this account; if 1 we ask for * registration of this account (and add if necessary) * @param forceReAdd if true, we will first remove the account and then * re-add it * @return true if the operation get completed without problem * @throws SameThreadException */ public boolean setAccountRegistration(SipProfile account, int renew, boolean forceReAdd) throws SameThreadException { int status = -1; if (!created || account == null) { Log.e(THIS_FILE, "PJSIP is not started here, nothing can be done"); return false; } if (account.id == SipProfile.INVALID_ID) { Log.w(THIS_FILE, "Trying to set registration on a deleted account"); return false; } SipProfileState profileState = getProfileState(account); // If local account -- Ensure we are not deleting, because this would be // invalid if (profileState.getWizard().equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) { if (renew == 0) { return false; } } // In case of already added, we have to act finely // If it's local we can just consider that we have to re-add account // since it will actually just touch the account with a modify if (profileState != null && profileState.isAddedToStack() && !profileState.getWizard().equalsIgnoreCase(WizardUtils.LOCAL_WIZARD_TAG)) { // The account is already there in accounts list service.getContentResolver().delete( ContentUris.withAppendedId(SipProfile.ACCOUNT_STATUS_URI, account.id), null, null); Log.d(THIS_FILE, "Account already added to stack, remove and re-load or delete"); if (renew == 1) { if (forceReAdd) { status = pjsua.acc_del(profileState.getPjsuaId()); addAccount(account); } else { pjsua.acc_set_online_status(profileState.getPjsuaId(), getOnlineForStatus(service.getPresence())); status = pjsua.acc_set_registration(profileState.getPjsuaId(), renew); } } else { // if(status == pjsuaConstants.PJ_SUCCESS && renew == 0) { Log.d(THIS_FILE, "Delete account !!"); status = pjsua.acc_del(profileState.getPjsuaId()); } } else { if (renew == 1) { addAccount(account); } else { Log.w(THIS_FILE, "Ask to unregister an unexisting account !!" + account.id); } } // PJ_SUCCESS = 0 return status == 0; } /** * Set self presence * * @param presence the SipManager.SipPresence * @param statusText the text of the presence * @throws SameThreadException */ public void setPresence(PresenceStatus presence, String statusText, long accountId) throws SameThreadException { if (!created) { Log.e(THIS_FILE, "PJSIP is not started here, nothing can be done"); return; } SipProfile account = new SipProfile(); account.id = accountId; SipProfileState profileState = getProfileState(account); // In case of already added, we have to act finely // If it's local we can just consider that we have to re-add account // since it will actually just touch the account with a modify if (profileState != null && profileState.isAddedToStack()) { // The account is already there in accounts list pjsua.acc_set_online_status(profileState.getPjsuaId(), getOnlineForStatus(presence)); } } private int getOnlineForStatus(PresenceStatus presence) { return presence == PresenceStatus.ONLINE ? 1 : 0; } public static long getAccountIdForPjsipId(Context ctxt, int pjId) { long accId = SipProfile.INVALID_ID; Cursor c = ctxt.getContentResolver().query(SipProfile.ACCOUNT_STATUS_URI, null, null, null, null); if (c != null) { try { c.moveToFirst(); do { int pjsuaId = c.getInt(c.getColumnIndex(SipProfileState.PJSUA_ID)); Log.d(THIS_FILE, "Found pjsua " + pjsuaId + " searching " + pjId); if (pjsuaId == pjId) { accId = c.getInt(c.getColumnIndex(SipProfileState.ACCOUNT_ID)); break; } } while (c.moveToNext()); } catch (Exception e) { Log.e(THIS_FILE, "Error on looping over sip profiles", e); } finally { c.close(); } } return accId; } public SipProfile getAccountForPjsipId(int pjId) { long accId = getAccountIdForPjsipId(service, pjId); if (accId == SipProfile.INVALID_ID) { return null; } else { return service.getAccount(accId); } } public int validateAudioClockRate(int aClockRate) { if (mediaManager != null) { return mediaManager.validateAudioClockRate(aClockRate); } return -1; } public void setAudioInCall(int beforeInit) { if (mediaManager != null) { mediaManager.setAudioInCall(beforeInit == pjsuaConstants.PJ_TRUE); } } public void unsetAudioInCall() { if (mediaManager != null) { mediaManager.unsetAudioInCall(); } } public SipCallSession getActiveCallInProgress() { if (created && userAgentReceiver != null) { return userAgentReceiver.getActiveCallInProgress(); } return null; } public void refreshCallMediaState(final int callId) { service.getExecutor().execute(new SipRunnable() { @Override public void doRun() throws SameThreadException { if (created && userAgentReceiver != null) { userAgentReceiver.updateCallMediaState(callId); } } }); } /** * Transform a string callee into a valid sip uri in the context of an * account * * @param callee the callee string to call * @param accountId the context account * @return ToCall object representing what to call and using which account */ private ToCall sanitizeSipUri(String callee, long accountId) throws SameThreadException { // accountId is the id in term of csipsimple database // pjsipAccountId is the account id in term of pjsip adding int pjsipAccountId = (int) SipProfile.INVALID_ID; // Fake a sip profile empty to get it's profile state // Real get from db will be done later SipProfile account = new SipProfile(); account.id = accountId; SipProfileState profileState = getProfileState(account); long finalAccountId = accountId; // If this is an invalid account id if (accountId == SipProfile.INVALID_ID || !profileState.isAddedToStack()) { int defaultPjsipAccount = pjsua.acc_get_default(); boolean valid = false; account = getAccountForPjsipId(defaultPjsipAccount); if (account != null) { profileState = getProfileState(account); valid = profileState.isAddedToStack(); } // If default account is not active if (!valid) { Cursor c = service.getContentResolver().query(SipProfile.ACCOUNT_STATUS_URI, null, null, null, null); if (c != null) { try { if (c.getCount() > 0) { c.moveToFirst(); do { SipProfileState ps = new SipProfileState(c); if (ps.isValidForCall()) { finalAccountId = ps.getAccountId(); pjsipAccountId = ps.getPjsuaId(); break; } } while (c.moveToNext()); } } catch (Exception e) { Log.e(THIS_FILE, "Error on looping over sip profiles state", e); } finally { c.close(); } } } else { // Use the default account finalAccountId = profileState.getAccountId(); pjsipAccountId = profileState.getPjsuaId(); } } else { // If the account is valid pjsipAccountId = profileState.getPjsuaId(); } if (pjsipAccountId == SipProfile.INVALID_ID) { Log.e(THIS_FILE, "Unable to find a valid account for this call"); return null; } // Check integrity of callee field // Get real account information now account = service.getAccount((int) finalAccountId); ParsedSipContactInfos finalCallee = account.formatCalleeNumber(callee); String digitsToAdd = null; if (!TextUtils.isEmpty(finalCallee.userName) && (finalCallee.userName.contains(",") || finalCallee.userName.contains(";"))) { int commaIndex = finalCallee.userName.indexOf(","); int semiColumnIndex = finalCallee.userName.indexOf(";"); if (semiColumnIndex > 0 && semiColumnIndex < commaIndex) { commaIndex = semiColumnIndex; } digitsToAdd = finalCallee.userName.substring(commaIndex); finalCallee.userName = finalCallee.userName.substring(0, commaIndex); } Log.d(THIS_FILE, "will call " + finalCallee); if (pjsua.verify_sip_url(finalCallee.toString(false)) == 0) { // In worse worse case, find back the account id for uri.. but // probably useless case if (pjsipAccountId == SipProfile.INVALID_ID) { pjsipAccountId = pjsua.acc_find_for_outgoing(pjsua.pj_str_copy(finalCallee .toString(false))); } return new ToCall(pjsipAccountId, finalCallee.toString(true), digitsToAdd); } return null; } public void onGSMStateChanged(int state, String incomingNumber) throws SameThreadException { // Avoid ringing if new GSM state is not idle if (state != TelephonyManager.CALL_STATE_IDLE && mediaManager != null) { mediaManager.stopRingAndUnfocus(); } // If new call state is not idle if (state != TelephonyManager.CALL_STATE_IDLE && userAgentReceiver != null) { SipCallSession currentActiveCall = userAgentReceiver.getActiveCallOngoing(); // If we have a sip call on our side if (currentActiveCall != null) { AudioManager am = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE); if (state == TelephonyManager.CALL_STATE_OFFHOOK) { // GSM is now off hook => hold current sip call hasBeenHoldByGSM = currentActiveCall.getCallId(); callHold(hasBeenHoldByGSM); pjsua.set_no_snd_dev(); am.setMode(AudioManager.MODE_IN_CALL); } else { // We have a ringing incoming call. // Avoid ringing hasBeenChangedRingerMode = am.getRingerMode(); am.setRingerMode(AudioManager.RINGER_MODE_SILENT); // And try to notify with tone if (mediaManager != null) { mediaManager.playInCallTone(MediaManager.TONE_CALL_WAITING); } } } } else { // GSM is now back to an IDLE state, resume previously stopped SIP // calls if (hasBeenHoldByGSM != null && isCreated()) { pjsua.set_snd_dev(0, 0); callReinvite(hasBeenHoldByGSM, true); hasBeenHoldByGSM = null; } // GSM is now back to an IDLE state, reset ringerMode if was // changed. if (hasBeenChangedRingerMode != null) { AudioManager am = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE); am.setRingerMode(hasBeenChangedRingerMode); hasBeenChangedRingerMode = null; } } } /* * public void sendKeepAlivePackets() throws SameThreadException { * ArrayList<SipProfileState> accounts = getActiveProfilesState(); for * (SipProfileState acc : accounts) { * pjsua.send_keep_alive(acc.getPjsuaId()); } } */ public void zrtpSASVerified(int callId) throws SameThreadException { if (!created) { return; } pjsua.jzrtp_SASVerified(callId); } public void zrtpSASRevoke(int callId) throws SameThreadException { if (!created) { return; } pjsua.jzrtp_SASRevoked(callId); } protected void setDetectedNatType(String natName, int status) { // Maybe we will need to treat status to eliminate some set (depending of unknown string fine for 3rd part dev) mNatDetected = natName; } /** * @return nat type name detected by pjsip. Empty string if nothing detected */ public String getDetectedNatType() { return mNatDetected; } // Config subwrapper private pj_str_t[] getNameservers() { pj_str_t[] nameservers = null; if (prefsWrapper.enableDNSSRV()) { String prefsDNS = prefsWrapper .getPreferenceStringValue(SipConfigManager.OVERRIDE_NAMESERVER); if (TextUtils.isEmpty(prefsDNS)) { String ipv6Escape = "[ \\[\\]]"; String ipv4Matcher = "^\\d+(\\.\\d+){3}$"; String ipv6Matcher = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$"; List<String> dnsServers; List<String> dnsServersAll = new ArrayList<String>(); List<String> dnsServersIpv4 = new ArrayList<String>(); for (int i = 1; i <= 2; i++) { String dnsName = prefsWrapper.getSystemProp("net.dns" + i); if (!TextUtils.isEmpty(dnsName)) { dnsName = dnsName.replaceAll(ipv6Escape, ""); if (!TextUtils.isEmpty(dnsName) && !dnsServersAll.contains(dnsName)) { if (dnsName.matches(ipv4Matcher) || dnsName.matches(ipv6Matcher)) { dnsServersAll.add(dnsName); } if (dnsName.matches(ipv4Matcher)) { dnsServersIpv4.add(dnsName); } } } } if (dnsServersIpv4.size() > 0) { // Prefer pure ipv4 list since pjsua doesn't manage ipv6 // resolution yet dnsServers = dnsServersIpv4; } else { dnsServers = dnsServersAll; } if (dnsServers.size() == 0) { // This is the ultimate fallback... we should never be there // ! nameservers = new pj_str_t[] { pjsua.pj_str_copy("127.0.0.1") }; } else if (dnsServers.size() == 1) { nameservers = new pj_str_t[] { pjsua.pj_str_copy(dnsServers.get(0)) }; } else { nameservers = new pj_str_t[] { pjsua.pj_str_copy(dnsServers.get(0)), pjsua.pj_str_copy(dnsServers.get(1)) }; } } else { nameservers = new pj_str_t[] { pjsua.pj_str_copy(prefsDNS) }; } } return nameservers; } private pjmedia_srtp_use getUseSrtp() { try { int use_srtp = Integer.parseInt(prefsWrapper .getPreferenceStringValue(SipConfigManager.USE_SRTP)); if (use_srtp >= 0) { return pjmedia_srtp_use.swigToEnum(use_srtp); } } catch (NumberFormatException e) { Log.e(THIS_FILE, "Transport port not well formated"); } return pjmedia_srtp_use.PJMEDIA_SRTP_DISABLED; } public void setNoSnd() throws SameThreadException { if (!created) { return; } pjsua.set_no_snd_dev(); } public void setSnd() throws SameThreadException { if (!created) { return; } pjsua.set_snd_dev(0, 0); } // Recorder private SparseArray<List<IRecorderHandler>> callRecorders = new SparseArray<List<IRecorderHandler>>(); /** * Start recording of a call. * * @param callId the call id of the call to record * @throws SameThreadException virtual exception to be sure we are calling * this from correct thread */ public void startRecording(int callId, int way) throws SameThreadException { // Make sure we are in a valid state for recording if (!canRecord(callId)) { return; } // Sanitize call way : if 0 assume all if (way == 0) { way = SipManager.BITMASK_ALL; } try { File recFolder = PreferencesProviderWrapper.getRecordsFolder(service); IRecorderHandler recoder = new SimpleWavRecorderHandler(getCallInfo(callId), recFolder, way); List<IRecorderHandler> recordersList = callRecorders.get(callId, new ArrayList<IRecorderHandler>()); recordersList.add(recoder); callRecorders.put(callId, recordersList); recoder.startRecording(); userAgentReceiver.updateRecordingStatus(callId, false, true); } catch (IOException e) { service.notifyUserOfMessage(R.string.cant_write_file); } catch (RuntimeException e) { Log.e(THIS_FILE, "Impossible to record ", e); } } /** * Stop recording of a call. * * @param callId the call to stop record for. * @throws SameThreadException virtual exception to be sure we are calling * this from correct thread */ public void stopRecording(int callId) throws SameThreadException { if (!created) { return; } List<IRecorderHandler> recoders = callRecorders.get(callId, null); if (recoders != null) { for (IRecorderHandler recoder : recoders) { recoder.stopRecording(); // Broadcast to other apps the a new sip record has been done SipCallSession callInfo = getPublicCallInfo(callId); Intent it = new Intent(SipManager.ACTION_SIP_CALL_RECORDED); it.putExtra(SipManager.EXTRA_CALL_INFO, callInfo); recoder.fillBroadcastWithInfo(it); service.sendBroadcast(it, SipManager.PERMISSION_USE_SIP); } // In first case we drop everything callRecorders.delete(callId); userAgentReceiver.updateRecordingStatus(callId, true, false); } } /** * Can we record for this call id ? * * @param callId The call id to record to a file * @return true if seems to be possible to record this call. */ public boolean canRecord(int callId) { if (!created) { // Not possible to record if service not here return false; } SipCallSession callInfo = getCallInfo(callId); if (callInfo == null) { // Not possible to record if no call info for given call id return false; } int ms = callInfo.getMediaStatus(); if (ms != SipCallSession.MediaState.ACTIVE && ms != SipCallSession.MediaState.REMOTE_HOLD) { // We can't record if media state not running on our side return false; } return true; } /** * Are we currently recording the call? * * @param callId The call id to test for a recorder presence * @return true if recording this call */ public boolean isRecording(int callId) throws SameThreadException { List<IRecorderHandler> recorders = callRecorders.get(callId, null); if (recorders == null) { return false; } return recorders.size() > 0; } // Stream players // We use a list for future possible extensions. For now api only manages // one private SparseArray<List<IPlayerHandler>> callPlayers = new SparseArray<List<IPlayerHandler>>(); /** * Play one wave file in call stream. * * @param filePath The path to the file we'd like to play * @param callId The call id we want to play to. Even if we only use * {@link SipManager#BITMASK_IN} this must correspond to some * call since it's used to identify internally created player. * @param way The way we want to play this file to. Bitmasked value that * could be compounded of {@link SipManager#BITMASK_IN} (read * local) and {@link SipManager#BITMASK_OUT} (read to remote * party of the call) * @throws SameThreadException virtual exception to be sure we are calling * this from correct thread */ public void playWaveFile(String filePath, int callId, int way) throws SameThreadException { if (!created) { return; } // Stop any current player stopPlaying(callId); if (TextUtils.isEmpty(filePath)) { // Nothing to do if we have not file path return; } if (way == 0) { way = SipManager.BITMASK_ALL; } // We create a new player conf port. try { IPlayerHandler player = new SimpleWavPlayerHandler(getCallInfo(callId), filePath, way); List<IPlayerHandler> playersList = callPlayers.get(callId, new ArrayList<IPlayerHandler>()); playersList.add(player); callPlayers.put(callId, playersList); player.startPlaying(); } catch (IOException e) { // TODO : add a can't read file txt service.notifyUserOfMessage(R.string.cant_write_file); } catch (RuntimeException e) { Log.e(THIS_FILE, "Impossible to play file", e); } } /** * Stop eventual player for a given call. * * @param callId the call id corresponding to player previously created with * {@link #playWaveFile(String, int, int)} * @throws SameThreadException virtual exception to be sure we are calling * this from correct thread */ public void stopPlaying(int callId) throws SameThreadException { List<IPlayerHandler> players = callPlayers.get(callId, null); if (players != null) { for (IPlayerHandler player : players) { player.stopPlaying(); } callPlayers.delete(callId); } } public void updateTransportIp(String oldIPAddress) throws SameThreadException { if (!created) { return; } Log.d(THIS_FILE, "Trying to update my address in the current call to " + oldIPAddress); pjsua.update_transport(pjsua.pj_str_copy(oldIPAddress)); } public static String pjStrToString(pj_str_t pjStr) { try { if (pjStr != null) { // If there's utf-8 ptr length is possibly lower than slen int len = pjStr.getSlen(); if (len > 0 && pjStr.getPtr() != null) { // Be robust to smaller length detected if (pjStr.getPtr().length() < len) { len = pjStr.getPtr().length(); } if (len > 0) { return pjStr.getPtr().substring(0, len); } } } } catch (StringIndexOutOfBoundsException e) { Log.e(THIS_FILE, "Impossible to retrieve string from pjsip ", e); } return ""; } /** * Get the signal level * @param port The pjsip port to get signal from * @return an encoded long with rx level on higher byte and tx level on lower byte */ public long getRxTxLevel(int port) { long[] rx_level = new long[1]; long[] tx_level = new long[1]; pjsua.conf_get_signal_level(port, tx_level, rx_level); return (rx_level[0] << 8 | tx_level[0]); } /** * Connect mic source to speaker output. * Usefull for tests. */ public void startLoopbackTest() { pjsua.conf_connect(0, 0); } /** * Stop connection between mic source to speaker output. * @see startLoopbackTest */ public void stopLoopbackTest() { pjsua.conf_disconnect(0, 0); } private Map<String, PjsipModule> pjsipModules = new HashMap<String, PjsipModule>(); private void initModules() { // TODO : this should be more modular and done from outside PjsipModule rModule = new RegHandlerModule(); pjsipModules.put(RegHandlerModule.class.getCanonicalName(), rModule); rModule = new SipClfModule(); pjsipModules.put(SipClfModule.class.getCanonicalName(), rModule); rModule = new EarlyLockModule(); pjsipModules.put(EarlyLockModule.class.getCanonicalName(), rModule); for (PjsipModule mod : pjsipModules.values()) { mod.setContext(service); } } public interface PjsipModule { /** * Set the android context for the module. Could be usefull to get * preferences for examples. * * @param ctxt android context */ void setContext(Context ctxt); /** * Here pjsip endpoint should have this module added. */ void onBeforeStartPjsip(); /** * This is fired just after account was added to pjsip and before will * be registered. Modules does not necessarily implement something here. * * @param pjId the pjsip id of the added account. * @param acc the profile account. */ void onBeforeAccountStartRegistration(int pjId, SipProfile acc); } /** * Provide video render surface to native code. * @param callId The call id for this video surface * @param window The video surface object */ public void setVideoAndroidRenderer(int callId, SurfaceView window) { pjsua.vid_set_android_renderer(callId, (Object) window); } /** * Provide video capturer surface view (the one binded to camera). * @param window The surface view object */ public void setVideoAndroidCapturer(SurfaceView window) { pjsua.vid_set_android_capturer((Object) window); } private static int boolToPjsuaConstant(boolean v) { return v ? pjsuaConstants.PJ_TRUE : pjsuaConstants.PJ_FALSE; } }