package com.tutpro.baresip.plus import android.Manifest import android.annotation.SuppressLint import android.app.* import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothHeadset import android.bluetooth.BluetoothManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL import android.database.ContentObserver import android.graphics.BitmapFactory import android.graphics.Rect import android.hardware.display.DisplayManager import android.media.* import android.media.AudioManager.MODE_IN_COMMUNICATION import android.media.AudioManager.MODE_NORMAL import android.media.AudioManager.RINGER_MODE_SILENT import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import android.net.* import android.net.wifi.WifiManager import android.os.* import android.os.Build.VERSION import android.provider.ContactsContract import android.provider.Settings import android.telecom.TelecomManager import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.util.JsonReader import android.util.Size import android.view.LayoutInflater import android.view.View import android.widget.RemoteViews import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.ColorRes import androidx.annotation.Keep import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData import androidx.media.AudioAttributesCompat import androidx.media.AudioFocusRequestCompat import androidx.media.AudioManagerCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONObject import java.io.File import java.io.IOException import java.net.InetAddress import java.nio.ByteBuffer import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.time.Clock import java.time.LocalDate import java.time.LocalDateTime import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt class BaresipService: Service() { internal lateinit var intent: Intent private lateinit var am: AudioManager private lateinit var rt: Ringtone private lateinit var nt: Ringtone private lateinit var nm: NotificationManager private lateinit var snb: NotificationCompat.Builder private lateinit var cm: ConnectivityManager private lateinit var pm: PowerManager private lateinit var wm: WifiManager private lateinit var tm: TelecomManager private lateinit var btm: BluetoothManager private lateinit var vibrator: Vibrator private lateinit var partialWakeLock: PowerManager.WakeLock private lateinit var proximityWakeLock: PowerManager.WakeLock private lateinit var wifiLock: WifiManager.WifiLock private lateinit var bluetoothReceiver: BroadcastReceiver private lateinit var hotSpotReceiver: BroadcastReceiver private lateinit var androidContactsObserver: ContentObserver private lateinit var stopState: String private lateinit var quitTimer: CountDownTimer private var vbTimer: Timer? = null private var origVolume = mutableMapOf() private var linkAddresses = mutableMapOf() private var activeNetwork: Network? = null private var allNetworks = mutableSetOf() private var hotSpotIsEnabled = false private var hotSpotAddresses = mapOf() private var mediaPlayer: MediaPlayer? = null private var androidContactsObserverRegistered = false private var isServiceClean = false private lateinit var sipReqeustReceiver: BroadcastReceiver @SuppressLint("WakelockTimeout") override fun onCreate() { super.onCreate() Log.d(TAG, "BaresipService onCreate") intent = Intent("com.tutpro.baresip.plus.EVENT") intent.setPackage("com.tutpro.baresip.plus") filesPath = filesDir.absolutePath pName = packageName am = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, RingtoneManager.TYPE_NOTIFICATION) nt = RingtoneManager.getRingtone(applicationContext, ntUri) val rtUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, RingtoneManager.TYPE_RINGTONE) rt = RingtoneManager.getRingtone(applicationContext, rtUri) nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager createNotificationChannels() snb = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID) pm = getSystemService(Context.POWER_SERVICE) as PowerManager vibrator = if (VERSION.SDK_INT >= 31) { val vibratorManager = applicationContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") applicationContext.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator } // This is needed to keep service running also in Doze Mode partialWakeLock = pm.run { newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "com.tutpro.baresip:partial_wakelog" ).apply { acquire() } } cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val builder = NetworkRequest.Builder() .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) cm.registerNetworkCallback( builder.build(), object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) Log.d(TAG, "Network $network is available") if (network !in allNetworks) allNetworks.add(network) val netInfo = Utils.getEthernetInfo(applicationContext) Utils.propertySet("sys.ritosip.ipv4.address", netInfo.get("ip")!!) val newIntent = Intent() newIntent.setClassName("kr.co.rito.ritosip", "com.tutpro.baresip.plus.MainActivity") newIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK newIntent.putExtra("action", "network available") newIntent.putExtra("address", netInfo.get("ip")) deviceIpAddress = netInfo.get("ip")!! startActivity(newIntent) } override fun onLosing(network: Network, maxMsToLive: Int) { super.onLosing(network, maxMsToLive) Log.d(TAG, "Network $network is losing after $maxMsToLive ms") } override fun onLost(network: Network) { super.onLost(network) Log.d(TAG, "Network $network is lost") if (network in allNetworks) allNetworks.remove(network) if (isServiceRunning) updateNetwork() } override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { super.onCapabilitiesChanged(network, caps) Log.d(TAG, "Network $network capabilities changed: $caps") if (network !in allNetworks) allNetworks.add(network) if (isServiceRunning) updateNetwork() } override fun onLinkPropertiesChanged(network: Network, props: LinkProperties) { super.onLinkPropertiesChanged(network, props) Log.d(TAG, "Network $network link properties changed: $props") if (network !in allNetworks) allNetworks.add(network) if (isServiceRunning) updateNetwork() } } ) fun aorGenerateHistory(aor: String) { CallsActivity.uaHistory.clear() for (i in BaresipService.callHistory.indices.reversed()) { val h = BaresipService.callHistory[i] if (h.aor == aor) { val direction: Int = if (h.direction == "in") { if (h.startTime != null) { R.drawable.call_down_green } else { if (h.rejected) R.drawable.call_down_red else R.drawable.call_missed_in } } else { if (h.startTime != null) { R.drawable.call_up_green } else { if (h.rejected) R.drawable.call_up_red else R.drawable.call_missed_out } } //println("[History] ${h.startTime?.toZonedDateTime()} ${h.peerUri} ${h.direction}") CallsActivity.uaHistory.add( CallRow( h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording ) ) } } } sipReqeustReceiver = object : BroadcastReceiver() { fun sendContactList() { val resArr : JSONArray = JSONArray() for(idx in 0..baresipContacts.size - 1) { val res : JSONObject = JSONObject() val row = baresipContacts[idx] var directionStr : String res.put("id", row.id) res.put("name", row.name) res.put("favorite", row.favorite) res.put("uri", row.uri) resArr.put(res) } val responseIntent = Intent("kr.co.rito.ritosip.RESPONSE") responseIntent.putExtra("req", "contact_list") responseIntent.putExtra("data", resArr.toString()) responseIntent.setPackage("kr.co.rito.sipsvc"); sendBroadcast(responseIntent) } override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == "kr.co.rito.ritosip.REQUEST") { val req = intent?.getStringExtra("request") println("[RITO] GOT Req : ${req}") if(req == "call_history") { if(uas.size > 0) { aorGenerateHistory(BaresipService.uas[0].account.aor) } val resArr : JSONArray = JSONArray() for(idx in 0..CallsActivity.uaHistory.size - 1) { val res : JSONObject = JSONObject() val row = CallsActivity.uaHistory[idx] var directionStr : String if(row.direction == R.drawable.call_down_green || row.direction == R.drawable.call_down_red || row.direction == R.drawable.call_missed_in) { directionStr = "in" } else { directionStr = "out" } res.put("peerUri", row.peerUri) res.put("direction", directionStr) res.put("stopTime", row.stopTime.toZonedDateTime()) resArr.put(res) } val responseIntent = Intent("kr.co.rito.ritosip.RESPONSE") responseIntent.putExtra("req", req) responseIntent.putExtra("data", resArr.toString()) responseIntent.setPackage("kr.co.rito.sipsvc"); context?.sendBroadcast(responseIntent) } else if(req == "contact_list") { sendContactList() } else if(req == "save_address") { val param = intent?.getStringExtra("param") var result = "failed" if(param != null) { val json = JSONObject(param) val name = json.getString("name") //val favorite = json.getString("favorite") val uri = json.getString("uri") val contact = Contact.BaresipContact(name, uri, 0, SystemClock.elapsedRealtime(), false) Contact.addBaresipContact(contact) Contact.saveBaresipContacts() result = "success" } val responseIntent = Intent("kr.co.rito.ritosip.RESPONSE") responseIntent.putExtra("req", req) responseIntent.putExtra("data", result) responseIntent.setPackage("kr.co.rito.sipsvc") context?.sendBroadcast(responseIntent) sendContactList() } else if(req == "remove_address") { val param = intent?.getStringExtra("param") var result = "failed" if(param != null) { val json = JSONObject(param) val uri = json.getString("uri") val iterator = baresipContacts.iterator() while (iterator.hasNext()) { val it = iterator.next() if(it.uri == uri) { iterator.remove() Contact.saveBaresipContacts() result = "success" } } } val responseIntent = Intent("kr.co.rito.ritosip.RESPONSE") responseIntent.putExtra("req", req) responseIntent.putExtra("data", result) responseIntent.setPackage("kr.co.rito.sipsvc"); context?.sendBroadcast(responseIntent) sendContactList() } if(req == "start_call") { if(uas.size > 0) { val param = intent?.getStringExtra("param") if (param != null) { val json = JSONObject(param) uas[0].account.resumeUri = json.getString("number") } else { uas[0].account.resumeUri = "7779" } println("resumeUri : ${uas[0].account.resumeUri}") val newIntent = Intent(context, MainActivity::class.java) newIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK newIntent.putExtra("action", "video call") newIntent.putExtra("peer", uas[0].account.resumeUri) startActivity(newIntent) } } else if(req == "stop_call") { if(uas.size > 0) { val newIntent = Intent(context, MainActivity::class.java) newIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK newIntent.putExtra("action", "hangup") startActivity(newIntent) } } else if(req == "audio_mute") { Api.calls_mute(true) isMicMuted = true } else if(req == "audio_unmute") { Api.calls_mute(false) isMicMuted = false } else if(req == "set_account") { UserAgent.deleteAllUas() val param = intent?.getStringExtra("param") if(param != null) { try { val json = JSONObject(param) val display_name = json.getString("display_name") val account_name = json.getString("account_name") val password = json.getString("password") val server_address = json.getString("server_address") val transport = json.getString("transport") val media_encryption = json.getString("media_encryption") //var param = "transport=tls" val ua = UserAgent.uaAlloc("sip:${account_name}@${server_address}") if (ua != null) { val acc = ua.account acc.nickName = display_name acc.displayName = acc.nickName //acc.authUser = account_name //acc.authPass = password Api.account_set_display_name(acc.accp, display_name) Api.account_set_auth_user(acc.accp, account_name) acc.authUser = Api.account_auth_user(acc.accp) Api.account_set_auth_pass(acc.accp, password) acc.authPass = Api.account_auth_pass(acc.accp) Api.account_set_answermode(acc.accp, Api.ANSWERMODE_AUTO) acc.answerMode = Api.ANSWERMODE_AUTO /* Api.log_level_set(0) Log.logLevelSet(0) Api.uag_enable_sip_trace(true) */ Api.ua_register(ua.uap) if (transport != null && transport.length > 0) { Config.replaceVariable("prefer_transport", transport) BaresipService.preferTransport = transport Config.save() } applyTransportConfiguration() if (media_encryption != null && media_encryption.length > 0) { var mediaEnc = "" if(media_encryption == "srtpo") { mediaEnc = "srtp" } else if(media_encryption == "srtpm") { mediaEnc = "srtp-mand" } if (mediaEnc != acc.mediaEnc) { if (Api.account_set_mediaenc(acc.accp, mediaEnc) == 0) { acc.mediaEnc = Api.account_mediaenc(acc.accp) println("[RITO] New mediaenc is ${acc.mediaEnc}") } else { Log.e(TAG, "Setting of mediaenc $mediaEnc failed") } } } /* Api.config_verify_server_set(false) Api.config_transport_server_set("tls") Api.uag_reset_transp(true, true) */ Api.account_set_regint(acc.accp, REGISTRATION_INTERVAL) acc.regint = REGISTRATION_INTERVAL //UserAgent.apply { ua } println("[RITO] set_account : ${ua.account.print()}") BaresipService.uas[0] = ua //Api.ua_register(ua.uap) AccountsActivity.generateAccounts() AccountsActivity.saveAccounts() //UserAgent.register() } } catch(e : Exception) { e.printStackTrace() } } } else if(req == "layout_setting") { val param = intent?.getStringExtra("param") if(param != null) { val json = JSONObject(param) val layout = json.getString("layout") if(layout == "layout1") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_우하단 } else if(layout == "layout2") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_우상단 } else if(layout == "layout3") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_좌상단 } else if(layout == "layout4") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_좌하단 } else if(layout == "layout5") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_대상단_근거리_소하단 } else if(layout == "layout6") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_대좌단_근거리_소우단 } else if(layout == "layout7") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_대하단_근거리_소상단 } else if(layout == "layout8") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_반좌단_근거리_반우단 } sendActivityAction("update layout") } } else if(req == "misc_setting") { val param = intent?.getStringExtra("param") if(param != null) { val json = JSONObject(param) val device_name = json.getString("device_name") Config.replaceVariable("device_name", device_name) BaresipService.deviceName = device_name Config.save() sendActivityAction("update info") } } } } } this.registerReceiver(sipReqeustReceiver, IntentFilter("kr.co.rito.ritosip.REQUEST")) wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager hotSpotIsEnabled = Utils.isHotSpotOn(wm) hotSpotReceiver = object : BroadcastReceiver() { override fun onReceive(contxt: Context, intent: Intent) { val action = intent.action if ("android.net.wifi.WIFI_AP_STATE_CHANGED" == action) { val state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0) if (WifiManager.WIFI_STATE_ENABLED == state % 10) { if (hotSpotIsEnabled) { Log.d(TAG, "HotSpot is still enabled") } else { Log.d(TAG, "HotSpot is enabled") hotSpotIsEnabled = true Timer().schedule(1000) { hotSpotAddresses = Utils.hotSpotAddresses() Log.d(TAG, "HotSpot addresses $hotSpotAddresses") if (hotSpotAddresses.isNotEmpty()) { var reset = false for ((k, v) in hotSpotAddresses) if (afMatch(k)) if (Api.net_add_address_ifname(k, v) != 0) Log.e(TAG, "Failed to add $v address $k") else reset = true if (reset) Timer().schedule(2000) { updateNetwork() } } else { Log.w(TAG, "Could not get hotspot addresses") } } } } else { if (!hotSpotIsEnabled) { Log.d(TAG, "HotSpot is still disabled") } else { Log.d(TAG, "HotSpot is disabled") hotSpotIsEnabled = false if (hotSpotAddresses.isNotEmpty()) { for ((k, _) in hotSpotAddresses) if (Api.net_rm_address(k) != 0) Log.e(TAG, "Failed to remove address $k") hotSpotAddresses = mapOf() updateNetwork() } } } } } } this.registerReceiver(hotSpotReceiver, IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED")) tm = getSystemService(Context.TELECOM_SERVICE) as TelecomManager btm = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager btAdapter = btm.adapter proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "com.tutpro.baresip.plus:proximity_wakelock") wifiLock = if (VERSION.SDK_INT < 29) @Suppress("DEPRECATION") wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip+") else wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Baresip+") wifiLock.setReferenceCounted(false) bluetoothReceiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { when (intent.action) { BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED -> { val state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED) when (state) { BluetoothHeadset.STATE_CONNECTED -> { Log.d(TAG, "Bluetooth headset is connected") if (audioFocusRequest != null) startBluetoothSco(applicationContext, 1000L, 3) } BluetoothHeadset.STATE_DISCONNECTED -> { Log.d(TAG, "Bluetooth headset is disconnected") if (audioFocusRequest != null) stopBluetoothSco(applicationContext) } } } BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED -> { val state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED) when (state) { BluetoothHeadset.STATE_AUDIO_CONNECTED -> { Log.d(TAG, "Bluetooth headset audio is connected") } BluetoothHeadset.STATE_AUDIO_DISCONNECTED -> { Log.d(TAG, "Bluetooth headset audio is disconnected") } } } AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> { val state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_DISCONNECTED) when (state) { AudioManager.SCO_AUDIO_STATE_CONNECTING -> { Log.d(TAG, "Bluetooth headset SCO is connecting") } AudioManager.SCO_AUDIO_STATE_CONNECTED -> { Log.d(TAG, "Bluetooth headset SCO is connected") } AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> { Log.d(TAG, "Bluetooth headset SCO is disconnected") resetCallVolume() } AudioManager.SCO_AUDIO_STATE_ERROR -> { Log.d(TAG, "Bluetooth headset SCO state ERROR") } } } } } } if (btAdapter != null) { val filter = IntentFilter() filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED) filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) this.registerReceiver(bluetoothReceiver, filter) } androidContactsObserver = object : ContentObserver(null) { override fun onChange(self: Boolean) { Log.d(TAG, "Android contacts change") if (contactsMode != "baresip") { Contact.loadAndroidContacts(this@BaresipService.applicationContext) Contact.contactsUpdate() } } } stopState = "initial" quitTimer = object : CountDownTimer(5000, 1000) { override fun onTick(millisUntilFinished: Long) { Log.d(TAG, "Seconds remaining: ${millisUntilFinished / 1000}") } override fun onFinish() { when (stopState) { "initial" -> { if (isServiceRunning) baresipStop(true) stopState = "force" quitTimer.start() } "force" -> { cleanService() isServiceRunning = false postServiceEvent(ServiceEvent("stopped", arrayListOf(""), System.nanoTime())) stopSelf() // exitProcess(0) } } } } val scope = CoroutineScope(Dispatchers.Default) var captureCount = 0 var running = true val job = scope.launch { println("camera capture manager is running...") Utils.deleteFiles("/mnt/obb", "jpg") var updateDate = LocalDateTime.now() while (running) { if(File("/mnt/obb/camera_near.rgb565.done").exists() && !File("/mnt/obb/camera_near.jpg").exists()) { val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera_near.rgb565") Utils.saveRGB565AsJPG( applicationContext, nearBuf, 1280, 720, "/mnt/obb/camera_near.jpg" ) } if(File("/mnt/obb/camera_far.rgb565.done").exists() && !File("/mnt/obb/camera_far.jpg").exists()) { val farBuf = Utils.readFileToByteBuffer("/mnt/obb/camera_far.rgb565") Utils.saveRGB565AsJPG( applicationContext, farBuf, 1280, 720, "/mnt/obb/camera_far.jpg" ) } //delay(800) // 1초마다 실행 for(i in 1..8) { var callExist = false if(uas.size > 0) { val ua = uas[0] val call = ua.currentCall() var sipaddr = ua.account.aor sipaddr = sipaddr.removePrefix("sip:") Utils.propertySet("sys.ritosip.sip.address", sipaddr) if(ua.status == R.drawable.circle_yellow) { Utils.propertySet("sys.ritosip.sip.status", "registering") } else if(ua.status == R.drawable.circle_green) { Utils.propertySet("sys.ritosip.sip.status", "registered") } else if(ua.status == R.drawable.circle_red) { Utils.propertySet("sys.ritosip.sip.status", "registering failed") } else if(ua.status == R.drawable.circle_white) { Utils.propertySet("sys.ritosip.sip.status", "unregistered") } if (call != null) { callExist = true var audioMute = "unmute" if(isMicMuted) audioMute = "mute" Utils.propertySet("sys.ritosip.call.mute.audio", audioMute) Utils.propertySet("sys.ritosip.call.status", "calling") Utils.propertySet("sys.ritosip.call.number", call.peerUri) var stats = call.stats("audio") if (stats != "") { //println("stats audio = ${stats}") val parts = stats.split(",") as ArrayList if (parts[2] == "0/0") { parts[2] = "?/?" parts[3] = "?/?" parts[4] = "?/?" } Utils.propertySet("sys.ritosip.call.audio.current_bitrate", parts[0]) Utils.propertySet("sys.ritosip.call.audio.average_bitrate", parts[1]) Utils.propertySet("sys.ritosip.call.audio.packets", parts[2]) Utils.propertySet("sys.ritosip.call.audio.lost", parts[3]) Utils.propertySet("sys.ritosip.call.audio.jitter", parts[4]) } val duration = call.duration() Utils.propertySet("sys.ritosip.call.duration", duration.toString()) val codecs = call.audioCodecs() if(codecs.length >= 2) { val txCodec = codecs.split(',')[0].split("/") val rxCodec = codecs.split(',')[1].split("/") Utils.propertySet("sys.ritosip.call.audio.txCodec", txCodec[0]) Utils.propertySet("sys.ritosip.call.audio.rxCodec", rxCodec[0]) } stats = call.stats("video") if (stats != "") { //println("stats video = ${stats}") val parts = stats.split(",") Utils.propertySet("sys.ritosip.call.video.current_bitrate", parts[0]) Utils.propertySet("sys.ritosip.call.video.average_bitrate", parts[1]) Utils.propertySet("sys.ritosip.call.video.packets", parts[2]) Utils.propertySet("sys.ritosip.call.video.lost", parts[3]) Utils.propertySet("sys.ritosip.call.video.jitter", parts[4]) val codecs = call.videoCodecs().split(',') if(codecs.size >= 2) { val txCodec = codecs[0] val rxCodec = codecs[1] Utils.propertySet("sys.ritosip.call.video.txCodec", txCodec) Utils.propertySet("sys.ritosip.call.video.rxCodec", rxCodec) } if(MainActivity.videoView != null) { Utils.propertySet( "sys.ritosip.call.video.format", "" + MainActivity.videoView.get_frameFormat() ) } } } else { Utils.propertySet("sys.ritosip.call.status", "standby") Utils.propertySet("sys.ritosip.call.number", "") Utils.propertySet("sys.ritosip.call.duration", "") Utils.propertySet("sys.ritosip.call.audio.txCodec", "") Utils.propertySet("sys.ritosip.call.audio.rxCodec", "") Utils.propertySet("sys.ritosip.call.video.txCodec", "") Utils.propertySet("sys.ritosip.call.video.rxCodec", "") } } else { Utils.propertySet("sys.ritosip.sip.address", "") Utils.propertySet("sys.ritosip.sip.status", "unregistered") } val display1 = Utils.checkDisplayConnection(0) val display2 = Utils.checkDisplayConnection(1) var connectedCount = 0 if(display1 == "connected") connectedCount++ if(display2 == "connected") connectedCount++ if(connectedDisplayCount != connectedCount) { val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager val displays = displayManager.displays if(connectedCount == displays.size) { connectedDisplayCount = connectedCount sendActivityAction("update display") } } //connectedDisplayCount = connectedCount Utils.propertySet("sys.ritosip.display1.status", display1) Utils.propertySet("sys.ritosip.display2.status", display2) val camera1 = Utils.checkCameraConnection(0) val camera2 = Utils.checkCameraConnection(1) Utils.propertySet("sys.ritosip.camera1.status", camera1) Utils.propertySet("sys.ritosip.camera2.status", camera2) delay(100) // 1초마다 실행 val now = LocalDateTime.now() if(now.minute != updateDate.minute) { sendActivityAction("update info") updateDate = now } } var nearFilePath = "/mnt/obb/near_$captureCount.jpg" if(!Utils.copyFile("/mnt/obb/camera_near.jpg", nearFilePath)) { nearFilePath = "" } var farFilePath = "/mnt/obb/far_$captureCount.jpg" if(!Utils.copyFile("/mnt/obb/camera_far.jpg", farFilePath)) { farFilePath = "" } Utils.deleteFile(File("/mnt/obb/camera_near.rgb565")) Utils.deleteFile(File("/mnt/obb/camera_near.rgb565.done")) Utils.deleteFile(File("/mnt/obb/camera_near.jpg")) Utils.deleteFile(File("/mnt/obb/camera_far.rgb565")) Utils.deleteFile(File("/mnt/obb/camera_far.rgb565.done")) Utils.deleteFile(File("/mnt/obb/camera_far.jpg")) for(i in captureCount - 10 .. captureCount - 5) { var delFilePath = "/mnt/obb/near_$i.jpg" Utils.deleteFile(File(delFilePath)) delFilePath = "/mnt/obb/far_$i.jpg" Utils.deleteFile(File(delFilePath)) } if(uas.size > 0) { val ua = uas[0] val call = ua.currentCall() if (call != null) { if (nearFilePath.length > 0) Utils.propertySet("sys.ritosip.near_screen_file", nearFilePath) if (farFilePath.length > 0) Utils.propertySet("sys.ritosip.far_screen_file", farFilePath) } else { Utils.propertySet("sys.ritosip.near_screen_file", "") Utils.propertySet("sys.ritosip.far_screen_file", "") } } captureCount++ delay(100) /* delay(800) // 1초마다 실행 File("/mnt/obb/camera_near.rgb565").delete() File("/mnt/obb/camera_near.rgb565.done").delete() File("/mnt/obb/camera_near.jpg").delete() File("/mnt/obb/camera_far.rgb565").delete() File("/mnt/obb/camera_far.rgb565.done").delete() File("/mnt/obb/camera_far.jpg").delete() delay(200) // 1초마다 실행 */ } } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val action: String if (intent == null) { action = "Start" Log.d(TAG, "Received onStartCommand with null intent") } else { // Utils.dumpIntent(intent) action = intent.action!! Log.d(TAG, "Received onStartCommand action $action") } when (action) { "Start" -> { isStartReceived = true if (VERSION.SDK_INT < 31) { @Suppress("DEPRECATION") allNetworks = cm.allNetworks.toMutableSet() } updateDnsServers() val assets = arrayOf("accounts", "config", "contacts") var file = File(filesPath) if (!file.exists()) { Log.i(TAG, "Creating baresip directory") try { File(filesPath).mkdirs() } catch (e: Error) { Log.e(TAG, "Failed to create directory: $e") } } for (a in assets) { file = File("${filesPath}/$a") if (!file.exists() && a != "config") { Log.i(TAG, "Copying asset '$a'") Utils.copyAssetToFile(applicationContext, a, "$filesPath/$a") } else { Log.d(TAG, "Asset '$a' already copied") } if (a == "config") Config.initialize(applicationContext) } if (!File(filesDir, "tmp").exists()) File(filesDir, "tmp").mkdir() if (!File(filesDir, "recordings").exists()) File(filesDir, "recordings").mkdir() if (contactsMode != "android") Contact.restoreBaresipContacts() if (contactsMode != "baresip") { Contact.loadAndroidContacts(applicationContext) registerAndroidContactsObserver() } Contact.contactsUpdate() val history = CallHistory.get() if (history.isEmpty()) { CallHistoryNew.restore() } else { for (old in history) { val new = CallHistoryNew(old.aor, old.peerUri, old.direction) new.stopTime = old.stopTime new.startTime = old.startTime new.recording = old.recording callHistory.add(new) } CallHistoryNew.save() } Message.restore() hotSpotAddresses = Utils.hotSpotAddresses() linkAddresses = linkAddresses() var addresses = "" for (la in linkAddresses) addresses = "$addresses;${la.key};${la.value}" Log.i(TAG, "Link addresses: $addresses") activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") Log.d(TAG, "AEC/AGC/NS available = " + "$aecAvailable/$agcAvailable/$nsAvailable") val userAgent = Config.variable("user_agent") Thread { baresipStart( filesPath, addresses.removePrefix(";"), logLevel, if (userAgent != "") userAgent else "ritosip v${BuildConfig.VERSION_NAME} " + "(Android ${VERSION.RELEASE}/${System.getProperty("os.arch") ?: "?"})" ) }.start() isServiceRunning = true showStatusNotification() sendActivityAction("update info") val accounts = Utils.getFileContents("$filesPath/accounts") if ((accounts != null) && accounts.isNotEmpty()) { Utils.putFileContents("$filesPath/accounts", accounts.toString(Charsets.UTF_8).replace( "pubint=0;call_transfer", "pubint=0;inreq_allowed=yes;call_transfer" ).toByteArray(Charsets.UTF_8) ) } if (linkAddresses.isEmpty()) { val newIntent = Intent(this, MainActivity::class.java) newIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK newIntent.putExtra("action", "no network") startActivity(newIntent) } applyTransportConfiguration() } "Start Content Observer" -> { registerAndroidContactsObserver() } "Stop Content Observer" -> { unRegisterAndroidContactsObserver() } "Call Answer" -> { val uap = intent!!.getLongExtra("uap", 0L) val callp = intent.getLongExtra("callp", 0L) stopRinging() stopMediaPlayer() setCallVolume() proximitySensing(true) Api.ua_answer(uap, callp, Api.VIDMODE_ON) } "Call Reject" -> { val callp = intent!!.getLongExtra("callp", 0L) val call = Call.ofCallp(callp) if (call == null) { Log.w(TAG, "onStartCommand did not find call $callp") } else { val peerUri = call.peerUri val aor = call.ua.account.aor Log.d(TAG, "Aor $aor rejected incoming call $callp from $peerUri") call.rejected = true Api.ua_hangup(call.ua.uap, callp, 486, "Rejected") } } "Transfer Deny" -> { val callp = intent!!.getLongExtra("callp", 0L) val call = Call.ofCallp(callp) if (call == null) Log.w(TAG, "onStartCommand did not find call $callp") else call.notifySipfrag(603, "Decline") nm.cancel(TRANSFER_NOTIFICATION_ID) } "Message Save" -> { val uap = intent!!.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) Log.w(TAG, "onStartCommand did not find UA $uap") else ChatsActivity.saveUaMessage( ua.account.aor, intent.getStringExtra("time")!!.toLong() ) nm.cancel(MESSAGE_NOTIFICATION_ID) } "Message Delete" -> { val uap = intent!!.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) Log.w(TAG, "onStartCommand did not find UA $uap") else ChatsActivity.deleteUaMessage( ua.account.aor, intent.getStringExtra("time")!!.toLong() ) nm.cancel(MESSAGE_NOTIFICATION_ID) } "Update Notification" -> { updateStatusNotification() } "Stop" -> { cleanService() if (isServiceRunning) { baresipStop(false) quitTimer.start() } } else -> { Log.e(TAG, "Unknown start action $action") } } return START_STICKY } override fun onBind(intent: Intent): IBinder? { return null } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy at Baresip Service") cleanService() if (isServiceRunning) sendBroadcast(Intent("com.tutpro.baresip.plus.Restart")) } fun applyTransportConfiguration() { Api.config_verify_server_set(false) if(BaresipService.preferTransport == "udp") { Api.config_transport_server_set("udp") } else if(BaresipService.preferTransport == "tcp") { Api.config_transport_server_set("tcp") } else if(BaresipService.preferTransport == "tls") { Api.config_transport_server_set("tls") } Api.uag_reset_transp(true, true) } fun sendActivityAction(order : String) { val newIntent = Intent() newIntent.setClassName( "kr.co.rito.ritosip", "com.tutpro.baresip.plus.MainActivity" ) newIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK newIntent.putExtra("action", order) startActivity(newIntent) } @SuppressLint("DiscouragedApi") @Keep fun uaEvent(event: String, uap: Long, callp: Long) { if (!isServiceRunning) return val ev = event.split(",") if (ev[0] == "create") { val ua = UserAgent(uap) ua.status = if (ua.account.regint == 0) R.drawable.circle_white else R.drawable.circle_yellow uas.add(ua) val acc = ua.account if (acc.authUser != "" && acc.authPass == NO_AUTH_PASS) { val password = aorPasswords[acc.aor] if (password != null) { Api.account_set_auth_pass(acc.accp, password) acc.authPass = password } else { Api.account_set_auth_pass(acc.accp, NO_AUTH_PASS) } } Log.d(TAG, "got uaEvent $event/${acc.aor}") return } if (ev[0] == "sndfile dump") { Log.d(TAG, "Got sndfile dump ${ev[1]}") if (Call.inCall()) { if (ev[1].endsWith("enc.wav")) Call.calls()[0].dumpfiles[0] = ev[1] else Call.calls()[0].dumpfiles[1] = ev[1] } return } if (ev[0] == "player sessionid") { playerSessionId = ev[1].toInt() Log.d(TAG, "got player sessionid $playerSessionId") return } if (ev[0] == "recorder sessionid") { recorderSessionId = ev[1].toInt() Log.d(TAG, "got recorder sessionid $recorderSessionId") return } val ua = UserAgent.ofUap(uap) val aor = ua?.account?.aor Log.d(TAG, "got uaEvent $event/$aor/$callp") if (ua == null) { when (ev[0]) { "snapshot" -> { val file = File(ev[2]) if (file.length() > 0) { val fileName = ev[2].split("/").last() try { val bitmap = BitmapFactory.decodeStream(file.inputStream()) Utils.savePicture(this, bitmap, fileName) Log.d(TAG, "Saved snapshot $fileName") } catch (e: IOException) { Log.d(TAG, "Failed to save snapshot $fileName") } } file.delete() } else -> { Log.w(TAG, "uaEvent did not find ua $uap") } } return } val call = Call.ofCallp(callp) if (call == null && callp != 0L && !setOf("incoming call", "call incoming", "call closed").contains(ev[0])) { Log.w(TAG, "uaEvent $event did not find call $callp") return } var newEvent: String? = null for (accountIndex in uas.indices) { if (uas[accountIndex].account.aor == aor) { when (ev[0]) { "registering", "unregistering" -> { ua.status = R.drawable.circle_yellow updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) return } "registered" -> { ua.status = if (Api.account_regint(ua.account.accp) == 0) R.drawable.circle_white else R.drawable.circle_green updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) return } "registering failed" -> { ua.status = if (Api.account_regint(ua.account.accp) == 0) R.drawable.circle_white else R.drawable.circle_red updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) if (Utils.isVisible()) { val reason = if (ev.size > 1) { if (ev[1] == "Invalid argument") // Likely due to DNS lookup failure ": DNS lookup failed" else ": ${ev[1]}" } else "" toast(String.format(getString(R.string.registering_failed), aor) + reason) } return } "call outgoing" -> { if (call!!.status == "transferring") break stopMediaPlayer() setCallVolume() if (speakerPhone && !Utils.isSpeakerPhoneOn(am)) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am) proximitySensing(true) return } "call ringing" -> { playRingBack() return } "call progress" -> { if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() else playRingBack() return } "incoming call" -> { val peerUri = ev[1] val bevent = ev[2].toLong() val toastMsg = if (!Utils.checkPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO))) getString(R.string.no_calls) else if (!requestAudioFocus(applicationContext)) // request fails if there is an active telephony call getString(R.string.audio_focus_denied) else if (Call.inCall()) String.format(getString(R.string.call_auto_rejected), Utils.friendlyUri(this, peerUri, ua.account)) else "" if (toastMsg != "") { Log.d(TAG, "Auto-rejecting incoming call $uap/$peerUri") Api.sip_treply(callp, 486, "Busy Here") Api.bevent_stop(bevent) toast(toastMsg) val name = "callwaiting_$toneCountry" val resourceId = applicationContext.resources.getIdentifier( name, "raw", applicationContext.packageName) if (resourceId != 0) { playUnInterrupted(resourceId, 1) } else { Log.e(TAG, "Callwaiting tone $name.wav not found\")") } if (ua.account.callHistory) { CallHistoryNew.add(CallHistoryNew(aor, peerUri, "in")) CallHistoryNew.save() ua.account.missedCalls = true } return } // callp holds SIP message pointer Api.ua_accept(uap, callp) return } "call incoming" -> { val peerUri = ev[1] Log.d(TAG, "Incoming call $uap/$callp/$peerUri") Call(callp, ua, peerUri, "in", "incoming", Utils.dtmfWatcher(callp)).add() if (speakerPhone && !Utils.isSpeakerPhoneOn(am)) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am) if (ua.account.answerMode == Api.ANSWERMODE_MANUAL) { Log.d(TAG, "CurrentInterruptionFilter ${nm.currentInterruptionFilter}") if (nm.currentInterruptionFilter <= NotificationManager.INTERRUPTION_FILTER_ALL) startRinging() } else { val newIntent = Intent(this, MainActivity::class.java) newIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK newIntent.putExtra("action", "call answer") newIntent.putExtra("callp", callp) startActivity(newIntent) return } if (!Utils.isVisible()) { val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "call show") .putExtra("callp", callp) val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val nb = NotificationCompat.Builder(this, if (shouldVibrate()) MEDIUM_CHANNEL_ID else HIGH_CHANNEL_ID) val caller = Utils.friendlyUri(this, peerUri, ua.account) nb.setSmallIcon(R.drawable.ic_stat_call) .setColor(ContextCompat.getColor(this, R.color.colorBaresip)) .setContentIntent(pi) .setCategory(Notification.CATEGORY_CALL) .setAutoCancel(false) .setOngoing(true) .setContentTitle(getString(R.string.incoming_call_from)) .setContentText(caller) .setWhen(System.currentTimeMillis()) .setShowWhen(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_HIGH) .setFullScreenIntent(pi, true) val answerIntent = Intent(applicationContext, MainActivity::class.java) answerIntent.putExtra("action", "call answer") .putExtra("callp", callp) val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE, answerIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val rejectIntent = Intent(this, BaresipService::class.java) rejectIntent.action = "Call Reject" rejectIntent.putExtra("callp", callp) val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) nb.addAction(R.drawable.ic_stat_call, getActionText(R.string.answer, R.color.colorGreen), api) nb.addAction(R.drawable.ic_stat_call_end, getActionText(R.string.reject, R.color.colorRed), rpi) nm.notify(CALL_NOTIFICATION_ID, nb.build()) return } } "remote call answered" -> { newEvent = "call update" } "remote call offered" -> { val callHasVideo = ev[1] == "1" val remoteHasVideo = ev[2] == "1" val ldir = ev[3].toInt() val rdir = if (Utils.isCameraAvailable(this)) ev[4].toInt() else ev[4].toInt() and Api.SDP_RECVONLY when (ev[5]) { "0", "1" -> call!!.held = true // inactive, recvonly "2", "3" -> call!!.held = false // sendonly, sendrecv } if (!isMainVisible || call!!.status != "connected") return if (!(callHasVideo && remoteHasVideo && ldir == 0) && (!callHasVideo && remoteHasVideo && (rdir != Api.SDP_INACTIVE) && (ldir != rdir))) { postServiceEvent(ServiceEvent("call video request", arrayListOf(uap, callp, rdir), System.nanoTime())) return } newEvent = "call update" } "call answered" -> { stopMediaPlayer() if (call!!.status == "incoming") call.status = "answered" else return } "call redirect", "video call redirect"-> { stopMediaPlayer() } "call established" -> { nm.cancel(CALL_NOTIFICATION_ID) Log.d(TAG, "AoR $aor call $callp established in mode ${am.mode}") if (am.mode != MODE_IN_COMMUNICATION) am.mode = MODE_IN_COMMUNICATION call!!.status = "connected" if (recorderSessionId != 0) { if (aecAvailable && !webrtcAec) { aec = AcousticEchoCanceler.create(recorderSessionId) if (aec != null) { if (!aec!!.getEnabled()) { aec!!.setEnabled(true) if (aec!!.getEnabled()) Log.d(TAG, "AEC is enabled") else Log.w(TAG, "Failed to enable AEC") } } else Log.w(TAG, "Failed to create AEC") } if (agcAvailable) { agc = AutomaticGainControl.create(recorderSessionId) if (agc != null) { if (!agc!!.getEnabled()) { agc!!.setEnabled(true) if (agc!!.getEnabled()) Log.d(TAG, "AGC is enabled") } } else Log.w(TAG, "Failed to create AGC") } if (nsAvailable) { ns = NoiseSuppressor.create(recorderSessionId) if (ns != null) { if (!ns!!.getEnabled()) { ns!!.setEnabled(true) if (ns!!.getEnabled()) Log.d(TAG, "ns is enabled") } } else Log.w(TAG, "Failed to create NS") } recorderSessionId = 0 } call.onhold = false if (ua.account.callHistory) call.startTime = GregorianCalendar() if (!isMainVisible) return } "call update" -> { call!!.held = when (ev[1].toInt()) { Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true else /* Api.SDP_SENDONLY, Api.SDP_SENDRECV */ -> false } if (call.state() == Api.CALL_STATE_EARLY) { if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() else playRingBack() } if (!isMainVisible || call.status != "connected") return } "call verified", "call secure" -> { if (ev[0] == "call secure") { call!!.security = R.drawable.locked_yellow } else { call!!.security = R.drawable.locked_green call.zid = ev[1] } if (!isMainVisible) return } "call transfer" -> { if (!Utils.isVisible()) { val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "transfer show") .putExtra("callp", callp).putExtra("uri", ev[1]) val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) val target = Utils.friendlyUri(this, ev[1], ua.account) nb.setSmallIcon(R.drawable.ic_stat_call) .setColor(ContextCompat.getColor(this, R.color.colorBaresip)) .setContentIntent(pi) .setDefaults(Notification.DEFAULT_SOUND) .setAutoCancel(true) .setContentTitle(getString(R.string.transfer_request_to)) .setContentText(target) val acceptIntent = Intent(applicationContext, MainActivity::class.java) acceptIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK acceptIntent.putExtra("action","transfer accept") .putExtra("callp", callp).putExtra("uri", ev[1]) val api = PendingIntent.getActivity(applicationContext, ACCEPT_REQ_CODE, acceptIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val denyIntent = Intent(this, BaresipService::class.java) denyIntent.action = "Transfer Deny" denyIntent.putExtra("callp", callp) val dpi = PendingIntent.getService(this, DENY_REQ_CODE, denyIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) nb.addAction(R.drawable.ic_stat_call, getString(R.string.accept), api) nb.addAction(R.drawable.ic_stat_call_end, getString(R.string.deny), dpi) nm.notify(TRANSFER_NOTIFICATION_ID, nb.build()) return } } "transfer failed" -> { Log.d(TAG, "AoR $aor call $callp transfer failed: ${ev[1]}") stopMediaPlayer() call!!.referTo = "" if (Utils.isVisible()) toast("${getString(R.string.transfer_failed)}: ${ev[1].trim()}") if (!isMainVisible) return } "call closed" -> { Log.d(TAG, "AoR $aor call $callp is closed") if (call != null) { nm.cancel(CALL_NOTIFICATION_ID) stopRinging() stopMediaPlayer() aec?.release() agc?.release() ns?.release() val newCall = call.newCall if (newCall != null) { newCall.onHoldCall = null call.newCall = null } val onHoldCall = call.onHoldCall if (onHoldCall != null) { onHoldCall.newCall = null onHoldCall.referTo = "" call.onHoldCall = null } call.remove() val reason = ev[1] val tone = ev[2] if (tone == "busy") { playBusy() } else if (!Call.inCall()) { resetCallVolume() abandonAudioFocus(applicationContext) proximitySensing(false) } if (call.dir == "out") call.rejected = call.startTime == null && !reason.startsWith("408") && !reason.startsWith("480") && !reason.startsWith("Connection reset by") val missed = call.dir == "in" && call.startTime == null && !call.rejected if (ua.account.callHistory) { val history = CallHistoryNew(aor, call.peerUri, call.dir) history.startTime = call.startTime history.stopTime = GregorianCalendar() history.rejected = call.rejected if (call.startTime != null && call.dumpfiles[0] != "") history.recording = call.dumpfiles CallHistoryNew.add(history) CallHistoryNew.save() ua.account.missedCalls = ua.account.missedCalls || missed } if (!Utils.isVisible()) { if (missed) { val caller = Utils.friendlyUri(this, call.peerUri, ua.account) val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "call missed") .putExtra("uap", uap) val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) nb.setSmallIcon(R.drawable.ic_stat_phone_missed) .setColor(ContextCompat.getColor(this, R.color.colorBaresip)) .setContentIntent(pi) .setCategory(Notification.CATEGORY_CALL) .setAutoCancel(true) var missedCalls = 0 for (notification in nm.activeNotifications) if (notification.id == CALL_MISSED_NOTIFICATION_ID) missedCalls++ if (missedCalls == 0) { nb.setContentTitle(getString(R.string.missed_call_from)) nb.setContentText(caller) } else { nb.setContentTitle(getString(R.string.missed_calls)) nb.setContentText( String.format(getString(R.string.missed_calls_count), missedCalls + 1)) } nm.notify(CALL_MISSED_NOTIFICATION_ID, nb.build()) } return } } val reason = ev[1].trim() if ((reason != "") && (ua.calls().isEmpty())) { if (reason[0].isDigit()) { if (reason[0] != '3') { toast("${getString(R.string.call_failed)}: $reason") Utils.propertySet("sys.ritosip.call.event.result", "call_failed") // Utils.propertySet("sys.ritosip.call.event.time", (Clock.systemUTC().millis() / 1000).toString()) // val responseIntent = Intent("kr.co.rito.ritosip.RESPONSE") // responseIntent.putExtra("req", "call_result") // responseIntent.putExtra("data", "failed") // responseIntent.setPackage("kr.co.rito.sipsvc") // sendBroadcast(responseIntent) } } else { //toast("${getString(R.string.call_closed)}: ${Api.call_peer_uri(callp)}: $reason") toast("${getString(R.string.call_closed)}") } } } } } } postServiceEvent(ServiceEvent(newEvent ?: event, arrayListOf(uap, callp), System.nanoTime())) } private fun postServiceEvent(event: ServiceEvent) { serviceEvents.add(event) if (serviceEvents.size == 1) { Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}") serviceEvent.postValue(Event(event.timeStamp)) } else { Log.d(TAG, "Added service event ${event.event}") } } @Keep fun messageEvent(uap: Long, peerUri: String, cType: String, msg: ByteArray) { val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "messageEvent did not find ua $uap") return } val charsetString = Utils.paramValue(cType.replace(" ", ""), "charset") val charset = try { Charset.forName(charsetString) } catch (e: Exception) { StandardCharsets.UTF_8 } val text = try { String(msg, charset) } catch (e: Exception) { val error = "Decoding of message failed using charset $charset from $cType!" Log.w(TAG, error) error } val timeStamp = System.currentTimeMillis() val timeStampString = timeStamp.toString() Log.d(TAG, "Message event for $uap from $peerUri at $timeStampString") Message(ua.account.aor, peerUri, text, timeStamp, MESSAGE_DOWN, 0, "", true).add() Message.save() ua.account.unreadMessages = true if (!Utils.isVisible()) { val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "message show").putExtra("uap", uap) .putExtra("peer", peerUri) val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) val sender = Utils.friendlyUri(this, peerUri, ua.account) nb.setSmallIcon(R.drawable.ic_stat_message) .setColor(ContextCompat.getColor(this, R.color.colorBaresip)) .setContentIntent(pi) .setSound(Settings.System.DEFAULT_NOTIFICATION_URI) .setAutoCancel(true) .setContentTitle(getString(R.string.message_from) + " " + sender) .setContentText(text) val replyIntent = Intent(applicationContext, MainActivity::class.java) replyIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK replyIntent.putExtra("action", "message reply") .putExtra("uap", uap).putExtra("peer", peerUri) val rpi = PendingIntent.getActivity(applicationContext, REPLY_REQ_CODE, replyIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val saveIntent = Intent(this, BaresipService::class.java) saveIntent.action = "Message Save" saveIntent.putExtra("uap", uap) .putExtra("time", timeStampString) val spi = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) val deleteIntent = Intent(this, BaresipService::class.java) deleteIntent.action = "Message Delete" deleteIntent.putExtra("uap", uap) .putExtra("time", timeStampString) val dpi = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) nb.addAction(R.drawable.ic_stat_reply, "Reply", rpi) nb.addAction(R.drawable.ic_stat_save, "Save", spi) nb.addAction(R.drawable.ic_stat_delete, "Delete", dpi) nm.notify(MESSAGE_NOTIFICATION_ID, nb.build()) return } if (nm.currentInterruptionFilter <= NotificationManager.INTERRUPTION_FILTER_ALL) nt.play() postServiceEvent(ServiceEvent("message show", arrayListOf(uap, peerUri), System.nanoTime())) } @Keep fun started() { Log.d(TAG, "Received 'started' from baresip") Api.net_debug() postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) callActionUri = "" Log.d(TAG, "Battery optimizations are ignored: " + "${pm.isIgnoringBatteryOptimizations(packageName)}") Log.d(TAG, "Partial wake lock/wifi lock is held: " + "${partialWakeLock.isHeld}/${wifiLock.isHeld}") updateStatusNotification() } @Keep @Suppress("UNUSED") fun messageResponse(responseCode: Int, responseReason: String, time: String) { Log.d(TAG, "Message response '$responseCode $responseReason' at $time") val timeStamp = time.toLong() for (m in messages.reversed()) if (m.timeStamp == timeStamp) { if (responseCode < 300) { m.direction = MESSAGE_UP } else { m.direction = MESSAGE_UP_FAIL m.responseCode = responseCode m.responseReason = responseReason } messageUpdate.postValue(System.currentTimeMillis()) break } else { if (m.timeStamp < timeStamp - 60000) break } } /* @Keep fun checkVideo(uap: String, callp: String, reqDir: Int): Int { if (!isServiceRunning) return -1 val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(LOG_TAG, "checkVideo did not find ua $uap") return -1 } val call = Call.ofCallp(callp) if (call == null) { Log.d(LOG_TAG, "checkVideo did not find call $callp") return -1 } Log.d(LOG_TAG, "checkVideo reqDir = $reqDir, videoAllowed = ${call.videoAllowed}," + " call.video = ${call.video}") if ((reqDir == Api.SDP_INACTIVE) || (!cameraAvailable && (reqDir == Api.SDP_SENDONLY))) { call.video = Api.SDP_INACTIVE return Api.SDP_INACTIVE } if ((call.video == Api.SDP_INACTIVE) && !call.videoAllowed) { serviceEvent.postValue(Event(ServiceEvent("call video request", arrayListOf(uap, peer), timeStamp))) return Api.SDP_INACTIVE } call.videoAllowed = false if (cameraAvailable) call.video = reqDir else call.video = Api.SDP_RECVONLY return call.video }*/ @Keep fun stopped(error: String) { Log.d(TAG, "Received 'stopped' from baresip with start error '$error'") quitTimer.cancel() cleanService() isServiceRunning = false postServiceEvent(ServiceEvent("stopped", arrayListOf(error), System.nanoTime())) stopSelf() } private fun createNotificationChannels() { val defaultChannel = NotificationChannel(DEFAULT_CHANNEL_ID, "Default", NotificationManager.IMPORTANCE_LOW) defaultChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC nm.createNotificationChannel(defaultChannel) val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "High", NotificationManager.IMPORTANCE_HIGH) highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC highChannel.enableVibration(true) nm.createNotificationChannel(highChannel) val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Medium", NotificationManager.IMPORTANCE_HIGH) highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC highChannel.enableVibration(false) nm.createNotificationChannel(mediumChannel) } private fun showStatusNotification() { val intent = Intent(applicationContext, MainActivity::class.java) .setAction(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) val pi = PendingIntent.getActivity(applicationContext, STATUS_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE) val notificationLayout = RemoteViews(packageName, R.layout.status_notification) snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat) .setContentIntent(pi) .setOngoing(true) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(notificationLayout) if (VERSION.SDK_INT >= 31) snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_DEFAULT try { if (VERSION.SDK_INT >= 29) startForeground( STATUS_NOTIFICATION_ID, snb.build(), if (VERSION.SDK_INT >= 30) FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE or FOREGROUND_SERVICE_TYPE_CAMERA else FOREGROUND_SERVICE_TYPE_PHONE_CALL ) else startForeground(STATUS_NOTIFICATION_ID, snb.build()) } catch (e: Exception) { Log.e(TAG, "Failed to start foreground service") } } private fun updateStatusNotification() { val notificationLayout = RemoteViews(packageName, R.layout.status_notification) for (i: Int in 0..3) { val resId = when (i) { 0-> R.id.status0 1-> R.id.status1 2-> R.id.status2 else -> R.id.status3 } if (i < uas.size) { notificationLayout.setImageViewResource(resId, uas[i].status) notificationLayout.setViewVisibility(resId, View.VISIBLE) } else { notificationLayout.setViewVisibility(resId, View.INVISIBLE) } } if (uas.size > 4) notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE) else notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE) snb.setCustomContentView(notificationLayout) // Don't know why, but without the delay the notification is not always updated Timer().schedule(250) { nm.notify(STATUS_NOTIFICATION_ID, snb.build()) } } private fun toast(message: String) { // Handler(Looper.getMainLooper()).post { // Toast.makeText(this@BaresipService.applicationContext, message, Toast.LENGTH_LONG).show() // } Handler(Looper.getMainLooper()).post { showCustomToast(this@BaresipService.applicationContext, message) } } fun showCustomToast(context: Context, message: String) { val inflater = LayoutInflater.from(context) val layout: View = inflater.inflate(R.layout.custom_toast, null) val textView: TextView = layout.findViewById(R.id.toast_text) textView.text = message val toast = Toast(context) toast.duration = Toast.LENGTH_LONG toast.view = layout toast.show() } private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable { val spannable: Spannable = SpannableString(applicationContext.getText(stringRes)) spannable.setSpan( ForegroundColorSpan(applicationContext.getColor(colorRes)), 0, spannable.length, 0) return spannable } private fun startRinging() { rt.isLooping = true rt.play() if (shouldVibrate()) { vbTimer = Timer() vbTimer!!.schedule(object : TimerTask() { override fun run() { vibrator.vibrate( VibrationEffect.createOneShot( 500, VibrationEffect.DEFAULT_AMPLITUDE ) ) } }, 500L, 2000L) } } private fun shouldVibrate(): Boolean { return if (am.ringerMode != RINGER_MODE_SILENT) { if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) { true } else { if (am.getStreamVolume(AudioManager.STREAM_RING) != 0) { @Suppress("DEPRECATION") Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) == 1 } else { false } } } else { false } } private fun stopRinging() { rt.stop() if (vbTimer != null) { vbTimer!!.cancel() vbTimer = null } } @SuppressLint("DiscouragedApi") private fun playRingBack() { if (mediaPlayer == null) { val name = "ringback_$toneCountry" val resourceId = applicationContext.resources.getIdentifier( name, "raw", applicationContext.packageName) if (resourceId != 0) { mediaPlayer = MediaPlayer.create(this, resourceId) mediaPlayer?.isLooping = true mediaPlayer?.start() } else { Log.e(TAG, "Ringback tone $name.wav not found") } } } @SuppressLint("DiscouragedApi") private fun playBusy() { if (mediaPlayer == null ) { val name = "busy_$toneCountry" val resourceId = applicationContext.resources.getIdentifier( name, "raw", applicationContext.packageName) if (resourceId != 0) { mediaPlayer = MediaPlayer.create(this, resourceId) mediaPlayer?.setOnCompletionListener { stopMediaPlayer() if (!Call.inCall()) { resetCallVolume() abandonAudioFocus(applicationContext) proximitySensing(false) } } mediaPlayer?.start() } else { Log.e(TAG, "Busy tone $name.wav not found") } } } private fun playUnInterrupted(raw: Int, count: Int) { val player = MediaPlayer.create(this, raw) player.setOnCompletionListener { it.stop() it.release() if (count > 1) playUnInterrupted(raw, count - 1) } player.start() } private fun stopMediaPlayer() { mediaPlayer?.stop() mediaPlayer?.release() mediaPlayer = null } private fun setCallVolume() { if (callVolume != 0) for (streamType in listOf(AudioManager.STREAM_MUSIC, AudioManager.STREAM_VOICE_CALL)) { origVolume[streamType] = am.getStreamVolume(streamType) val maxVolume = am.getStreamMaxVolume(streamType) am.setStreamVolume(streamType, (callVolume * 0.1 * maxVolume).roundToInt(), 0) Log.d(TAG, "Orig/new/max $streamType volume is " + "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume") } } private fun resetCallVolume() { if (callVolume != 0) for ((streamType, streamVolume) in origVolume) { am.setStreamVolume(streamType, streamVolume, 0) Log.d(TAG, "Reset $streamType volume to ${am.getStreamVolume(streamType)}") } } @SuppressLint("WakelockTimeout", "Wakelock") private fun proximitySensing(enable: Boolean) { if (enable) { if (!proximityWakeLock.isHeld) { Log.d(TAG, "Acquiring proximity wake lock") proximityWakeLock.acquire() } else { Log.d(TAG, "Proximity wake lock already acquired") } } else { if (proximityWakeLock.isHeld) { proximityWakeLock.release() Log.d(TAG, "Released proximity wake lock") } else { Log.d(TAG, "Proximity wake lock is not held") } } } private fun updateNetwork() { /* for (n in allNetworks) Log.d(TAG, "NETWORK $n with caps ${cm.getNetworkCapabilities(n)} and props " + "${cm.getLinkProperties(n)} is active ${isNetworkActive(n)}") */ updateDnsServers() val addresses = linkAddresses() Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses") var added = 0 for (a in addresses) if (!linkAddresses.containsKey(a.key)) { if (Api.net_add_address_ifname(a.key, a.value) != 0) Log.e(TAG, "Failed to add address: $a") else added++ } var removed = 0 for (a in linkAddresses) if (!addresses.containsKey(a.key)) { if (Api.net_rm_address(a.key) != 0) Log.e(TAG, "Failed to remove address: $a") else removed++ } val active = cm.activeNetwork Log.d(TAG, "Added/Removed/Old/New Active = $added/$removed/$activeNetwork/$active") if (added > 0 || removed > 0 || active != activeNetwork) { linkAddresses = addresses activeNetwork = active Api.uag_reset_transp(register = true, reinvite = true) } Api.net_debug() if (activeNetwork != null) { val caps = cm.getNetworkCapabilities(activeNetwork) if (caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { Log.d(TAG, "Acquiring WiFi Lock") wifiLock.acquire() return } } Log.d(TAG, "Releasing WiFi Lock") wifiLock.release() } private fun linkAddresses(): MutableMap { val addresses = mutableMapOf() for (n in allNetworks) { val caps = cm.getNetworkCapabilities(n) ?: continue if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) { val props = cm.getLinkProperties(n) ?: continue for (la in props.linkAddresses) if (la.scope == android.system.OsConstants.RT_SCOPE_UNIVERSE && props.interfaceName != null && la.address.hostAddress != null && afMatch(la.address.hostAddress!!)) addresses[la.address.hostAddress!!] = props.interfaceName!! } } if (hotSpotIsEnabled) { for ((k, v) in hotSpotAddresses) if (afMatch(k)) addresses[k] = v } return addresses } private fun afMatch(address: String): Boolean { return when (addressFamily) { "" -> true "ipv4" -> address.contains(".") else -> address.contains(":") } } private fun updateDnsServers() { if (isServiceRunning && !dynDns) return val servers = mutableListOf() // Use DNS servers first from active network (if available) if (cm.activeNetwork != null) { val linkProps = cm.getLinkProperties(cm.activeNetwork) if (linkProps != null) servers.addAll(linkProps.dnsServers) } // Then add DNS servers from the other networks for (n in allNetworks) { if (n == cm.activeNetwork) continue val linkProps = cm.getLinkProperties(n) if (linkProps != null) for (server in linkProps.dnsServers) if (!servers.contains(server)) servers.add(server) } // Update if change if (servers != dnsServers) { if (isServiceRunning && Config.updateDnsServers(servers) != 0) { Log.w(TAG, "Failed to update DNS servers '${servers}'") } else { // Log.d(TAG, "Updated DNS servers: '${servers}'") dnsServers = servers } } } private fun registerAndroidContactsObserver() { if (!androidContactsObserverRegistered) try { contentResolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, androidContactsObserver) androidContactsObserverRegistered = true } catch (e: SecurityException) { Log.i(TAG, "No Contacts permission") } } private fun unRegisterAndroidContactsObserver() { if (androidContactsObserverRegistered) { contentResolver.unregisterContentObserver(androidContactsObserver) androidContactsObserverRegistered = false } } private fun cleanService() { if (!isServiceClean) { this.unregisterReceiver(bluetoothReceiver) this.unregisterReceiver(hotSpotReceiver) stopRinging() stopMediaPlayer() abandonAudioFocus(applicationContext) uas.clear() callHistory.clear() messages.clear() if (this::nm.isInitialized) nm.cancelAll() if (this::partialWakeLock.isInitialized && partialWakeLock.isHeld) partialWakeLock.release() if (this::proximityWakeLock.isInitialized && proximityWakeLock.isHeld) proximityWakeLock.release() if (this::wifiLock.isInitialized) wifiLock.release() if (this::androidContactsObserver.isInitialized) contentResolver.unregisterContentObserver(androidContactsObserver) isServiceClean = true } } private external fun baresipStart( path: String, addresses: String, logLevel: Int, software: String ) external fun baresipStop(force: Boolean) companion object { var isServiceRunning = false var isStartReceived = false var isConfigInitialized = false var libraryLoaded = false var supportedCameras = false var cameraFront = true var callVolume = 0 var speakerPhone = false var audioDelay = if (VERSION.SDK_INT < 31) 1500L else 500L var dynDns = false var filesPath = "" var pName = "" var logLevel = 2 var sipTrace = false var callActionUri = "" var isMainVisible = false var isMicMuted = false var isRecOn = false var toneCountry = "us" var videoSize = Size(0, 0) val uas = ArrayList() val calls = ArrayList() var callHistory = ArrayList() var messages = ArrayList() val messageUpdate = MutableLiveData() val contactUpdate = MutableLiveData() val registrationUpdate = MutableLiveData() val baresipContacts = ArrayList() val androidContacts = ArrayList() val contacts = ArrayList() val contactNames = ArrayList() var contactsMode = "baresip" val chatTexts: MutableMap = mutableMapOf() val activities = mutableListOf() var addressFamily = "" var dnsServers = listOf() val serviceEvent = MutableLiveData>() val serviceEvents = mutableListOf() // of those accounts that have auth username without auth password val aorPasswords = mutableMapOf() var audioFocusRequest: AudioFocusRequestCompat? = null var aecAvailable = AcousticEchoCanceler.isAvailable() var agcAvailable = AutomaticGainControl.isAvailable() private val nsAvailable = NoiseSuppressor.isAvailable() private var aec: AcousticEchoCanceler? = null private var ns: NoiseSuppressor? = null private var agc: AutomaticGainControl? = null var webrtcAec = false private var btAdapter: BluetoothAdapter? = null private var playerSessionId = 0 private var recorderSessionId = 0 /* Added by ritoseo */ val DISPLAY_SPLIT_MODE_원거리_최대_근거리_우하단 = 1 val DISPLAY_SPLIT_MODE_원거리_최대_근거리_우상단 = 2 val DISPLAY_SPLIT_MODE_원거리_최대_근거리_좌상단 = 3 val DISPLAY_SPLIT_MODE_원거리_최대_근거리_좌하단 = 4 val DISPLAY_SPLIT_MODE_원거리_대상단_근거리_소하단 = 5 val DISPLAY_SPLIT_MODE_원거리_대좌단_근거리_소우단 = 6 val DISPLAY_SPLIT_MODE_원거리_대하단_근거리_소상단 = 7 val DISPLAY_SPLIT_MODE_원거리_반좌단_근거리_반우단 = 8 var isSupportAudioCall = false var deviceIpAddress = "0.0.0.0" var deviceName = "" var preferTransport = "udp" var videoFormatHeight = 480 var connectedDisplayCount = 0 var useDisplaySplitMode = false var displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_우하단 var farViewDisplayId = 1 var nearViewDisplayId = 2 var farViewLayout : Rect = Rect(0, 0, 1920, 1080) var nearViewLayout : Rect = Rect(0, 0, 1920, 1080) fun requestAudioFocus(ctx: Context): Boolean { Log.d(TAG, "Requesting audio focus") if (audioFocusRequest != null) { Log.d(TAG, "Already focused") return true } val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager val attributes = AudioAttributesCompat.Builder() .setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) .build() audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) .setAudioAttributes(attributes) .setOnAudioFocusChangeListener { } .build() if (AudioManagerCompat.requestAudioFocus(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.d(TAG, "requestAudioFocus granted") if (isBluetoothHeadsetConnected(ctx)) startBluetoothSco(ctx, 250L, 3) } else { Log.w(TAG, "requestAudioFocus denied") audioFocusRequest = null } return audioFocusRequest != null } fun abandonAudioFocus(ctx: Context) { val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager if (audioFocusRequest != null) { Log.d(TAG, "Abandoning audio focus") if (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { audioFocusRequest = null if (isBluetoothHeadsetConnected(ctx)) stopBluetoothSco(ctx) } else { Log.e(TAG, "Failed to abandon audio focus") } } am.mode = MODE_NORMAL } private fun isBluetoothHeadsetConnected(ctx: Context): Boolean { if (VERSION.SDK_INT >= 31 && ActivityCompat.checkSelfPermission(ctx, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) return false return btAdapter != null && btAdapter!!.isEnabled && btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothAdapter.STATE_CONNECTED } private fun isBluetoothScoOn(am: AudioManager): Boolean { return if (VERSION.SDK_INT < 31) @Suppress("DEPRECATION") am.isBluetoothScoOn else if (am.communicationDevice != null) am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO else false } private fun startBluetoothSco(ctx: Context, delay: Long, count: Int) { val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager if (isBluetoothScoOn(am)) { Log.d(TAG, "Bluetooth SCO is already on") return } Log.d(TAG, "Starting Bluetooth SCO at count $count") Handler(Looper.getMainLooper()).postDelayed({ if (VERSION.SDK_INT < 31) { @Suppress("DEPRECATION") am.startBluetoothSco() } else { Utils.setCommunicationDevice(am, AudioDeviceInfo.TYPE_BLUETOOTH_SCO) } if (!isBluetoothScoOn(am) && count > 1) startBluetoothSco(ctx, delay, count - 1) else am.isBluetoothScoOn = true }, delay) } private fun stopBluetoothSco(ctx: Context) { Log.d(TAG, "Stopping Bluetooth SCO") val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager if (!isBluetoothScoOn(am)) { Log.d(TAG, "Bluetooth SCO is already off") return } Handler(Looper.getMainLooper()).postDelayed({ if (VERSION.SDK_INT < 31) @Suppress("DEPRECATION") am.stopBluetoothSco() else am.clearCommunicationDevice() am.isBluetoothScoOn = false }, 100) } } }