package com.tutpro.baresip import android.Manifest import android.Manifest.permission.RECORD_AUDIO import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service 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_MICROPHONE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL import android.content.res.Configuration import android.database.ContentObserver import android.graphics.ImageDecoder import android.media.AudioAttributes import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.AudioManager.MODE_IN_COMMUNICATION import android.media.AudioManager.MODE_NORMAL import android.media.MediaPlayer import android.media.Ringtone import android.media.RingtoneManager import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import android.net.ConnectivityManager import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri import android.net.wifi.WifiManager import android.os.Build.VERSION import android.os.CountDownTimer import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.PowerManager import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager import android.provider.ContactsContract import android.provider.Settings import android.system.OsConstants import android.telecom.TelecomManager import android.view.View import android.widget.RemoteViews import android.widget.Toast import androidx.annotation.Keep import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import androidx.media.AudioAttributesCompat import androidx.media.AudioFocusRequestCompat import androidx.media.AudioManagerCompat import com.tutpro.baresip.Utils.toCircle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File import java.net.InetAddress import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.util.GregorianCalendar import java.util.Timer import java.util.TimerTask import kotlin.concurrent.schedule import kotlin.math.roundToInt class BaresipService: Service() { internal lateinit var intent: Intent private lateinit var am: AudioManager 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 networkCallback: ConnectivityManager.NetworkCallback private lateinit var stopState: String private lateinit var quitTimer: CountDownTimer private var vbTimer: Timer? = null private var origVolume = mutableMapOf() private var linkAddresses = mutableMapOf() private var activeNetwork: Network? = null private var allNetworks = mutableSetOf() private var hotSpotIsEnabled = false private var hotSpotAddresses = mapOf() private var mediaPlayer: MediaPlayer? = null private var androidContactsObserverRegistered = false private var bluetoothReceiverRegistered = false private var hotSpotReceiverRegistered = false private var isServiceClean = false @SuppressLint("WakelockTimeout") override fun onCreate() { super.onCreate() Log.i(TAG, "BaresipService onCreate") intent = Intent("com.tutpro.baresip.EVENT") intent.setPackage("com.tutpro.baresip") filesPath = filesDir.absolutePath pName = packageName am = applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, RingtoneManager.TYPE_NOTIFICATION) nt = RingtoneManager.getRingtone(applicationContext, ntUri) val rtUri = if (Preferences(applicationContext).ringtoneUri == "") Settings.System.DEFAULT_RINGTONE_URI else Preferences(applicationContext).ringtoneUri!!.toUri() rt = RingtoneManager.getRingtone(applicationContext, rtUri) nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager createNotificationChannels() snb = NotificationCompat.Builder(this, LOW_CHANNEL_ID) pm = getSystemService(POWER_SERVICE) as PowerManager vibrator = if (VERSION.SDK_INT >= 31) { val vibratorManager = applicationContext.getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator } // This is needed to keep service running also in Doze Mode partialWakeLock = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "com.tutpro.baresip:wakelock" ).apply { setReferenceCounted(false) } networkCallback = 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) } 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() } } cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager cm.registerNetworkCallback( NetworkRequest.Builder() .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .build(), networkCallback ) wm = applicationContext.getSystemService(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() } } } } } } ContextCompat.registerReceiver( applicationContext, hotSpotReceiver, IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED"), ContextCompat.RECEIVER_NOT_EXPORTED ) hotSpotReceiverRegistered = true tm = getSystemService(TELECOM_SERVICE) as TelecomManager btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager btAdapter = btm.adapter proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "com.tutpro.baresip: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) ContextCompat.registerReceiver(applicationContext, bluetoothReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) bluetoothReceiverRegistered = true } androidContactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { 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) } } } } } 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.message}") } } 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.i(TAG, "Asset '$a' already copied") } if (a == "config") Config.initialize(applicationContext) } 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 new.add() } } Blocked.restore() val recordings = File(filesDir, "recordings") val restored = File(filesPath, "restored") if (restored.exists()) { Log.d(TAG, "Clearing recordings") CallHistoryNew.clearRecordings() CallHistoryNew.save() if (recordings.exists()) recordings.deleteRecursively() restored.delete() } File(filesDir, "recordings").mkdir() File(filesDir, "tmp").mkdir() 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.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable") val userAgent = Config.variable("user_agent") Thread { baresipStart( filesPath, addresses.removePrefix(";"), logLevel, if (userAgent != "") userAgent else "baresip v${BuildConfig.VERSION_NAME} " + "(Android ${VERSION.RELEASE}/${System.getProperty("os.arch") ?: "?"})" ) }.start() isServiceRunning = true showStatusNotification() 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()) toast(getString(R.string.no_network), Toast.LENGTH_LONG) if (!aecAvailable) toast(getString(R.string.no_aec), Toast.LENGTH_LONG) } "Notification Dismissed" -> { updateStatusNotification() } "Start Content Observer" -> { registerAndroidContactsObserver() } "Stop Content Observer" -> { unRegisterAndroidContactsObserver() } "Call Answer" -> { val uap = intent!!.getLongExtra("uap", 0L) val callp = intent.getLongExtra("callp", 0L) stopRinging() stopMediaPlayer() am.mode = MODE_IN_COMMUNICATION setCallVolume() proximitySensing(proximitySensing) Api.ua_answer(uap, callp, Api.VIDMODE_OFF) } "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 { Message.updateAorMessage( ua.account.aor, intent.getStringExtra("time")!!.toLong() ) ua.account.unreadMessages = Message.unreadMessages(ua.account.aor) } 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 { Message.deleteAorMessage( ua.account.aor, intent.getStringExtra("time")!!.toLong() ) ua.account.unreadMessages = Message.unreadMessages(ua.account.aor) } nm.cancel(MESSAGE_NOTIFICATION_ID) } "Message Inline Reply" -> { val remoteInputResults = RemoteInput.getResultsFromIntent(intent!!) if (remoteInputResults != null) { val replyText = remoteInputResults.getCharSequence(KEY_TEXT_REPLY)?.toString() if (!replyText.isNullOrEmpty()) { val uap = intent.getLongExtra("uap", -1L) val ua = UserAgent.ofUap(uap)!! val aor = ua.account.aor var peerUri = intent.getStringExtra("peer")!! val timeStamp = intent.getLongExtra("time", 0L) if (Utils.isTelUri(peerUri)) { if (ua.account.telProvider == "") { Log.w(TAG, "No telephony provider for $aor") peerUri = "" } else peerUri = Utils.telToSip(peerUri, ua.account) } if (peerUri != "") { Log.d(TAG, "Direct Reply from $aor to $peerUri: $replyText") Message.updateAorMessage(aor, timeStamp) val time = System.currentTimeMillis() val msg = Message(aor, peerUri, replyText, time, MESSAGE_UP_WAIT, 0, "", false) msg.add() if (Api.message_send(uap, peerUri, replyText, time.toString()) != 0) { Log.w(TAG, "message_send failed") msg.direction = MESSAGE_UP_FAIL msg.responseReason = getString(R.string.message_failed) } else { ua.account.unreadMessages = Message.unreadMessages(aor) } } } } 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 onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK when (currentNightMode) { Configuration.UI_MODE_NIGHT_NO -> { darkTheme.value = Preferences(applicationContext).displayTheme == AppCompatDelegate.MODE_NIGHT_YES } Configuration.UI_MODE_NIGHT_YES -> { darkTheme.value = true } } } 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.Restart")) } @Suppress("unused") @SuppressLint("UnspecifiedImmutableFlag", "DiscouragedApi", "FullScreenIntentPolicy") @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 circleYellow.getValue(colorblind) ua.add() 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] == "recorder sessionid") { recorderSessionId = ev[1].toInt() Log.d(TAG, "got recorder sessionid $recorderSessionId") if (recorderSessionId != 0) { if (aecAvailable) { aec = AcousticEchoCanceler.create(recorderSessionId) if (aec != null) { if (!aec!!.enabled) { aec!!.enabled = true if (aec!!.enabled) Log.d(TAG, "AEC is enabled") else Log.w(TAG, "Failed to enable AEC") } else Log.d(TAG, "AEC is already enabled") } else Log.w(TAG, "Failed to create AEC for session $recorderSessionId") } if (agcAvailable) { agc = AutomaticGainControl.create(recorderSessionId) if (agc != null) { if (!agc!!.enabled) { agc!!.enabled = true if (agc!!.enabled) 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!!.enabled) { ns!!.enabled = true if (ns!!.enabled) Log.d(TAG, "NS is enabled") } } else Log.w(TAG, "Failed to create NS") } recorderSessionId = 0 } return } val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "uaEvent $event did not find ua $uap") return } val aor = ua.account.aor Log.d(TAG, "got uaEvent $event/$aor/$callp") 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 } for (accountIndex in uas.value.indices) { if (uas.value[accountIndex].account.aor == aor) { when (ev[0]) { "registering", "unregistering" -> { ua.updateStatus(circleYellow.getValue(colorblind)) updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) return } "registered" -> { ua.updateStatus( if (Api.account_regint(ua.account.accp) == 0) R.drawable.circle_white else circleGreen.getValue(colorblind) ) updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) return } "registering failed" -> { ua.updateStatus(if (Api.account_regint(ua.account.accp) == 0) R.drawable.circle_white else circleRed.getValue(colorblind) ) 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.value == "transferring") break stopMediaPlayer() setCallVolume() if (speakerPhone && !Utils.isSpeakerPhoneOn(am)) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am) proximitySensing(proximitySensing) } "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 blockUnknown = ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri val toastMsg = if (Call.inCall()) String.format(getString(R.string.call_auto_rejected), Utils.friendlyUri(this, peerUri, ua.account)) else if (blockUnknown) String.format(getString(R.string.call_blocked), Utils.friendlyUri(this, peerUri, ua.account)) else if (!Utils.checkPermissions(this, arrayOf(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 (toastMsg != "") { Log.d(TAG, "Auto-rejecting incoming call to $uap from $peerUri") Api.sip_treply(callp, 486, "Busy Here") Api.bevent_stop(bevent) toast(toastMsg) if (blockUnknown) { if (ua.account.callHistory) Blocked( ua.account.aor, peerUri, "invite", GregorianCalendar().timeInMillis ).add() } else { 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(aor, peerUri, "in").add() 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").add() if (speakerPhone && !Utils.isSpeakerPhoneOn(am)) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am) if (ua.account.answerMode == Api.ANSWERMODE_AUTO) { 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 } val channelId = HIGH_CHANNEL_ID val callerNumber = peerUri.split(":")[1].split("@")[0] if (shouldStartRinging(callerNumber)) startRinging() if (!Utils.isVisible()) { val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT val intent = Intent(applicationContext, MainActivity::class.java) .putExtra("action", "call show") .putExtra("callp", callp) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags) val nb = NotificationCompat.Builder(this, channelId) val caller = Utils.friendlyUri(this, peerUri, ua.account) val callerContact = Contact.findContact(peerUri) val personBuilder = Person.Builder().setName(caller) val contactColor = callerContact?.color() ?: "#B0B0B0" val initial = if (caller.isNotEmpty()) caller.take(1) else "?" val textAvatarBitmap = Utils.createTextAvatar(initial,contactColor) var icon = IconCompat.createWithBitmap(textAvatarBitmap) if (callerContact is Contact.BaresipContact) { if (callerContact.avatarImage != null) icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle()) } else if (callerContact is Contact.AndroidContact) { if (callerContact.thumbnailUri != null) { try { val source = ImageDecoder.createSource(contentResolver, callerContact.thumbnailUri!!) val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE // decoder.setTargetSize(256, 256) } icon = IconCompat.createWithBitmap(bitmap.toCircle()) } catch (e: Exception) { Log.e(TAG, "Failed to load Android contact avatar: $e") } } } val person = personBuilder.setIcon(icon).build() nb.setSmallIcon(R.drawable.ic_notification_call) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) .setCategory(Notification.CATEGORY_CALL) .setAutoCancel(false) .setOngoing(true) .setContentText(getString(R.string.is_calling)) .setWhen(System.currentTimeMillis()) .setShowWhen(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_MAX) if (VERSION.SDK_INT < 34 || nm.canUseFullScreenIntent()) nb.setFullScreenIntent(pi, true) val answerIntent = Intent(applicationContext, MainActivity::class.java) .putExtra("action", "call answer") .putExtra("callp", callp) val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE, answerIntent, piFlags) val rejectIntent = Intent(this, BaresipService::class.java) rejectIntent.action = "Call Reject" rejectIntent.putExtra("callp", callp) val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags) nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api)) nm.notify(CALL_NOTIFICATION_ID, nb.build()) return } } "call answered" -> { stopMediaPlayer() if (call!!.status.value == "incoming") call.status.value = "answered" else return } "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.value = "connected" 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.value != "connected") return } "call verified", "call secure" -> { if (ev[0] == "call secure") { call!!.security = R.color.colorTrafficYellow } else { call!!.security = R.color.colorTrafficGreen call.zid = ev[1] } if (!isMainVisible) return } "call transfer" -> { if (!Utils.isVisible()) { val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 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, piFlags) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) val target = Utils.friendlyUri(this, ev[1], ua.account) nb.setSmallIcon(R.drawable.ic_notification_call) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .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 acceptPendingIntent = PendingIntent.getActivity(applicationContext, ACCEPT_REQ_CODE, acceptIntent, piFlags) val denyIntent = Intent(this, BaresipService::class.java) denyIntent.action = "Transfer Deny" denyIntent.putExtra("callp", callp) val denyPendingIntent = PendingIntent.getService(this, DENY_REQ_CODE, denyIntent, piFlags) nb.addAction(R.drawable.ic_notification_call, getString(R.string.accept), acceptPendingIntent) nb.addAction(R.drawable.ic_notification_call_end, getString(R.string.deny), denyPendingIntent) 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 prm: ${ev[1]}") nm.cancel(CALL_NOTIFICATION_ID) if (call != null) { stopRinging() stopMediaPlayer() aec?.release() aec = null agc?.release() agc = null ns?.release() ns = null 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() if (call.conferenceCall && ua.calls().isEmpty()) Api.module_unload("mixminus") 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 val completedElsewhere = missed && ev[2].startsWith("SIP") && ev[2].contains(";cause=200") if (ua.account.callHistory) { val history = CallHistoryNew(aor, call.peerUri, call.dir) history.stopTime = GregorianCalendar() history.startTime = if (completedElsewhere) history.stopTime else call.startTime history.rejected = call.rejected if (call.dumpfiles[0] != "") { history.recording = call.dumpfiles } history.add() CoroutineScope(Dispatchers.IO).launch { if (call.startTime != null && call.dumpfiles[0] != "") { delay(500) val rxFile = File(call.dumpfiles[0]) val txFile = File(call.dumpfiles[1]) val mergedFileName = rxFile.name .replace("dump", "rec") .replace("=>", "-") .replace("sip:", "") .replace("-enc", "") .replace("*", "#") .replace(";user=phone", "") val mergedFile = File(filesPath, mergedFileName) if (Utils.mergeWavFiles(rxFile, txFile, mergedFile)) { Log.d(TAG, "Automatic merge succeeded.") history.recording = arrayOf(mergedFile.absolutePath, "") CallHistoryNew.save() try { rxFile.delete() txFile.delete() } catch (e: Exception) { Log.w(TAG, "Could not delete temporary raw files after merge: ${e.message}") } } else { Log.e(TAG, "Automatic merge failed. Storing raw file paths as fallback.") history.recording = call.dumpfiles } } } ua.account.missedCalls = ua.account.missedCalls || missed } if (missed) { val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 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, piFlags) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) nb.setSmallIcon(R.drawable.ic_notification_call_missed) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .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()) } if (!Utils.isVisible()) 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") } else toast("${getString(R.string.call_closed)}: ${Api.call_peer_uri(callp)}: $reason") } } } } } postServiceEvent(ServiceEvent(event, arrayListOf(uap, callp), System.nanoTime())) } @Suppress("unused") @SuppressLint("UnspecifiedImmutableFlag") @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 } if (ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri) { Log.d(TAG, "Auto-rejecting incoming message by $uap from $peerUri") Blocked( ua.account.aor, peerUri, "message", GregorianCalendar().timeInMillis ).add() toast(String.format( getString(R.string.message_blocked), Utils.friendlyUri(this, peerUri, ua.account) ) ) // Api.sip_treply(callp, 486, "Busy Here") // Api.bevent_stop(bevent) return } val charsetString = Utils.paramValue(cType.replace(" ", ""), "charset") val charset = try { Charset.forName(charsetString) } catch (_: Exception) { StandardCharsets.UTF_8 } val text = try { String(msg, charset) } catch (e: Exception) { val error = "Decoding of message failed using charset $charset from $cType: ${e.message}!" 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() ua.account.unreadMessages = true if (!Utils.isVisible()) { // common flags val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT // message show 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, piFlags) // message notification builder val sender = Utils.friendlyUri(this, peerUri, ua.account) val senderContact = Contact.findContact(peerUri) val personBuilder = Person.Builder().setName(sender) val contactColor = senderContact?.color() ?: "#B0B0B0" val initial = if (sender.isNotEmpty()) sender.take(1) else "?" val textAvatarBitmap = Utils.createTextAvatar(initial,contactColor) var icon = IconCompat.createWithBitmap(textAvatarBitmap) if (senderContact is Contact.BaresipContact) { if (senderContact.avatarImage != null) icon = IconCompat.createWithBitmap(senderContact.avatarImage!!.toCircle()) } else if (senderContact is Contact.AndroidContact) { if (senderContact.thumbnailUri != null) { try { val source = ImageDecoder.createSource(contentResolver, senderContact.thumbnailUri!!) val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE // decoder.setTargetSize(256, 256) } icon = IconCompat.createWithBitmap(bitmap.toCircle()) } catch (e: Exception) { Log.e(TAG, "Failed to load Android contact avatar: $e") } } } val senderPerson = personBuilder.setIcon(icon).build() val localUserPerson = Person.Builder() .setName(getString(R.string.you)) .setKey(ua.account.aor) .build() val messagingStyle = MessagingStyle(localUserPerson) .setConversationTitle(null) .setGroupConversation(false) .addMessage(text, timeStamp, senderPerson) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification_message) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) .setSound(Settings.System.DEFAULT_NOTIFICATION_URI) .setAutoCancel(true) .setStyle(messagingStyle) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setPriority(NotificationCompat.PRIORITY_HIGH) // message inline reply val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY) .setLabel(getString(R.string.reply)) .build() val directReplyIntent = Intent(this, BaresipService::class.java) directReplyIntent.action = "Message Inline Reply" directReplyIntent.putExtra("uap", uap).putExtra("peer", peerUri).putExtra("time", timeStamp) val directReplyPendingIntent = PendingIntent.getService( this, DIRECT_REPLY_REQ_CODE, directReplyIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) val inlineReplyAction = NotificationCompat.Action.Builder( R.drawable.ic_notification_reply, getString(R.string.reply), directReplyPendingIntent ).addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY). build() // message save val saveIntent = Intent(this, BaresipService::class.java) saveIntent.action = "Message Save" saveIntent.putExtra("uap", uap).putExtra("time", timeStampString) val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags) val saveAction = NotificationCompat.Action.Builder( R.drawable.ic_notification_save, getString(R.string.save), savePendingIntent ).build() // message delete val deleteIntent = Intent(this, BaresipService::class.java) deleteIntent.action = "Message Delete" deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString) val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags) val deleteAction = NotificationCompat.Action.Builder( R.drawable.ic_notification_delete, getString(R.string.delete), deletePendingIntent ).build() nb.addAction(inlineReplyAction).addAction(saveAction).addAction(deleteAction) 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 @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 } } @Suppress("unused") @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() } @Suppress("unused") @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 lowChannel = NotificationChannel(LOW_CHANNEL_ID, "No sound, no vibrate", NotificationManager.IMPORTANCE_LOW) lowChannel.description = "Background status notifications" lowChannel.enableVibration(false) lowChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC nm.createNotificationChannel(lowChannel) val ringAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build() val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "Sound, vibrate, and peek", NotificationManager.IMPORTANCE_HIGH) highChannel.description = "Incoming calls and important alerts" highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC highChannel.enableVibration(true) highChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes) nm.createNotificationChannel(highChannel) val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Sound only", NotificationManager.IMPORTANCE_DEFAULT) mediumChannel.description = "Incoming messages" mediumChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC mediumChannel.enableVibration(false) mediumChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes) nm.createNotificationChannel(mediumChannel) } private fun buildStatusNotification(): Notification { if (VERSION.SDK_INT >= 31) snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE snb.setOngoing(true) snb.setCategory(Notification.CATEGORY_SERVICE) val notification = snb.build() notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT return notification } @SuppressLint("UnspecifiedImmutableFlag") 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 deleteIntent = Intent(this, BaresipService::class.java) .setAction("Notification Dismissed") val dpi = PendingIntent.getService( this, STATUS_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE ) val notificationLayout = RemoteViews(packageName, R.layout.status_notification) snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_notification_b) .setContentIntent(pi) .setDeleteIntent(dpi) .setOngoing(true) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(notificationLayout) val notification = buildStatusNotification() try { if (VERSION.SDK_INT >= 29) startForeground( STATUS_NOTIFICATION_ID, notification, if (VERSION.SDK_INT >= 30) { if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE else FOREGROUND_SERVICE_TYPE_PHONE_CALL } else FOREGROUND_SERVICE_TYPE_PHONE_CALL ) else startForeground(STATUS_NOTIFICATION_ID, notification) } catch (e: Exception) { Log.e(TAG, "Failed to start foreground service: ${e.message}") } } 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.value.size) { notificationLayout.setImageViewResource(resId, uas.value[i].status) notificationLayout.setViewVisibility(resId, View.VISIBLE) } else { notificationLayout.setViewVisibility(resId, View.INVISIBLE) } } if (uas.value.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, buildStatusNotification()) } updatePartialWakeLock() } @SuppressLint("WakelockTimeout") private fun updatePartialWakeLock() { val isAnyUaActive = uasStatus.value.values.any { it != R.drawable.circle_white } val isAnyCallActive = calls.isNotEmpty() val needsToStayAwake = isAnyUaActive || isAnyCallActive if (needsToStayAwake) { if (!partialWakeLock.isHeld) { Log.d(TAG, "Acquiring Partial Wake Lock") partialWakeLock.acquire() } } else { if (partialWakeLock.isHeld) { Log.d(TAG, "Releasing Partial Wake Lock") partialWakeLock.release() } } } private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { Handler(Looper.getMainLooper()).post { Toast.makeText(this@BaresipService.applicationContext, message, length).show() } } private fun startRinging() { am.mode = AudioManager.MODE_RINGTONE rt!!.isLooping = true rt!!.play() if (shouldVibrate()) { val effect = VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE) vbTimer = Timer() vbTimer!!.schedule(object : TimerTask() { override fun run() { if (VERSION.SDK_INT >= 33) { vibrator.vibrate( effect, android.os.VibrationAttributes.Builder() .setUsage(android.os.VibrationAttributes.USAGE_RINGTONE) .build() ) } else { val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build() @Suppress("DEPRECATION") vibrator.vibrate(effect, attributes) } } }, 500L, 2000L) } } private fun shouldStartRinging(callerNumber: String): Boolean { val currentFilter = nm.currentInterruptionFilter if (currentFilter <= NotificationManager.INTERRUPTION_FILTER_ALL) return true val channel = nm.getNotificationChannel(HIGH_CHANNEL_ID) if (channel != null && channel.canBypassDnd()) return true return isStarredContact(callerNumber) } private fun shouldVibrate(): Boolean { // 1. If the phone is in Silent mode, never vibrate if (am.ringerMode == AudioManager.RINGER_MODE_SILENT) return false // 2. If the phone is in Vibrate mode, always vibrate if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) return true // 3. If the phone is in Normal (Ringing) mode: // First, check if the ringer volume is actually non-zero if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false // Finally, check the system "Vibrate for calls" setting. return try { @Suppress("DEPRECATION") Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0 } catch (_: Exception) { // If the setting can't be read, default to FALSE. false } } private fun isStarredContact(callerNumber: String): Boolean { if (contactsMode == "baresip" || callerNumber.isBlank()) return false val phoneUri = Uri.withAppendedPath( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(callerNumber) ) val projection = arrayOf(ContactsContract.PhoneLookup.STARRED) try { val cursor = contentResolver.query(phoneUri, projection, null, null, null) cursor?.use { if (it.moveToFirst()) { val isStarred = it.getInt(0) == 1 if (isStarred) { Log.d(TAG, "Caller '$callerNumber' is a starred contact.") } return isStarred } } } catch (e: Exception) { Log.e(TAG, "Could not query contacts for starred status: ${e.message}") } return false } private fun stopRinging() { rt!!.stop() if (vbTimer != null) { vbTimer!!.cancel() vbTimer = null } } @SuppressLint("DiscouragedApi") private fun playRingBack() { if (mediaPlayer == null) { val name = "ringback_$toneCountry" val resourceId = applicationContext.resources.getIdentifier( name, "raw", applicationContext.packageName) if (resourceId != 0) { mediaPlayer = MediaPlayer.create(this, resourceId) mediaPlayer?.isLooping = true mediaPlayer?.start() } else { Log.e(TAG, "Ringback tone $name.wav not found") } } } @SuppressLint("DiscouragedApi") private fun playBusy() { if (mediaPlayer == null ) { val name = "busy_$toneCountry" val resourceId = applicationContext.resources.getIdentifier( name, "raw", applicationContext.packageName) if (resourceId != 0) { mediaPlayer = MediaPlayer.create(this, resourceId) mediaPlayer?.setOnCompletionListener { stopMediaPlayer() if (!Call.inCall()) { resetCallVolume() abandonAudioFocus(applicationContext) proximitySensing(false) } } mediaPlayer?.start() } else { Log.e(TAG, "Busy tone $name.wav not found") } } } private fun playUnInterrupted(raw: Int, count: Int) { val player = MediaPlayer.create(this, raw) player.setOnCompletionListener { it.stop() it.release() if (count > 1) playUnInterrupted(raw, count - 1) } player.start() } private fun stopMediaPlayer() { mediaPlayer?.stop() mediaPlayer?.release() mediaPlayer = null } private fun setCallVolume() { if (callVolume != 0) for (streamType in listOf(AudioManager.STREAM_MUSIC, AudioManager.STREAM_VOICE_CALL)) { origVolume[streamType] = am.getStreamVolume(streamType) val maxVolume = am.getStreamMaxVolume(streamType) am.setStreamVolume(streamType, (callVolume * 0.1 * maxVolume).roundToInt(), 0) Log.d(TAG, "Orig/new/max $streamType volume is " + "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume") } } private fun resetCallVolume() { if (callVolume != 0) for ((streamType, streamVolume) in origVolume) { am.setStreamVolume(streamType, streamVolume, 0) Log.d(TAG, "Reset $streamType volume to ${am.getStreamVolume(streamType)}") } } @SuppressLint("WakelockTimeout", "Wakelock") private fun proximitySensing(enable: Boolean) { if (enable) { if (!proximityWakeLock.isHeld) { Log.d(TAG, "Acquiring proximity wake lock") proximityWakeLock.acquire() } else { Log.d(TAG, "Proximity wake lock already acquired") } } else { if (proximityWakeLock.isHeld) { proximityWakeLock.release() Log.d(TAG, "Released proximity wake lock") } else { Log.d(TAG, "Proximity wake lock is not held") } } } private fun updateNetwork() { /* for (n in allNetworks) Log.d(TAG, "NETWORK $n with caps ${cm.getNetworkCapabilities(n)} and props " + "${cm.getLinkProperties(n)} is active ${isNetworkActive(n)}") */ updateDnsServers() val addresses = linkAddresses() Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses") var added = 0 for (a in addresses) if (!linkAddresses.containsKey(a.key)) { if (Api.net_add_address_ifname(a.key, a.value) != 0) Log.e(TAG, "Failed to add address: $a") else added++ } var removed = 0 for (a in linkAddresses) if (!addresses.containsKey(a.key)) { if (Api.net_rm_address(a.key) != 0) Log.e(TAG, "Failed to remove address: $a") else removed++ } val active = cm.activeNetwork Log.d(TAG, "Added/Removed/Old/New Active = $added/$removed/$activeNetwork/$active") if (added > 0 || removed > 0 || active != activeNetwork) { linkAddresses = addresses activeNetwork = active Api.uag_reset_transp(register = true, reinvite = true) } Api.net_debug() if (activeNetwork != null) { val caps = cm.getNetworkCapabilities(activeNetwork) if (caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { Log.d(TAG, "Acquiring WiFi Lock") wifiLock.acquire() return } } Log.d(TAG, "Releasing WiFi Lock") wifiLock.release() } private fun linkAddresses(): MutableMap { val addresses = mutableMapOf() for (n in allNetworks) { val caps = cm.getNetworkCapabilities(n) ?: continue if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) { val props = cm.getLinkProperties(n) ?: continue for (la in props.linkAddresses) if (la.scope == OsConstants.RT_SCOPE_UNIVERSE && props.interfaceName != null && la.address.hostAddress != null && afMatch(la.address.hostAddress!!)) addresses[la.address.hostAddress!!] = props.interfaceName!! } } if (hotSpotIsEnabled) { for ((k, v) in hotSpotAddresses) if (afMatch(k)) addresses[k] = v } return addresses } private fun afMatch(address: String): Boolean { return when (addressFamily) { "" -> true "ipv4" -> address.contains(".") else -> address.contains(":") } } private fun updateDnsServers() { if (isServiceRunning && !dynDns) return val servers = mutableListOf() // Use DNS servers first from active network (if available) val activeNetwork = cm.activeNetwork if (activeNetwork != null) { val linkProps = cm.getLinkProperties(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: ${e.message}") } } private fun unRegisterAndroidContactsObserver() { if (androidContactsObserverRegistered) { contentResolver.unregisterContentObserver(androidContactsObserver) androidContactsObserverRegistered = false } } private fun cleanService() { if (!isServiceClean) { if (bluetoothReceiverRegistered) { this.unregisterReceiver(bluetoothReceiver) bluetoothReceiverRegistered = false } if (hotSpotReceiverRegistered) { unregisterReceiver(hotSpotReceiver) hotSpotReceiverRegistered = false } stopRinging() stopMediaPlayer() abandonAudioFocus(applicationContext) uas.value = emptyList() uasStatus.value = emptyMap() callHistory.clear() messages = emptyList() 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::networkCallback.isInitialized) cm.unregisterNetworkCallback(networkCallback) 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) @SuppressLint("MutableCollectionMutableState") companion object { var isServiceRunning = false var isStartReceived = false var isConfigInitialized = false var libraryLoaded = false var callVolume = 0 var speakerPhone = false var audioDelay = if (VERSION.SDK_INT < 31) 1500L else 500L var dynDns = false var filesPath = "" var pName = "" var logLevel = 2 var sipTrace = false var callActionUri = "" var isMainVisible = false var isMicMuted = false var isRecOn = false var toneCountry = "us" var proximitySensing = true val uas = mutableStateOf(emptyList()) val uasStatus = mutableStateOf(emptyMap()) var contacts by mutableStateOf(mutableListOf()) val baresipContacts = mutableStateOf(emptyList()) val androidContacts = mutableStateOf(emptyList()) val contactNames = mutableStateOf(emptyList()) val darkTheme = mutableStateOf(false) val dynamicColors = mutableStateOf(false) var messages by mutableStateOf(emptyList()) val messageUpdate = MutableLiveData() val registrationUpdate = MutableLiveData() val serviceEvent = MutableLiveData>() val serviceEvents = mutableListOf() val calls = ArrayList() var callHistory = ArrayList() var blocked = ArrayList() var contactsMode = "baresip" var addressFamily = "" var dnsServers = listOf() // of those accounts that have auth username without auth password val aorPasswords = mutableMapOf() var audioFocusRequest: AudioFocusRequestCompat? = null var aecAvailable = false private var aec: AcousticEchoCanceler? = null var agcAvailable = false var rt: Ringtone? = null var colorblind = false val circleGreen = mapOf(true to R.drawable.circle_green_blind, false to R.drawable.circle_green) val circleYellow = mapOf(true to R.drawable.circle_yellow_blind, false to R.drawable.circle_yellow) val circleRed = mapOf(true to R.drawable.circle_red_blind, false to R.drawable.circle_red) private var agc: AutomaticGainControl? = null private val nsAvailable = NoiseSuppressor.isAvailable() private var ns: NoiseSuppressor? = null private var btAdapter: BluetoothAdapter? = null private var recorderSessionId = 0 internal const val KEY_TEXT_REPLY = "key_text_reply_baresip" 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}") } } 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(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(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(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(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) } } }