2580 lines
112 KiB
Kotlin
2580 lines
112 KiB
Kotlin
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
|
|
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.DisconnectCause
|
|
import android.telecom.PhoneAccountHandle
|
|
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 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<Int, Int>()
|
|
private var linkAddresses = mutableMapOf<String, String>()
|
|
private var activeNetwork: Network? = null
|
|
private var allNetworks = mutableSetOf<Network>()
|
|
private var hotSpotIsEnabled = false
|
|
private var hotSpotAddresses = mapOf<String, String>()
|
|
private var mediaPlayer: MediaPlayer? = null
|
|
private var androidContactsObserverRegistered = false
|
|
private var hotSpotReceiverRegistered = false
|
|
private var bluetoothReceiverRegistered = false
|
|
private var isNotificationInCall = false
|
|
private var isServiceClean = false
|
|
private var cleanupRunnable: Runnable? = null
|
|
|
|
@SuppressLint("WakelockTimeout")
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
|
|
Log.i(TAG, "BaresipService onCreate")
|
|
instance = this
|
|
|
|
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")
|
|
synchronized(allNetworks) {
|
|
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")
|
|
synchronized(allNetworks) {
|
|
if (network in allNetworks)
|
|
allNetworks.remove(network)
|
|
}
|
|
if (isServiceRunning)
|
|
updateNetwork()
|
|
}
|
|
|
|
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
|
|
super.onCapabilitiesChanged(network, caps)
|
|
synchronized(allNetworks) {
|
|
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")
|
|
synchronized(allNetworks) {
|
|
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
|
|
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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()
|
|
updatePartialWakeLock()
|
|
|
|
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")
|
|
|
|
if (telecom)
|
|
registerPhoneAccount()
|
|
else
|
|
if (btAdapter != null) {
|
|
Log.i(TAG, "Registering bluetooth receiver")
|
|
val filter = IntentFilter()
|
|
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
|
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
|
|
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
|
|
this.registerReceiver(bluetoothReceiver, filter)
|
|
bluetoothReceiverRegistered = true
|
|
}
|
|
|
|
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()
|
|
|
|
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()
|
|
setCallVolume()
|
|
proximitySensing(proximitySensing)
|
|
if (telecom)
|
|
Api.ua_answer(uap, callp, Api.VIDMODE_OFF)
|
|
else {
|
|
Api.ua_answer(uap, callp, Api.VIDMODE_OFF)
|
|
ensureCommunicationMode()
|
|
}
|
|
}
|
|
|
|
"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")
|
|
}
|
|
}
|
|
|
|
"Call Hangup" -> {
|
|
val callp = intent!!.getLongExtra("callp", 0L)
|
|
Log.d(TAG, "onStartCommand Hangup action for $callp")
|
|
val connection = ConnectionService.connections[callp]
|
|
if (connection != null) {
|
|
connection.onDisconnect() // This calls Api.ua_hangup(..., 0, "")
|
|
} else {
|
|
val call = Call.ofCallp(callp)
|
|
if (call != null) Api.ua_hangup(call.ua.uap, callp, 0, "")
|
|
}
|
|
}
|
|
|
|
"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()
|
|
}
|
|
|
|
"Start Call" -> {
|
|
val uap = intent!!.getLongExtra("uap", 0L)
|
|
val uri = intent.getStringExtra("uri")!!
|
|
val conferenceCall = intent.getBooleanExtra("conferenceCall", false)
|
|
val onHoldCallp = intent.getLongExtra("onHoldCallp", 0L)
|
|
if (!requestAudioFocus(applicationContext)) {
|
|
toast(getString(R.string.audio_focus_denied))
|
|
return START_STICKY
|
|
}
|
|
runCall(uap, uri, conferenceCall, onHoldCallp)
|
|
}
|
|
|
|
"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()
|
|
instance = null
|
|
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") {
|
|
val sessionId = ev[1].toInt()
|
|
if (sessionId > 0 && sessionId != recorderSessionId) {
|
|
Log.d(TAG, "got new recorder sessionid $sessionId (was $recorderSessionId)")
|
|
recorderSessionId = sessionId
|
|
aec?.release()
|
|
aec = null
|
|
agc?.release()
|
|
agc = null
|
|
ns?.release()
|
|
ns = null
|
|
if (aecAvailable) {
|
|
try {
|
|
aec = AcousticEchoCanceler.create(sessionId)
|
|
if (aec != null) {
|
|
aec!!.enabled = true
|
|
Log.d(TAG, "AEC is ${if (aec!!.enabled) "enabled" else "not enabled"}")
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Exception creating AEC: $e")
|
|
}
|
|
}
|
|
if (agcAvailable) {
|
|
try {
|
|
agc = AutomaticGainControl.create(sessionId)
|
|
if (agc != null) {
|
|
agc!!.enabled = true
|
|
Log.d(TAG, "AGC is ${if (agc!!.enabled) "enabled" else "not enabled"}")
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Exception creating AGC: $e")
|
|
}
|
|
}
|
|
if (nsAvailable) {
|
|
try {
|
|
ns = NoiseSuppressor.create(sessionId)
|
|
if (ns != null) {
|
|
ns!!.enabled = true
|
|
Log.d(TAG, "NS is ${if (ns!!.enabled) "enabled" else "failed to enable"}")
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Exception creating NS: $e")
|
|
}
|
|
}
|
|
}
|
|
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
|
|
speakerPhone = speakerPhoneAuto
|
|
stopMediaPlayer()
|
|
val hasTelecom = ConnectionService.connections.containsKey(callp) ||
|
|
ConnectionService.pendingOutgoingConnection != null
|
|
if (!hasTelecom) {
|
|
setCallVolume()
|
|
ensureCommunicationMode()
|
|
}
|
|
proximitySensing(proximitySensing)
|
|
}
|
|
"call ringing" -> {
|
|
if (!telecom) ensureCommunicationMode()
|
|
playRingBack()
|
|
return
|
|
}
|
|
"call progress" -> {
|
|
if (!telecom) ensureCommunicationMode()
|
|
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
|
stopMediaPlayer()
|
|
else {
|
|
playRingBack()
|
|
}
|
|
return
|
|
}
|
|
"incoming call" -> {
|
|
speakerPhone = speakerPhoneAuto
|
|
val peerUri = ev[1]
|
|
val toastMsg = if (Call.isAnyCallActive(applicationContext))
|
|
String.format(getString(R.string.call_auto_rejected),
|
|
Utils.friendlyUri(this, peerUri, ua.account))
|
|
else if (ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri)
|
|
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 (toastMsg != "") {
|
|
Log.d(TAG, "Auto-rejecting incoming call to $uap from $peerUri")
|
|
Api.sip_treply(callp, 486, "Busy Here")
|
|
Api.bevent_stop(ev[2].toLong())
|
|
toast(toastMsg)
|
|
if (toastMsg.contains(getString(R.string.call_blocked))) {
|
|
if (ua.account.callHistory)
|
|
Blocked(
|
|
ua.account.aor,
|
|
peerUri,
|
|
"invite",
|
|
GregorianCalendar().timeInMillis
|
|
).add()
|
|
}
|
|
else {
|
|
val name = "callwaiting_$toneCountry"
|
|
val resourceId = resources.getIdentifier(
|
|
name,
|
|
"raw",
|
|
packageName
|
|
)
|
|
if (resourceId != 0) {
|
|
ensureCommunicationMode()
|
|
if (!telecom) 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")
|
|
if (Call.ofCallp(callp) == null)
|
|
Call(callp, ua, peerUri, "in", "incoming").add()
|
|
if (telecom) {
|
|
val extras = android.os.Bundle()
|
|
extras.putLong("uap", uap)
|
|
extras.putLong("callp", callp)
|
|
extras.putString("peerUri", peerUri)
|
|
try {
|
|
tm.addNewIncomingCall(getPhoneAccountHandle(this), extras)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}")
|
|
}
|
|
} else {
|
|
if (!requestAudioFocus(applicationContext)) {
|
|
Log.w(TAG, "Audio focus denied for incoming call")
|
|
Api.ua_hangup(uap, callp, 486, "Busy Here")
|
|
return
|
|
}
|
|
handleIncomingCall(Call.ofCallp(callp)!!)
|
|
}
|
|
return
|
|
}
|
|
"call answered" -> {
|
|
stopMediaPlayer()
|
|
ConnectionService.connections[callp]?.setActive()
|
|
ensureCommunicationMode()
|
|
if (call!!.status.value == "incoming")
|
|
call.status.value = "answered"
|
|
else
|
|
return
|
|
}
|
|
"call redirect" -> {
|
|
stopMediaPlayer()
|
|
}
|
|
"call established" -> {
|
|
ensureCommunicationMode()
|
|
stopMediaPlayer()
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
Log.d(TAG, "AoR $aor call $callp established")
|
|
call!!.status.value = "connected"
|
|
call.onhold = false
|
|
call.startTime = GregorianCalendar()
|
|
updateStatusNotification()
|
|
if (!isMainVisible)
|
|
return
|
|
}
|
|
"call update" -> {
|
|
val newHeldState = when (ev[1].toInt()) {Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true
|
|
else -> false
|
|
}
|
|
val connection = ConnectionService.connections[callp]
|
|
if (call!!.held && !newHeldState) {
|
|
Log.d(TAG, "Call ${call.callp} un-held by peer.")
|
|
call.onhold = false
|
|
// Use a Coroutine with a small delay to let the SIP
|
|
// transaction (the re-INVITE from the peer) finish
|
|
// before trying to hold the other call and resume this one.
|
|
CoroutineScope(Dispatchers.Main).launch {
|
|
delay(100)
|
|
call.resume()
|
|
}
|
|
}
|
|
call.held = newHeldState
|
|
if (newHeldState) {
|
|
// Peer put us on hold
|
|
call.showOnHoldNotice.value = true
|
|
call.callOnHold.value = true
|
|
connection?.setOnHold()
|
|
} else {
|
|
// Peer un-held us
|
|
call.showOnHoldNotice.value = false
|
|
if (!call.onhold) {
|
|
call.callOnHold.value = false
|
|
connection?.setActive()
|
|
}
|
|
}
|
|
if (call.state() == Api.CALL_STATE_EARLY) {
|
|
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
|
stopMediaPlayer()
|
|
else {
|
|
if (!telecom) {
|
|
ConnectionService.connections[callp]?.setRinging()
|
|
playRingBack()
|
|
}
|
|
}
|
|
}
|
|
if (call.status.value == "connected" && !call.held && !call.onhold) {
|
|
if (call.callOnHold.value || call.showOnHoldNotice.value) {
|
|
Log.d(TAG, "Safety guard: Clearing stuck hold flags for ${call.callp}")
|
|
call.callOnHold.value = false
|
|
call.showOnHoldNotice.value = false
|
|
connection?.setActive()
|
|
}
|
|
}
|
|
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]}")
|
|
if (call != null) {
|
|
call.terminated.value = true
|
|
call.remove()
|
|
}
|
|
ConnectionService.lastDisconnectTime = System.currentTimeMillis()
|
|
val connection = ConnectionService.connections[callp]
|
|
if (connection != null) {
|
|
val cause = when {
|
|
ev[1].contains("200") -> DisconnectCause.LOCAL
|
|
ev[1].contains("486") -> DisconnectCause.BUSY
|
|
ev[1].contains("404") -> DisconnectCause.ERROR
|
|
ev[1].contains("403") || ev[1].contains("401") -> DisconnectCause.RESTRICTED
|
|
else -> DisconnectCause.REMOTE
|
|
}
|
|
connection.setDisconnected(DisconnectCause(cause))
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
connection.destroy()
|
|
ConnectionService.connections.remove(callp)
|
|
}, 500)
|
|
}
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
if (call != null) {
|
|
stopRinging()
|
|
stopMediaPlayer()
|
|
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
|
|
}
|
|
val isConference = call.conferenceCall
|
|
val hasOtherCalls = calls.any { it.ua == ua }
|
|
if (calls.isEmpty()) {
|
|
aec?.release()
|
|
aec = null
|
|
agc?.release()
|
|
agc = null
|
|
ns?.release()
|
|
ns = null
|
|
recorderSessionId = 0
|
|
}
|
|
updateStatusNotification()
|
|
if (isConference && !hasOtherCalls) {
|
|
Log.d(TAG, "Last conference call closed, scheduling mixminus unload")
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
if (calls.none { it.conferenceCall }) {
|
|
Log.d(TAG, "Unloading mixminus module")
|
|
Api.module_unload("mixminus")
|
|
}
|
|
}, 1000)
|
|
}
|
|
val reason = ev[1]
|
|
val tone = ev[2]
|
|
if (tone == "busy")
|
|
playBusy()
|
|
else {
|
|
if (!telecom && !Call.inCall())
|
|
abandonAudioFocus(applicationContext)
|
|
ensureCommunicationMode()
|
|
}
|
|
if (!telecom && !Call.inCall())
|
|
abandonAudioFocus(applicationContext)
|
|
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) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
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()
|
|
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
|
|
}
|
|
}
|
|
|
|
private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
|
|
|
|
fun runCall(uap: Long, uri: String, conferenceCall: Boolean, onHoldCallp: Long) {
|
|
|
|
val executeCall = {
|
|
val handler = Handler(Looper.getMainLooper())
|
|
handler.postDelayed({
|
|
val ua = UserAgent.ofUap(uap)
|
|
if (ua != null) {
|
|
if (conferenceCall && ua.calls().isEmpty())
|
|
Api.module_load("mixminus")
|
|
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
|
|
if (callp != 0L) {
|
|
ConnectionService.promoteOutgoingConnection(callp)
|
|
val onHoldCall = Call.ofCallp(onHoldCallp)
|
|
val call = Call(callp, ua, uri, "out", "outgoing")
|
|
call.onHoldCall = onHoldCall
|
|
call.conferenceCall = conferenceCall
|
|
call.add()
|
|
updateStatusNotification()
|
|
if (onHoldCall != null)
|
|
onHoldCall.newCall = call
|
|
if (!call.connect(uri)) {
|
|
Log.w(TAG, "call_connect $callp failed")
|
|
ConnectionService.onCallClosed(callp)
|
|
call.remove()
|
|
call.destroy()
|
|
updateStatusNotification()
|
|
}
|
|
} else {
|
|
ConnectionService.pendingOutgoingConnection?.let {
|
|
it.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
|
|
it.destroy()
|
|
ConnectionService.pendingOutgoingConnection = null
|
|
}
|
|
}
|
|
}
|
|
}, audioDelay)
|
|
}
|
|
|
|
val isTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } ||
|
|
ConnectionService.pendingOutgoingConnection != null
|
|
if (isTelecom)
|
|
executeCall()
|
|
else if (VERSION.SDK_INT < 31) {
|
|
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
|
am.mode = MODE_IN_COMMUNICATION
|
|
executeCall()
|
|
}
|
|
else {
|
|
val isAnyCallMode = am.mode == MODE_IN_COMMUNICATION || am.mode == AudioManager.MODE_IN_CALL
|
|
if (isAnyCallMode) {
|
|
Log.d(TAG, "Audio mode already in a call mode (${am.mode})")
|
|
executeCall()
|
|
}
|
|
else {
|
|
audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
|
|
if (mode == MODE_IN_COMMUNICATION || mode == AudioManager.MODE_IN_CALL) {
|
|
Log.d(TAG, "Audio mode changed to $mode")
|
|
audioModeChangedListener?.let {
|
|
am.removeOnModeChangedListener(it)
|
|
audioModeChangedListener = null
|
|
}
|
|
executeCall()
|
|
}
|
|
}
|
|
am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!)
|
|
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION (waiting for callback)")
|
|
am.mode = MODE_IN_COMMUNICATION
|
|
}
|
|
}
|
|
}
|
|
|
|
@Suppress("unused")
|
|
@Keep
|
|
fun started() {
|
|
Log.d(TAG, "Received 'started' from baresip")
|
|
isNativeReady = true
|
|
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'")
|
|
isNativeReady = false
|
|
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)
|
|
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)
|
|
.setCategory(Notification.CATEGORY_SERVICE)
|
|
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
|
.setCustomContentView(notificationLayout)
|
|
val notification = buildStatusNotification()
|
|
try {
|
|
if (VERSION.SDK_INT >= 29)
|
|
startForeground(STATUS_NOTIFICATION_ID, notification, foregroundServiceType())
|
|
else
|
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to start foreground service: ${e.message}")
|
|
}
|
|
}
|
|
|
|
fun updateStatusNotification() {
|
|
|
|
val activeCall = Call.calls().find {
|
|
it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered"
|
|
}
|
|
|
|
val builder = NotificationCompat.Builder(this, LOW_CHANNEL_ID)
|
|
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)
|
|
|
|
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
.setSmallIcon(R.drawable.ic_notification_b)
|
|
.setContentIntent(pi)
|
|
.setDeleteIntent(dpi)
|
|
.setOngoing(true)
|
|
|
|
if (activeCall != null) {
|
|
val peerUri = activeCall.peerUri
|
|
val caller = Utils.friendlyUri(this, peerUri, activeCall.ua.account)
|
|
val person = Person.Builder().setName(caller).build()
|
|
|
|
val hangupIntent = Intent(this, BaresipService::class.java)
|
|
hangupIntent.action = "Call Hangup"
|
|
hangupIntent.putExtra("callp", activeCall.callp)
|
|
val hpi = PendingIntent.getService(this, REJECT_REQ_CODE, hangupIntent,
|
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
|
|
builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, hpi))
|
|
.setCategory(Notification.CATEGORY_CALL)
|
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
.setWhen(activeCall.startTime?.timeInMillis ?: System.currentTimeMillis())
|
|
.setUsesChronometer(true)
|
|
.setContentText(
|
|
if (activeCall.onhold)
|
|
getString(R.string.call_is_on_hold)
|
|
else
|
|
when (activeCall.status.value) {
|
|
"outgoing", "incoming" -> getString(R.string.call_is_ringing)
|
|
"connected", "answered" -> getString(R.string.call_is_connected)
|
|
else -> getString(R.string.call)
|
|
}
|
|
)
|
|
} else {
|
|
builder.setStyle(null)
|
|
builder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
|
.setCategory(Notification.CATEGORY_SERVICE)
|
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
.setWhen(0)
|
|
.setShowWhen(false)
|
|
.setUsesChronometer(false)
|
|
.setContentTitle("")
|
|
.setContentText("")
|
|
|
|
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
|
|
for (i 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)
|
|
|
|
builder.setCustomContentView(notificationLayout)
|
|
}
|
|
|
|
if (VERSION.SDK_INT >= 31)
|
|
builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
|
|
|
|
builder.setOngoing(true)
|
|
val notification = builder.build()
|
|
notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT
|
|
|
|
try {
|
|
if (activeCall != null) {
|
|
if (VERSION.SDK_INT >= 29)
|
|
startForeground(STATUS_NOTIFICATION_ID, notification, foregroundServiceType())
|
|
else
|
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
|
isNotificationInCall = true
|
|
} else {
|
|
if (isNotificationInCall) {
|
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
isNotificationInCall = false
|
|
if (VERSION.SDK_INT >= 29)
|
|
startForeground(STATUS_NOTIFICATION_ID, notification, foregroundServiceType())
|
|
else
|
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
|
} else {
|
|
// Use standard notify for standby updates.
|
|
// This is much more stable for background registration.
|
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to update foreground notification: ${e.message}")
|
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
|
}
|
|
}
|
|
|
|
private fun foregroundServiceType(): Int {
|
|
var type = 0
|
|
if (VERSION.SDK_INT >= 30) {
|
|
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
}
|
|
if (VERSION.SDK_INT >= 34) {
|
|
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
}
|
|
return type
|
|
}
|
|
|
|
@SuppressLint("WakelockTimeout")
|
|
private fun updatePartialWakeLock() {
|
|
// Hold the wake lock as long as the service is active to ensure
|
|
// native SIP timers (registration, keep-alives) continue to run.
|
|
try {
|
|
if (!partialWakeLock.isHeld) {
|
|
Log.i(TAG, "Acquiring Partial Wake Lock")
|
|
partialWakeLock.acquire()
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Error managing partialWakeLock: ${e.message}")
|
|
}
|
|
}
|
|
|
|
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
|
|
Handler(Looper.getMainLooper()).post {
|
|
Toast.makeText(this@BaresipService.applicationContext, message, length).show()
|
|
}
|
|
}
|
|
|
|
@SuppressLint("FullScreenIntentPolicy")
|
|
fun handleIncomingCall(call: Call) {
|
|
val ua = call.ua
|
|
val peerUri = call.peerUri
|
|
val callp = call.callp
|
|
|
|
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 channelId = HIGH_CHANNEL_ID
|
|
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
|
|
}
|
|
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())
|
|
}
|
|
|
|
postServiceEvent(ServiceEvent("call incoming", arrayListOf(ua.uap, callp), System.nanoTime()))
|
|
}
|
|
|
|
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.
|
|
// Although deprecated, it is the standard way to check the "Also vibrate for calls" toggle.
|
|
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 = resources.getIdentifier(name, "raw", packageName)
|
|
if (resourceId != 0) {
|
|
val audioAttributes = AudioAttributes.Builder()
|
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
.build()
|
|
|
|
mediaPlayer = MediaPlayer().apply {
|
|
setAudioAttributes(audioAttributes)
|
|
val afd = resources.openRawResourceFd(resourceId)
|
|
setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
|
|
afd.close()
|
|
isLooping = true
|
|
prepare()
|
|
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 = resources.getIdentifier(name, "raw", packageName)
|
|
if (resourceId != 0) {
|
|
val audioAttributes = AudioAttributes.Builder()
|
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
.build()
|
|
|
|
mediaPlayer = MediaPlayer().apply {
|
|
setAudioAttributes(audioAttributes)
|
|
val afd = resources.openRawResourceFd(resourceId)
|
|
setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
|
|
afd.close()
|
|
setOnCompletionListener {
|
|
stopMediaPlayer()
|
|
ensureCommunicationMode()
|
|
}
|
|
prepare()
|
|
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)}")
|
|
}
|
|
}
|
|
|
|
private fun ensureCommunicationMode() {
|
|
val currentMode = am.mode
|
|
val isAnyCallMode = currentMode == MODE_IN_COMMUNICATION || currentMode == AudioManager.MODE_IN_CALL
|
|
|
|
val isSpeakerphoneOn = if (VERSION.SDK_INT >= 31)
|
|
am.communicationDevice?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
|
else {
|
|
@Suppress("DEPRECATION")
|
|
am.isSpeakerphoneOn
|
|
}
|
|
|
|
if (Call.inCall() && isAnyCallMode) {
|
|
cleanupRunnable?.let {
|
|
Log.d(TAG, "Canceling pending speakerphone cleanup because call is active")
|
|
Handler(Looper.getMainLooper()).removeCallbacks(it)
|
|
cleanupRunnable = null
|
|
}
|
|
if (isSpeakerphoneOn == speakerPhone) {
|
|
val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) }
|
|
if (hasTelecom || currentMode == MODE_IN_COMMUNICATION) {
|
|
Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.")
|
|
return
|
|
}
|
|
}
|
|
} else if (!Call.inCall() && currentMode == MODE_NORMAL) {
|
|
if (speakerPhone) {
|
|
Log.d(TAG, "Resetting speakerPhone UI state while idle")
|
|
speakerPhone = false
|
|
postServiceEvent(
|
|
ServiceEvent(
|
|
"speaker update,false",
|
|
arrayListOf(0L, 0L),
|
|
System.nanoTime()
|
|
)
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
Log.d(TAG, "Scheduling ensureCommunicationMode (current: $currentMode) in 500ms (target speaker: $speakerPhone)")
|
|
val handler = Handler(Looper.getMainLooper())
|
|
cleanupRunnable?.let { handler.removeCallbacks(it) }
|
|
|
|
val runnable = Runnable {
|
|
cleanupRunnable = null
|
|
if (Call.inCall()) {
|
|
val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) }
|
|
if (!hasTelecom && am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) {
|
|
am.mode = MODE_IN_COMMUNICATION
|
|
Log.d(TAG, "Manual Mode Guard: Setting MODE_IN_COMMUNICATON from ${am.mode}")
|
|
}
|
|
Log.d(TAG, "Applying speakerphone state: $speakerPhone")
|
|
if (!hasTelecom) {
|
|
Log.d(TAG, "No Telecom connection, using AudioManager for speaker")
|
|
Utils.setSpeakerPhone(mainExecutor, am, speakerPhone)
|
|
} else {
|
|
for (c in Call.calls()) {
|
|
ConnectionService.setOutput(c.callp, speakerPhone)
|
|
}
|
|
}
|
|
} else {
|
|
if (am.mode != MODE_NORMAL) {
|
|
am.mode = MODE_NORMAL
|
|
Log.d(TAG, "Manual Mode Guard: Resetting to MODE_NORMAL")
|
|
Utils.clearCommunicationDevice(am)
|
|
}
|
|
if (speakerPhone) {
|
|
Log.d(TAG, "Resetting speakerPhone runtime state after call")
|
|
speakerPhone = false
|
|
postServiceEvent(
|
|
ServiceEvent(
|
|
"speaker update,false",
|
|
arrayListOf(0L, 0L),
|
|
System.nanoTime()
|
|
)
|
|
)
|
|
}
|
|
if (isMicMuted) {
|
|
isMicMuted = false
|
|
postServiceEvent(
|
|
ServiceEvent(
|
|
"mic muted,false",
|
|
arrayListOf(0L, 0L),
|
|
System.nanoTime()
|
|
)
|
|
)
|
|
}
|
|
if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) })
|
|
resetCallVolume()
|
|
proximitySensing(false)
|
|
}
|
|
}
|
|
cleanupRunnable = runnable
|
|
handler.postDelayed(runnable, 500)
|
|
}
|
|
|
|
@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() {
|
|
if (!isNativeReady) return
|
|
|
|
val dnsChanged = updateDnsServers()
|
|
|
|
val addresses = linkAddresses()
|
|
if (linkAddresses != addresses)
|
|
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
|
|
if (added != removed || activeNetwork != active || dnsChanged)
|
|
Log.d(TAG, "Added/Removed = $added/$removed Old/New Active = $activeNetwork/$active DNS Changed = $dnsChanged")
|
|
|
|
if (added > 0 || removed > 0 || active != activeNetwork || dnsChanged) {
|
|
Api.net_debug()
|
|
linkAddresses = addresses
|
|
activeNetwork = active
|
|
Api.uag_reset_transp(register = true, reinvite = true)
|
|
}
|
|
|
|
val hasWifi = allNetworks.any { network ->
|
|
val caps = cm.getNetworkCapabilities(network)
|
|
caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|
}
|
|
|
|
if (hasWifi) {
|
|
if (!wifiLock.isHeld) {
|
|
Log.d(TAG, "Acquiring WiFi Lock")
|
|
wifiLock.acquire()
|
|
}
|
|
}
|
|
else {
|
|
if (wifiLock.isHeld) {
|
|
Log.d(TAG, "Releasing WiFi Lock")
|
|
wifiLock.release()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun linkAddresses(): MutableMap<String, String> {
|
|
val addresses = mutableMapOf<String, String>()
|
|
synchronized(allNetworks) {
|
|
for (n in allNetworks) {
|
|
val caps = cm.getNetworkCapabilities(n) ?: continue
|
|
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND) ||
|
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
|
) {
|
|
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(): Boolean {
|
|
if (isServiceRunning && !dynDns)
|
|
return false
|
|
val servers = mutableListOf<InetAddress>()
|
|
// 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
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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 registerPhoneAccount() {
|
|
val phoneAccountHandle = getPhoneAccountHandle(this)
|
|
val phoneAccount = android.telecom.PhoneAccount.builder(phoneAccountHandle, getString(R.string.app_name))
|
|
.setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP)
|
|
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL)
|
|
.build()
|
|
tm.registerPhoneAccount(phoneAccount)
|
|
}
|
|
|
|
private fun cleanService() {
|
|
if (!isServiceClean) {
|
|
try {
|
|
if (hotSpotReceiverRegistered) {
|
|
applicationContext.unregisterReceiver(hotSpotReceiver)
|
|
hotSpotReceiverRegistered = false
|
|
}
|
|
} catch (_: IllegalArgumentException) {
|
|
Log.e(TAG, "hotSpotReceiver was not registered with applicationContext")
|
|
}
|
|
if (bluetoothReceiverRegistered) {
|
|
try {
|
|
unregisterReceiver(bluetoothReceiver)
|
|
bluetoothReceiverRegistered = false
|
|
} catch (_: IllegalArgumentException) {}
|
|
}
|
|
val callps = ConnectionService.connections.keys.toList()
|
|
for (callp in callps)
|
|
ConnectionService.onCallClosed(callp)
|
|
ConnectionService.pendingOutgoingConnection?.let {
|
|
it.setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
|
it.destroy()
|
|
ConnectionService.pendingOutgoingConnection = null
|
|
}
|
|
stopRinging()
|
|
stopMediaPlayer()
|
|
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 {
|
|
|
|
private const val TAG = "BaresipService"
|
|
|
|
var instance: BaresipService? = null
|
|
var isServiceRunning = false
|
|
var isNativeReady = false
|
|
var isStartReceived = false
|
|
var isConfigInitialized = false
|
|
var libraryLoaded = false
|
|
var callVolume = 0
|
|
|
|
@Volatile
|
|
var speakerPhone = false
|
|
var speakerPhoneAuto = 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
|
|
var telecom = true
|
|
|
|
val uas = mutableStateOf(emptyList<UserAgent>())
|
|
val uasStatus = mutableStateOf(emptyMap<String, Int>())
|
|
var contacts by mutableStateOf(mutableListOf<Contact>())
|
|
val baresipContacts = mutableStateOf(emptyList<Contact.BaresipContact>())
|
|
val androidContacts = mutableStateOf(emptyList<Contact.AndroidContact>())
|
|
val contactNames = mutableStateOf(emptyList<String>())
|
|
|
|
val darkTheme = mutableStateOf(false)
|
|
val dynamicColors = mutableStateOf(false)
|
|
var messages by mutableStateOf(emptyList<Message>())
|
|
val messageUpdate = MutableLiveData<Long>()
|
|
val registrationUpdate = MutableLiveData<Long>()
|
|
|
|
val serviceEvent = MutableLiveData<Event<Long>>()
|
|
val serviceEvents = mutableListOf<ServiceEvent>()
|
|
|
|
val calls = ArrayList<Call>()
|
|
var callHistory = ArrayList<CallHistoryNew>()
|
|
var blocked = ArrayList<Blocked>()
|
|
|
|
var contactsMode by mutableStateOf("baresip")
|
|
var addressFamily = ""
|
|
var dnsServers = listOf<InetAddress>()
|
|
|
|
// <aor, password> of those accounts that have auth username without auth password
|
|
val aorPasswords = mutableMapOf<String, String>()
|
|
var aecAvailable = false
|
|
var agcAvailable = false
|
|
var nsAvailable = false
|
|
private var aec: AcousticEchoCanceler? = null
|
|
private var agc: AutomaticGainControl? = null
|
|
private var ns: NoiseSuppressor? = null
|
|
private var recorderSessionId = 0
|
|
private var btAdapter: BluetoothAdapter? = null
|
|
private var audioFocusRequest: androidx.media.AudioFocusRequestCompat? = null
|
|
|
|
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
|
|
)
|
|
|
|
internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
|
|
private const val PHONE_ACCOUNT_ID = "baresip_phone_account"
|
|
|
|
fun getPhoneAccountHandle(ctx: Context): PhoneAccountHandle {
|
|
val componentName = android.content.ComponentName(ctx, ConnectionService::class.java)
|
|
return PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID)
|
|
}
|
|
|
|
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 = androidx.media.AudioAttributesCompat.Builder()
|
|
.setUsage(androidx.media.AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
|
|
.setContentType(androidx.media.AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
|
.build()
|
|
audioFocusRequest =
|
|
androidx.media.AudioFocusRequestCompat.Builder(androidx.media.AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
|
.setAudioAttributes(attributes)
|
|
.setOnAudioFocusChangeListener { }
|
|
.build()
|
|
if (androidx.media.AudioManagerCompat.requestAudioFocus(
|
|
am,
|
|
audioFocusRequest!!
|
|
) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
) {
|
|
Log.d(TAG, "requestAudioFocus granted")
|
|
if (!telecom && 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 (androidx.media.AudioManagerCompat.abandonAudioFocusRequest(
|
|
am,
|
|
audioFocusRequest!!
|
|
) ==
|
|
AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
) {
|
|
audioFocusRequest = null
|
|
if (!telecom && 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)
|
|
}
|
|
}
|
|
}
|