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<Int, Int>()
    private var linkAddresses = mutableMapOf<String, String>()
    private var activeNetwork: Network? = null
    private var allNetworks = mutableSetOf<Network>()
    private var hotSpotIsEnabled = false
    private var hotSpotAddresses = mapOf<String, String>()
    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)
                        if(netInfo.get("ip") != null) {
                            deviceIpAddress = netInfo.get("ip")!!
                        }
                        Utils.propertySet("sys.ritosip.ipv4.address", deviceIpAddress)
                        if(netInfo.get("netmask") != null) {
                            Utils.propertySet("sys.ritosip.ipv4.netmask", netInfo.get("netmask")!!)
                        }
                        if(netInfo.get("gateway") != null) {
                            Utils.propertySet("sys.ritosip.ipv4.gateway", netInfo.get("gateway")!!)
                        }
                        if(netInfo.get("dns") != null) {
                            Utils.propertySet("sys.ritosip.ipv4.dns", netInfo.get("dns")!!)
                        }

                        Utils.checkNetworkIpType()

                        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"))
                        newIntent.putExtra("address", deviceIpAddress)

                        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 == "remove_history") {
                        CallsActivity.uaHistory.clear()
                        BaresipService.callHistory.clear()
                        CallHistoryNew.delete()
                    } 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 == "video_mute") {
                        if(uas.size > 0) {
                            val ua = uas[0]
                            val call = ua.currentCall()
                            call?.setVideoMute(true)
                            isVideoMuted = true
                        }
                    } else if(req == "video_unmute") {
                        if(uas.size > 0) {
                            val ua = uas[0]
                            val call = ua.currentCall()
                            call?.setVideoMute(false)
                            isVideoMuted = false
                        }
                    } else if(req == "audio_source") {
                        val param = intent?.getStringExtra("param")
                        if(param != null) {
                            try {
                                val json = JSONObject(param)
                                var device = "usb"
                                val source_target = json.getString("target")
                                if(source_target == "aux") {
                                    device = "codec"
                                }
                                Utils.setAudioSourceDevice(device)
                                audioInputDevice = device
                            } catch(e : Exception) {
                                e.printStackTrace()
                            }
                        }
                    } 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)

                                    if(BaresipService.callAnswerMode == "manual") {
                                        Api.account_set_answermode(acc.accp, Api.ANSWERMODE_MANUAL)
                                        acc.answerMode = Api.ANSWERMODE_MANUAL
                                    } else {
                                        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_원거리_반좌단_근거리_반우단
                            } else if(layout == "layout9") {
                                displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_없음
                            } else if(layout == "layout10") {
                                displaySplitMode = DISPLAY_SPLIT_MODE_원거리_없음_근거리_최대
                            }

                            Config.replaceVariable("display_split_mode", displaySplitMode.toString())
                            Config.save()

                            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")
                            val display_type = json.getString("display_type")

                            Config.replaceVariable("device_name", device_name)
                            BaresipService.deviceName = device_name

                            Config.replaceVariable("far_view_display_id", display_type)
                            BaresipService.farViewDisplayId = display_type.toInt()
                            Config.save()

                            sendActivityAction("update info")
                            sendActivityAction("update display")
                        }
                    }
                }
            }
        }
        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()) {
                    try {
                        val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera_near.rgb565")
                        val bufSize : Int = nearBuf.capacity() / 2
                        var width : Int = 1280
                        var height : Int = 720
                        if(bufSize == 1280 * 720) {
                            width = 1280
                            height = 720
                        } else if(bufSize == 1920 * 1080) {
                            width = 1920
                            height = 1080
                        }
                        Utils.saveRGB565AsJPG(
                            applicationContext,
                            nearBuf,
                            width,
                            height,
                            "/mnt/obb/camera_near.jpg"
                        )
                    } catch(e : Exception) {
                        e.printStackTrace()
                    }
                }
                if(File("/mnt/obb/camera_far.rgb565.done").exists() && !File("/mnt/obb/camera_far.jpg").exists()) {
                    try {
                        val farBuf = Utils.readFileToByteBuffer("/mnt/obb/camera_far.rgb565")
                        val bufSize : Int = farBuf.capacity() / 2
                        var width : Int = 1280
                        var height : Int = 720
                        if(bufSize == 1280 * 720) {
                            width = 1280
                            height = 720
                        } else if(bufSize == 1920 * 1080) {
                            width = 1920
                            height = 1080
                        }
                        Utils.saveRGB565AsJPG(
                            applicationContext,
                            farBuf,
                            width,
                            height,
                            "/mnt/obb/camera_far.jpg"
                        )
                    } catch(e : Exception) {
                        e.printStackTrace()
                    }
                }

                //delay(800)  // 1초마다 실행
                for(i in 1..8) {
                    var callExist = false
                    if(uas.size > 0) {
                        val ua = uas[0]
                        val account = ua.account
                        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(account != null) {
                            Utils.propertySet("sys.ritosip.account.display_name", account.nickName)
                            Utils.propertySet("sys.ritosip.account.account_name", account.authUser)
                            Utils.propertySet("sys.ritosip.account.server_address", account.host())
                            Utils.propertySet("sys.ritosip.account.prefer_transport", preferTransport)
                            if(account.mediaEnc.startsWith("srtp", ignoreCase = true)) {
                                Utils.propertySet(
                                    "sys.ritosip.account.media_encryption",
                                    "srtp"
                                )
                            } else {
                                Utils.propertySet(
                                    "sys.ritosip.account.media_encryption",
                                    "none"
                                )
                            }
                        }

                        if (call != null) {
                            callExist = true
                            var audioMute = "unmute"
                            var videoMute = "unmute"
                            if(isMicMuted)
                                audioMute = "mute"
                            if(isVideoMuted)
                                videoMute = "mute"
                            Utils.propertySet("sys.ritosip.call.mute.audio", audioMute)
                            Utils.propertySet("sys.ritosip.call.mute.video", videoMute)

                            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++

                    //println("Changed connectedDisplayCount : ${connectedDisplayCount}, connectedCount : ${connectedCount}")
                    if(connectedDisplayCount != connectedCount) {
                        val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
                        val displays = displayManager.displays
                        if(connectedCount > connectedDisplayCount) {
                            if(connectedCount == displays.size) {
                                connectedDisplayCount = connectedCount
                                //println("Changed connectedDisplayCount : ${connectedDisplayCount}")

                                sendActivityAction("update display")
                            }
                        } else {
                            connectedDisplayCount = connectedCount
                            //println("Changed connectedDisplayCount : ${connectedDisplayCount}")

                            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)
                    //call.setVideoSource(!BaresipService.cameraFront)
                    val prevCamera2 = Utils.propertyGet("sys.ritosip.camera2.status")
                    Utils.propertySet("sys.ritosip.camera2.status", camera2)
                    if(camera2 != prevCamera2) {
                        if(uas.size > 0) {
                            val ua = uas[0]
                            val call = ua.currentCall()

                            if(call != null) {
                                if(camera2 == "plugin") {
                                    BaresipService.cameraFront = false;     // Contents Input
                                    call.setVideoSource(false)
                                } else {
                                    BaresipService.cameraFront = true;      // Camera Input
                                    call.setVideoSource(true)
                                }
                            }
                        }
                    }

                    if(BaresipService.cameraFront) {
                        Utils.propertySet("sys.ritosip.media.input", "camera")
                    } else {
                        Utils.propertySet("sys.ritosip.media.input", "content")
                    }

                    val usbMicExist = Utils.checkUsbMicrophone()
                    if(usbMicExist) {
                        Utils.propertySet("sys.ritosip.usb.microphone", "connected")
                        if(audioInputDevice == "usb") {
                            Utils.propertySet("sys.rito.audio.input.device", "usb")
                        } else {
                            Utils.propertySet("sys.rito.audio.input.device", "codec")
                        }
                    } else {
                        Utils.propertySet("sys.ritosip.usb.microphone", "disconnected")
                    }

                    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", "")
                    }
                }

                if(BaresipService.deviceName != null) {
                    Utils.propertySet("sys.ritosip.device_name", BaresipService.deviceName)
                }

                if(BaresipService.callAnswerMode != null) {
                    Utils.propertySet("sys.ritosip.answer_mode", BaresipService.callAnswerMode)
                }

                Utils.propertySet("sys.ritosip.far_view_display_id", BaresipService.farViewDisplayId.toString())
                Utils.propertySet("sys.ritosip.display_split_mode", BaresipService.displaySplitMode.toString())
                Utils.propertySet("sys.ritosip.sip.total_duration", BaresipService.totalDurationSeconds.toString())

                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()
                }

                val value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration")
                if(value != null) {
                    BaresipService.totalDurationSeconds = value
                }

                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") ?: "?"})"
                    )


                    Api.log_level_set(0)
                    Log.logLevelSet(0)
                    Api.uag_enable_sip_trace(true)
                }.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
        }

        if (ev[0] == "vu_tx_report") {
            Utils.propertySet("sys.ritosip.call.audio.tx_level", ev[1])
            return
        }

        if (ev[0] == "vu_rx_report") {
            Utils.propertySet("sys.ritosip.call.audio.rx_level", ev[1])
            return
        }

        val ua = UserAgent.ofUap(uap)
        val aor = ua?.account?.aor

        Log.w(TAG, "[RITO] got uaEvent $event/$aor/$callp")
        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
                            }

                            /* Added by ritoseo */
                            val duration = call.duration()
                            BaresipService.totalDurationSeconds += duration
                            Utils.saveNumberToFile("/sdcard/Documents/sip_total_duration", BaresipService.totalDurationSeconds)
                            /* ---------------- */

                            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<String, String> {
        val addresses = mutableMapOf<String, String>()
        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<InetAddress>()
        // 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 isVideoMuted = false
        var isRecOn = false
        var toneCountry = "us"
        var videoSize = Size(0, 0)

        val uas = ArrayList<UserAgent>()
        val calls = ArrayList<Call>()
        var callHistory = ArrayList<CallHistoryNew>()
        var messages = ArrayList<Message>()
        val messageUpdate = MutableLiveData<Long>()
        val contactUpdate = MutableLiveData<Long>()
        val registrationUpdate = MutableLiveData<Long>()
        val baresipContacts = ArrayList<Contact.BaresipContact>()
        val androidContacts = ArrayList<Contact.AndroidContact>()
        val contacts = ArrayList<Contact>()
        val contactNames = ArrayList<String>()
        var contactsMode = "baresip"
        val chatTexts: MutableMap<String, String> = mutableMapOf()
        val activities = mutableListOf<String>()
        var addressFamily = ""
        var dnsServers = listOf<InetAddress>()
        val serviceEvent = MutableLiveData<Event<Long>>()
        val serviceEvents = mutableListOf<ServiceEvent>()
        // <aor, password> of those accounts that have auth username without auth password
        val aorPasswords = mutableMapOf<String, String>()
        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
        val DISPLAY_SPLIT_MODE_원거리_최대_근거리_없음 = 9
        val DISPLAY_SPLIT_MODE_원거리_없음_근거리_최대 = 10
        var isSupportAudioCall = false
        var deviceIpAddress = "0.0.0.0"
        var deviceName = ""
        var preferTransport = "udp"
        var videoFormatHeight = 480
        var connectedDisplayCount = 0
        var prevConnectedDisplayCount = 0
        var callAnswerMode = "auto"
        var audioInputDevice = "usb"
        var useDisplaySplitMode = false
        var totalDurationSeconds = 0
        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)
        }

    }

}
