Files
2021-07-19 14:47:44 +03:00

1241 lines
55 KiB
Kotlin

package com.tutpro.baresip
import android.Manifest
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.*
import android.app.PendingIntent.getActivity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHeadset
import android.content.*
import android.media.*
import android.net.*
import android.net.wifi.WifiManager
import android.os.*
import android.os.Build.VERSION
import android.provider.Settings
import android.telephony.TelephonyManager
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.RemoteViews
import androidx.annotation.ColorRes
import androidx.annotation.Keep
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import java.io.File
import java.net.InetAddress
import java.nio.charset.StandardCharsets
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class BaresipService: Service() {
internal lateinit var intent: Intent
internal lateinit var am: AudioManager
internal 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 tm: TelephonyManager
private lateinit var partialWakeLock: PowerManager.WakeLock
private lateinit var proximityWakeLock: PowerManager.WakeLock
private lateinit var fl: WifiManager.WifiLock
private lateinit var br: BroadcastReceiver
internal var rtTimer: Timer? = null
private var audioFocusRequest: AudioFocusRequest? = null
private var audioFocusUsage = -1
private var origVolume = -1
private val btAdapter = BluetoothAdapter.getDefaultAdapter()
internal var activeNetwork = ""
@SuppressLint("WakelockTimeout")
override fun onCreate() {
Log.d(TAG, "At onCreate")
intent = Intent("com.tutpro.baresip.EVENT")
intent.setPackage("com.tutpro.baresip")
filesPath = filesDir.absolutePath
am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// Hack to avoid playback delay when outgoing call is established
am.mode = AudioManager.MODE_IN_COMMUNICATION
val rtUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext,
RingtoneManager.TYPE_RINGTONE)
rt = RingtoneManager.getRingtone(applicationContext, rtUri)
val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext,
RingtoneManager.TYPE_NOTIFICATION)
nt = RingtoneManager.getRingtone(applicationContext, ntUri)
nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannels()
snb = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
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.i(TAG, "Network $network is available")
updateNetwork()
}
override fun onLost(network: Network) {
super.onLost(network)
Log.i(TAG, "Network $network is lost")
if (activeNetwork == "$network")
updateNetwork()
}
override fun onLinkPropertiesChanged(network: Network, props: LinkProperties) {
super.onLinkPropertiesChanged(network, props)
Log.i(TAG, "Network $network link properties changed")
if (activeNetwork == "$network")
updateNetwork()
}
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
super.onCapabilitiesChanged(network, caps)
Log.i(TAG, "Network $network capabilities changed: $caps")
}
}
)
pm = getSystemService(Context.POWER_SERVICE) as PowerManager
tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// 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()
}
}
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
"com.tutpro.baresip:proximity_wakelog")
br = 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 (isAudioFocused()) {
// Without delay, SCO_AUDIO_STATE_CONNECTING ->
// SCO_AUDIO_STATE_DISCONNECTED
Timer("Sco", false).schedule(1000) {
Log.d(TAG, "Starting Bluetooth SCO")
am.startBluetoothSco()
}
}
}
BluetoothHeadset.STATE_DISCONNECTED -> {
Log.d(TAG, "Bluetooth headset is disconnected")
if (am.isBluetoothScoOn) {
Log.d(TAG, "Stopping Bluetooth SCO")
am.stopBluetoothSco()
}
}
}
}
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")
if (am.isBluetoothScoOn) {
Log.d(TAG, "Stopping Bluetooth SCO")
am.stopBluetoothSco()
}
}
}
}
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")
abandonAudioFocus()
}
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(br, filter)
}
super.onCreate()
}
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" -> {
val wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
fl = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip")
val assets = arrayOf("accounts", "config", "contacts", "busy.wav", "callwaiting.wav",
"error.wav", "notfound.wav", "ring.wav", "ringback.wav")
var file = File(filesPath)
if (!file.exists()) {
Log.d(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()) {
Log.d(TAG, "Copying asset '$a'")
Utils.copyAssetToFile(applicationContext, a, "$filesPath/$a")
} else {
Log.d(TAG, "Asset '$a' already copied")
}
if (a == "config")
Config.initialize()
}
if (File(filesDir, "history").exists())
File(filesDir, "history").renameTo(File(filesDir, "calls"))
Contact.restore()
CallHistory.restore()
Message.restore()
val ipV4Addr = Utils.findIpV4Address(linkAddresses)
val ipV6Addr = Utils.findIpV6Address(linkAddresses)
val dnsServers = Utils.findDnsServers(BaresipService.dnsServers)
if ((ipV4Addr == "") && (ipV6Addr == ""))
Log.w(TAG, "Starting baresip without IP addresses")
Thread {
baresipStart(
filesPath, ipV4Addr, ipV6Addr, "", dnsServers, Api.AF_UNSPEC, logLevel
)
}.start()
isServiceRunning = true
showStatusNotification()
if (AccountsActivity.noAccounts()) {
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", "accounts")
startActivity(newIntent)
}
}
"Call Show", "Call Answer" -> {
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", action.lowercase(Locale.ROOT))
newIntent.putExtra("callp", intent!!.getStringExtra("callp"))
startActivity(newIntent)
}
"Call Missed" -> {
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", action.lowercase(Locale.ROOT))
newIntent.putExtra("uap", intent!!.getStringExtra("uap"))
startActivity(newIntent)
}
"Call Reject" -> {
val callp = intent!!.getStringExtra("callp")!!
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")
Api.ua_hangup(call.ua.uap, callp, 486, "Rejected")
if (call.ua.account.callHistory) {
CallHistory.add(CallHistory(aor, peerUri, "in", false))
CallHistory.save()
}
}
}
"Transfer Show", "Transfer Accept" -> {
val uap = intent!!.getStringExtra("uap")!!
val ua = UserAgent.ofUap(uap)
if (ua == null) {
Log.w(TAG, "onStartCommand did not find ua $uap")
} 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", action.lowercase(Locale.ROOT))
newIntent.putExtra("callp", intent.getStringExtra("callp"))
newIntent.putExtra("uri", intent.getStringExtra("uri"))
startActivity(newIntent)
nm.cancel(TRANSFER_NOTIFICATION_ID)
}
}
"Transfer Deny" -> {
val callp = intent!!.getStringExtra("callp")!!
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 Show", "Message Reply" -> {
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", action.lowercase(Locale.ROOT))
newIntent.putExtra("uap", intent!!.getStringExtra("uap"))
newIntent.putExtra("peer", intent.getStringExtra("peer"))
startActivity(newIntent)
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
"Message Save" -> {
val uap = intent!!.getStringExtra("uap")!!
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!!.getStringExtra("uap")!!
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)
}
"UpdateNotification" -> {
updateStatusNotification()
}
"Stop", "Stop Force" -> {
if (!isServiceClean) cleanService()
if (isServiceRunning) baresipStop(action == "Stop Force")
}
"Kill" -> {
if (!isServiceClean) cleanService()
isServiceRunning = false
stopForeground(true)
stopSelf()
}
}
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
Log.d(TAG, "At Baresip Service onDestroy")
super.onDestroy()
this.unregisterReceiver(br)
if (am.isBluetoothScoOn) am.stopBluetoothSco()
cleanService()
if (isServiceRunning) {
sendBroadcast(Intent("com.tutpro.baresip.Restart"))
}
}
@Keep
fun uaAdd(uap: String) {
val ua = UserAgent(uap)
Log.d(TAG, "uaAdd ${ua.account.aor} at BaresipService")
uas.add(ua)
if (ua.account.preferIPv6Media)
Api.account_set_mediaaf(ua.account.accp, Api.AF_INET6)
if (Api.ua_isregistered(uap)) {
Log.d(TAG, "Ua ${ua.account.aor} is registered")
status.add(R.drawable.dot_green)
} else {
Log.d(TAG, "Ua ${ua.account.aor} is NOT registered")
if (ua.account.regint == 0)
status.add(R.drawable.dot_white)
else
status.add(R.drawable.dot_yellow)
}
}
@Keep
fun uaEvent(event: String, uap: String, callp: String) {
if (!isServiceRunning) return
val ua = UserAgent.ofUap(uap)
if (ua == null) {
Log.w(TAG, "uaEvent did not find ua $uap")
return
}
Log.d(TAG, "got uaEvent $event/${ua.account.aor}/$callp")
val aor = ua.account.aor
var newEvent: String? = null
val ev = event.split(",")
for (account_index in uas.indices) {
if (uas[account_index].account.aor == aor) {
when (ev[0]) {
"registering" -> {
return
}
"registered" -> {
if (ua.account.regint == 0)
status[account_index] = R.drawable.dot_white
else
status[account_index] = R.drawable.dot_green
ua.registrationFailed = false
updateStatusNotification()
if (!Utils.isVisible())
return
}
"registering failed" -> {
status[account_index] = R.drawable.dot_red
updateStatusNotification()
if ((ev.size > 1) && (ev[1] == "Invalid argument")) {
// Most likely this error is due to DNS lookup failure
newEvent = "registering failed,DNS lookup failed"
Api.net_dns_debug()
if (dynDns)
if (VERSION.SDK_INT >= 23) {
val activeNetwork = cm.activeNetwork
if (activeNetwork != null) {
val props = cm.getLinkProperties(activeNetwork)
if (props != null) {
val dnsServers = props.dnsServers
Log.d(TAG, "Updating DNS Servers = $dnsServers")
if (Config.updateDnsServers(dnsServers) != 0) {
Log.w(TAG, "Failed to update DNS servers '$dnsServers'")
} else {
Api.net_dns_debug()
if (!ua.registrationFailed) {
ua.registrationFailed = true
Api.ua_register(uap)
}
}
}
} else {
Log.d(TAG, "No active network!")
}
}
}
if ((ev.size > 1) && (ev[1] == "Software caused connection abort")) {
// Perhaps due to VPN connect/disconnect
updateNetwork()
}
if (!Utils.isVisible())
return
}
"unregistering" -> {
status[account_index] = R.drawable.dot_white
updateStatusNotification()
if (!Utils.isVisible())
return
}
"call offered" -> {
proximitySensing(true)
return
}
"call progress", "call ringing" -> {
requestAudioFocus(AudioAttributes.USAGE_VOICE_COMMUNICATION,
AudioAttributes.CONTENT_TYPE_SPEECH)
setCallVolume()
return
}
"call incoming" -> {
val peerUri = Api.call_peeruri(callp)
if ((Call.calls().size > 0) ||
(tm.callState != TelephonyManager.CALL_STATE_IDLE) ||
!Utils.checkPermission(applicationContext,
Manifest.permission.RECORD_AUDIO)) {
Log.d(TAG, "Auto-rejecting incoming call $uap/$callp/$peerUri")
Api.ua_hangup(uap, callp, 486, "Busy Here")
if (ua.account.callHistory) {
CallHistory.add(CallHistory(aor, peerUri, "in", false))
CallHistory.save()
ua.account.missedCalls = true
}
if (!Utils.isVisible())
return
newEvent = "call rejected"
} else {
Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
Call(callp, ua, peerUri, "in", "incoming",
Utils.dtmfWatcher(callp)).add()
if (ua.account.answerMode == Api.ANSWERMODE_MANUAL) {
if (VERSION.SDK_INT >= 23) {
Log.d(TAG, "CurrentInterruptionFilter ${nm.currentInterruptionFilter}")
if (nm.currentInterruptionFilter <= NotificationManager.INTERRUPTION_FILTER_ALL)
startRinging()
} else {
startRinging()
}
} else {
val mediaPlayer = MediaPlayer.create(this, R.raw.autoanswer)
mediaPlayer.start()
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(this, BaresipService::class.java)
intent.action = "Call Show"
intent.putExtra("callp", callp)
val pi = PendingIntent.getService(this, CALL_REQ_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val caller = Utils.friendlyUri(ContactsActivity.contactName(peerUri),
Utils.aorDomain(aor))
nb.setSmallIcon(R.drawable.ic_stat)
.setColor(ContextCompat.getColor(this,
R.color.colorBaresip))
.setContentIntent(pi)
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(true)
.setOngoing(true)
.setContentTitle(getString(R.string.incoming_call_from))
.setContentText(caller)
.setShowWhen(true)
.setFullScreenIntent(pi, true)
if (VERSION.SDK_INT < 26) {
@Suppress("DEPRECATION")
nb.setVibrate(LongArray(0))
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.priority = Notification.PRIORITY_HIGH
}
val answerIntent = Intent(this, BaresipService::class.java)
answerIntent.action = "Call Answer"
answerIntent.putExtra("callp", callp)
val answerPendingIntent = PendingIntent.getService(this,
ANSWER_REQ_CODE, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val rejectIntent = Intent(this, BaresipService::class.java)
rejectIntent.action = "Call Reject"
rejectIntent.putExtra("callp", callp)
val rejectPendingIntent = PendingIntent.getService(this,
REJECT_REQ_CODE, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT)
nb.addAction(R.drawable.ic_stat,
getActionText(R.string.answer, R.color.colorGreen),
answerPendingIntent)
nb.addAction(R.drawable.ic_stat,
getActionText(R.string.reject, R.color.colorRed),
rejectPendingIntent)
nm.notify(CALL_NOTIFICATION_ID, nb.build())
return
}
}
"call answered" -> {
stopRinging()
proximitySensing(true)
return
}
"call established" -> {
nm.cancel(CALL_NOTIFICATION_ID)
val call = Call.ofCallp(callp)
if (call == null) {
Log.w(TAG, "Call $callp that is established is not found")
return
}
Log.d(TAG, "AoR $aor call $callp established")
call.status = "connected"
call.onhold = false
if (ua.account.callHistory) {
CallHistory.add(CallHistory(aor, call.peerUri, call.dir, true))
CallHistory.save()
call.hasHistory = true
}
requestAudioFocus(AudioAttributes.USAGE_VOICE_COMMUNICATION,
AudioAttributes.CONTENT_TYPE_SPEECH)
setCallVolume()
if (!Utils.isVisible())
return
}
"call verified", "call secure" -> {
val call = Call.ofCallp(callp)
if (call == null) {
Log.w("Baresip", "Call $callp that is verified is not found")
return
}
if (ev[0] == "call secure") {
call.security = R.drawable.box_yellow
} else {
call.security = R.drawable.box_green
call.zid = ev[1]
}
if (!Utils.isVisible())
return
}
"call transfer" -> {
val call = Call.ofCallp(callp)
if (call == null) {
Log.w(TAG, "Call $callp to be transferred is not found")
return
}
if (!Utils.isVisible()) {
val intent = Intent(this, BaresipService::class.java)
intent.action = "Transfer Show"
intent.putExtra("uap", uap)
.putExtra("callp", callp)
.putExtra("uri", ev[1])
val pi = PendingIntent.getService(this, TRANSFER_REQ_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val target = Utils.friendlyUri(ContactsActivity.contactName(ev[1]),
Utils.aorDomain(aor))
nb.setSmallIcon(R.drawable.ic_stat)
.setColor(ContextCompat.getColor(this, R.color.colorBaresip))
.setContentIntent(pi)
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setContentTitle(getString(R.string.transfer_request_to))
.setContentText(target)
if (VERSION.SDK_INT < 26)
@Suppress("DEPRECATION")
nb.setVibrate(LongArray(0))
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE).priority =
Notification.PRIORITY_HIGH
val acceptIntent = Intent(this, BaresipService::class.java)
acceptIntent.action = "Transfer Accept"
acceptIntent.putExtra("uap", uap)
.putExtra("callp", callp)
.putExtra("uri", ev[1])
val acceptPendingIntent = PendingIntent.getService(this,
ACCEPT_REQ_CODE, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val denyIntent = Intent(this, BaresipService::class.java)
denyIntent.action = "Transfer Deny"
denyIntent.putExtra("callp", callp)
val denyPendingIntent = PendingIntent.getService(this,
DENY_REQ_CODE, denyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
nb.addAction(R.drawable.ic_stat, getString(R.string.accept),
acceptPendingIntent)
nb.addAction(R.drawable.ic_stat, getString(R.string.deny),
denyPendingIntent)
nm.notify(TRANSFER_NOTIFICATION_ID, nb.build())
return
}
}
"call closed" -> {
nm.cancel(CALL_NOTIFICATION_ID)
val call = Call.ofCallp(callp)
if (call == null) {
Log.d(TAG, "AoR $aor call $callp that is closed is not found")
return
}
Log.d(TAG, "AoR $aor call $callp is closed")
stopRinging()
call.remove()
if (Call.calls().size == 0) {
resetCallVolume()
am.isSpeakerphoneOn = false
if (am.isBluetoothScoOn) {
Log.d(TAG, "Stopping Bluetooth SCO")
am.stopBluetoothSco()
} else {
abandonAudioFocus()
}
proximitySensing(false)
}
if (ua.account.callHistory && !call.hasHistory) {
CallHistory.add(CallHistory(aor, call.peerUri, call.dir, false))
CallHistory.save()
if (call.dir == "in") ua.account.missedCalls = true
}
if (!Utils.isVisible() && !call.hasHistory && call.dir == "in") {
val caller = Utils.friendlyUri(ContactsActivity.contactName(call.peerUri),
Utils.aorDomain(aor))
val intent = Intent(this, BaresipService::class.java)
intent.action = "Call Missed"
intent.putExtra("uap", uap)
val pi = PendingIntent.getService(this, CALL_REQ_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
nb.setSmallIcon(R.drawable.ic_stat)
.setColor(ContextCompat.getColor(this,
R.color.colorBaresip))
.setContentIntent(pi)
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(true)
.setContentTitle(getString(R.string.missed_call_from))
.setContentText(caller)
if (VERSION.SDK_INT < 26) {
@Suppress("DEPRECATION")
nb.setVibrate(LongArray(0))
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.priority = Notification.PRIORITY_HIGH
}
nm.notify(CALL_NOTIFICATION_ID, nb.build())
return
}
}
"refer failed" -> {
Log.d(TAG, "AoR $aor hanging up call $callp with ${ev[1]}")
Api.ua_hangup(uap, callp, 0, "")
val call = Call.ofCallp(callp)
if (call == null) {
Log.w(TAG, "Call $callp with failed refer is not found")
} else {
call.referTo = ""
}
if (!Utils.isVisible()) return
}
}
}
}
if (newEvent == null) newEvent = event
val intent = Intent("service event")
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.putExtra("event", newEvent)
intent.putExtra("params", arrayListOf(uap, callp))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@Keep
fun messageEvent(uap: String, peer: String, msg: ByteArray) {
var text = "Decoding of message failed!"
try {
text = String(msg, StandardCharsets.UTF_8)
} catch (e: Exception) {
Log.w(TAG, "UTF-8 decode failed")
}
val ua = UserAgent.ofUap(uap)
if (ua == null) {
Log.w(TAG, "messageEvent did not find ua $uap")
return
}
val timeStamp = System.currentTimeMillis().toString()
Log.d(TAG, "Message event for $uap from $peer at $timeStamp")
Message(ua.account.aor, peer, text, timeStamp.toLong(),
R.drawable.arrow_down_green, 0, "", true).add()
Message.save()
ua.account.unreadMessages = true
if (!Utils.isVisible()) {
val intent = Intent(this, BaresipService::class.java)
intent.action = "Message Show"
intent.putExtra("uap", uap)
.putExtra("peer", peer)
val pi = PendingIntent.getService(this, MESSAGE_REQ_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
val sender = Utils.friendlyUri(ContactsActivity.contactName(peer),
Utils.aorDomain(ua.account.aor))
nb.setSmallIcon(R.drawable.ic_stat)
.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)
if (VERSION.SDK_INT < 26) {
@Suppress("DEPRECATION")
nb.setVibrate(LongArray(0))
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.priority = Notification.PRIORITY_HIGH
}
val replyIntent = Intent(this, BaresipService::class.java)
replyIntent.action = "Message Reply"
replyIntent.putExtra("uap", uap)
.putExtra("peer", peer)
val replyPendingIntent = PendingIntent.getService(this,
REPLY_REQ_CODE, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val saveIntent = Intent(this, BaresipService::class.java)
saveIntent.action = "Message Save"
saveIntent.putExtra("uap", uap)
.putExtra("time", timeStamp)
val savePendingIntent = PendingIntent.getService(this,
SAVE_REQ_CODE, saveIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val deleteIntent = Intent(this, BaresipService::class.java)
deleteIntent.action = "Message Delete"
deleteIntent.putExtra("uap", uap)
.putExtra("time", timeStamp)
val deletePendingIntent = PendingIntent.getService(this,
DELETE_REQ_CODE, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
nb.addAction(R.drawable.ic_stat, "Reply", replyPendingIntent)
nb.addAction(R.drawable.ic_stat, "Save", savePendingIntent)
nb.addAction(R.drawable.ic_stat, "Delete", deletePendingIntent)
nm.notify(MESSAGE_NOTIFICATION_ID, nb.build())
return
} else {
nt.play()
}
val intent = Intent("service event")
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.putExtra("event", "message show")
intent.putExtra("params", arrayListOf(uap, peer))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@Keep
@Suppress("UNUSED")
fun messageResponse(responseCode: Int, responseReason: String, time: String) {
Log.d(TAG, "Message response '$responseCode $responseReason' at $time")
val intent = Intent("message response")
intent.putExtra("response code", responseCode)
intent.putExtra("response reason", responseReason)
intent.putExtra("time", time)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
@Keep
fun getPassword(aor: String): String {
if (!isServiceRunning) return ""
Log.d(TAG, "getPassword of $aor")
return if (MainActivity.aorPasswords[aor] != null)
MainActivity.aorPasswords[aor]!!
else
""
}
@Keep
fun started() {
Log.d(TAG, "Received 'started' from baresip")
val intent = Intent("service event")
intent.putExtra("event", "started")
intent.putExtra("params", arrayListOf(callActionUri))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
callActionUri = ""
}
@Keep
fun stopped(error: String) {
Log.d(TAG, "Received 'stopped' from baresip with param '$error'")
isServiceRunning = false
val intent = Intent("service event")
intent.putExtra("event", "stopped")
intent.putExtra("params", arrayListOf(error))
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
stopForeground(true)
stopSelf()
}
private fun createNotificationChannels() {
if (VERSION.SDK_INT >= 26) {
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)
}
}
private fun showStatusNotification() {
val intent = Intent(this, MainActivity::class.java)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
val pi = getActivity(this, STATUS_REQ_CODE, intent, 0)
snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_stat)
.setContentIntent(pi)
.setOngoing(true)
.setContent(RemoteViews(packageName, R.layout.status_notification))
startForeground(STATUS_NOTIFICATION_ID, snb.build())
}
private fun updateStatusNotification() {
val contentView = RemoteViews(packageName, R.layout.status_notification)
for (i: Int in 0..5) {
val resID = resources.getIdentifier("status$i", "id", packageName)
if (i < status.size) {
contentView.setImageViewResource(resID, status[i])
contentView.setViewVisibility(resID, View.VISIBLE)
} else {
contentView.setViewVisibility(resID, View.INVISIBLE)
}
}
if (status.size > 4)
contentView.setViewVisibility(R.id.etc, View.VISIBLE)
else
contentView.setViewVisibility(R.id.etc, View.INVISIBLE)
snb.setContent(contentView)
nm.notify(STATUS_NOTIFICATION_ID, snb.build())
}
private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable {
val spannable: Spannable = SpannableString(applicationContext.getText(stringRes))
if (VERSION.SDK_INT >= 25) {
spannable.setSpan(
ForegroundColorSpan(applicationContext.getColor(colorRes)),
0, spannable.length, 0)
}
return spannable
}
private fun requestAudioFocus(usage: Int, type: Int) {
if (audioFocusUsage != -1) {
if (audioFocusUsage == usage)
return
else
abandonAudioFocus()
}
if ((VERSION.SDK_INT >= 26) && (audioFocusRequest == null)) {
@TargetApi(26)
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.run {
setAudioAttributes(AudioAttributes.Builder().run {
setUsage(usage)
setContentType(type)
build()
})
build()
}
@TargetApi(26)
if (am.requestAudioFocus(audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audio focus granted for usage $usage")
audioFocusUsage = usage
if (isBluetoothHeadsetConnected() && !am.isBluetoothScoOn) {
Log.d(TAG, "Starting Bluetooth Sco")
am.startBluetoothSco()
}
} else {
Log.d(TAG, "Audio focus denied")
audioFocusRequest = null
audioFocusUsage = -1
}
} else {
@Suppress("DEPRECATION")
if (am.requestAudioFocus(null, usage, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audio focus granted for usage $usage")
audioFocusUsage = usage
if (isBluetoothHeadsetConnected() && !am.isBluetoothScoOn)
am.startBluetoothSco()
} else {
Log.d(TAG, "Audio focus denied")
audioFocusUsage = -1
}
}
}
private fun isBluetoothHeadsetConnected(): Boolean {
return (btAdapter != null) && btAdapter.isEnabled &&
(btAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET) ==
BluetoothHeadset.STATE_CONNECTED)
}
private fun isAudioFocused(): Boolean {
return audioFocusUsage != -1
}
private fun abandonAudioFocus() {
if (VERSION.SDK_INT >= 26) {
if (audioFocusRequest != null) {
if (am.abandonAudioFocusRequest(audioFocusRequest!!) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audio focus abandoned")
audioFocusRequest = null
audioFocusUsage = -1
} else {
Log.d(TAG, "Failed to abandon audio focus")
}
}
} else {
if (audioFocusUsage != -1) {
@Suppress("DEPRECATION")
if (am.abandonAudioFocus(null) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audio focus abandoned")
audioFocusUsage = -1
} else {
Log.d(TAG, "Failed to abandon audio focus")
}
}
}
}
private fun startRinging() {
am.mode = AudioManager.MODE_RINGTONE
requestAudioFocus(AudioAttributes.USAGE_NOTIFICATION_RINGTONE,
AudioAttributes.CONTENT_TYPE_MUSIC)
if (VERSION.SDK_INT >= 28) {
rt.isLooping = true
rt.play()
} else {
rt.play()
rtTimer = Timer()
rtTimer!!.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (!rt.isPlaying) {
rt.play()
}
}
}, 1000, 1000)
}
}
private fun stopRinging() {
if (am.mode == AudioManager.MODE_RINGTONE) {
abandonAudioFocus()
if ((VERSION.SDK_INT < 28) && (rtTimer != null)) {
rtTimer!!.cancel()
rtTimer = null
}
rt.stop()
am.mode = AudioManager.MODE_IN_COMMUNICATION
}
}
private fun setCallVolume() {
if (callVolume != 0 && origVolume == -1) {
val streamType = AudioManager.STREAM_VOICE_CALL
origVolume = am.getStreamVolume(streamType)
am.setStreamVolume(streamType,
(callVolume * 0.1 * am.getStreamMaxVolume(streamType)).roundToInt(),
0)
Log.d(TAG, "Orig/new call volume is $origVolume/${am.getStreamVolume(streamType)}")
}
}
private fun resetCallVolume() {
if (origVolume != -1) {
val streamType = AudioManager.STREAM_VOICE_CALL
am.setStreamVolume(streamType, origVolume, 0)
Log.d(TAG, "Reset volume to ${am.getStreamVolume(streamType)}")
origVolume = -1
}
}
@SuppressLint("WakelockTimeout")
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() {
// Use VPN network if available
for (n in cm.allNetworks) {
val caps = cm.getNetworkCapabilities(n) ?: continue
val props = cm.getLinkProperties(n) ?: continue
if (isNetworkActive(n) && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
Log.i(TAG, "Active VPN network $n is available with caps: " +
"$caps, props: $props")
activeNetwork = "$n"
if (isConfigInitialized) {
Utils.updateLinkProperties(props)
} else {
for (s in props.dnsServers)
Log.i(TAG, "DNS Server ${s.hostAddress}")
dnsServers = props.dnsServers
linkAddresses = props.linkAddresses
}
return
}
}
// Otherwise, use active network with Internet access
for (n in cm.allNetworks) {
val caps = cm.getNetworkCapabilities(n) ?: continue
val props = cm.getLinkProperties(n) ?: continue
if (isNetworkActive(n) && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
Log.i(TAG, "Active Internet network $n is available with caps: " +
"$caps, props: $props")
activeNetwork = "$n"
if (isServiceRunning) {
Utils.updateLinkProperties(props)
} else {
dnsServers = props.dnsServers
linkAddresses = props.linkAddresses
}
return
}
}
// Otherwise, use an active network
for (n in cm.allNetworks) {
val caps = cm.getNetworkCapabilities(n) ?: continue
val props = cm.getLinkProperties(n) ?: continue
if (isNetworkActive(n)) {
Log.i(TAG, "Active network $n is available with caps: " +
"$caps, props: $props")
activeNetwork = "$n"
if (isServiceRunning) {
Utils.updateLinkProperties(props)
} else {
dnsServers = props.dnsServers
linkAddresses = props.linkAddresses
}
return
}
}
}
private fun isNetworkActive(network: Network): Boolean {
if (VERSION.SDK_INT >= 23)
return network == cm.activeNetwork
@Suppress("DEPRECATION")
if ((cm.activeNetworkInfo != null) && (cm.getNetworkInfo(network) != null))
return cm.activeNetworkInfo!!.toString() == cm.getNetworkInfo(network)!!.toString()
return false
}
private fun cleanService() {
abandonAudioFocus()
uas.clear()
status.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::fl.isInitialized && fl.isHeld)
fl.release()
isServiceClean = true
}
private external fun baresipStart(path: String, ipV4Addr: String, ipV6Addr: String,
netInterface: String, dnsServers: String, netAf: Int,
logLevel: Int)
private external fun baresipStop(force: Boolean)
companion object {
const val STATUS_NOTIFICATION_ID = 101
const val CALL_NOTIFICATION_ID = 102
const val TRANSFER_NOTIFICATION_ID = 103
const val MESSAGE_NOTIFICATION_ID = 104
const val STATUS_REQ_CODE = 1
const val CALL_REQ_CODE = 2
const val ANSWER_REQ_CODE = 3
const val REJECT_REQ_CODE = 4
const val TRANSFER_REQ_CODE = 5
const val ACCEPT_REQ_CODE = 6
const val DENY_REQ_CODE = 7
const val MESSAGE_REQ_CODE = 8
const val REPLY_REQ_CODE = 9
const val SAVE_REQ_CODE = 10
const val DELETE_REQ_CODE = 11
const val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default"
const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high"
var isServiceRunning = false
var isConfigInitialized = false
var libraryLoaded = false
var isServiceClean = false
var callVolume = 0
var dynDns = false
var netInterface = ""
var filesPath = ""
var logLevel = 2
var sipTrace = false
var callActionUri = ""
val uas = ArrayList<UserAgent>()
val status = ArrayList<Int>()
val calls = ArrayList<Call>()
var callHistory = ArrayList<CallHistory>()
var messages = ArrayList<Message>()
val contacts = ArrayList<Contact>()
val chatTexts: MutableMap<String, String> = mutableMapOf()
val activities = mutableListOf<String>()
var linkAddresses = listOf<LinkAddress>()
var dnsServers = listOf<InetAddress>()
}
init {
if (!libraryLoaded) {
Log.d(TAG, "Loading baresip library")
System.loadLibrary("baresip")
libraryLoaded = true
}
}
}