After restore, do not show mobile account status as green if baresip is not anymore default phone app
3300 lines
137 KiB
Kotlin
3300 lines
137 KiB
Kotlin
@file:Suppress("DEPRECATION")
|
|
package com.tutpro.baresip
|
|
|
|
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.AudioFocusRequest
|
|
import android.media.AudioManager
|
|
import android.media.AudioManager.MODE_IN_COMMUNICATION
|
|
import android.media.AudioManager.MODE_IN_CALL
|
|
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
|
|
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.Connection
|
|
import android.telecom.DisconnectCause
|
|
import android.telecom.PhoneAccountHandle
|
|
import android.telecom.TelecomManager
|
|
import android.telephony.PhoneStateListener
|
|
import android.telephony.ServiceState
|
|
import android.telephony.TelephonyCallback
|
|
import android.telephony.TelephonyManager
|
|
import android.view.View
|
|
import android.widget.RemoteViews
|
|
import android.widget.Toast
|
|
import androidx.annotation.Keep
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.appcompat.app.AppCompatDelegate
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.setValue
|
|
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.e164Uri
|
|
import com.tutpro.baresip.Utils.toCircle
|
|
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
|
|
import kotlin.time.Duration.Companion.milliseconds
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
|
|
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 telephonyManager: TelephonyManager
|
|
private lateinit var telephonyCallback: TelephonyCallback
|
|
@Suppress("DEPRECATION")
|
|
private lateinit var phoneStateListener: PhoneStateListener
|
|
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 airplaneModeReceiver: BroadcastReceiver
|
|
private lateinit var simStateReceiver: BroadcastReceiver
|
|
private lateinit var androidContactsObserver: ContentObserver
|
|
private lateinit var networkCallback: ConnectivityManager.NetworkCallback
|
|
private lateinit var stopState: String
|
|
private lateinit var quitTimer: CountDownTimer
|
|
|
|
private data class PendingMessage(val sender: String, val body: String, val time: Long)
|
|
private val pendingMessages = mutableListOf<PendingMessage>()
|
|
|
|
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 lastLinkProperties = mutableMapOf<Network, LinkProperties>()
|
|
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 airplaneModeReceiverRegistered = false
|
|
private var simStateReceiverRegistered = false
|
|
private var telephonyCallbackRegistered = false
|
|
private var isNotificationInCall = false
|
|
private var isServiceClean = false
|
|
private var cleanupRunnable: Runnable? = null
|
|
private var previousMobileServiceState = -1
|
|
|
|
@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 = getSystemService(AUDIO_SERVICE) as AudioManager
|
|
|
|
val ntUri = RingtoneManager.getActualDefaultRingtoneUri(this,
|
|
RingtoneManager.TYPE_NOTIFICATION)
|
|
nt = RingtoneManager.getRingtone(this, ntUri)
|
|
|
|
val rtUri = if (Preferences(this).ringtoneUri == "")
|
|
Settings.System.DEFAULT_RINGTONE_URI
|
|
else
|
|
Preferences(this).ringtoneUri!!.toUri()
|
|
rt = RingtoneManager.getRingtone(this, 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 = getSystemService(VIBRATOR_MANAGER_SERVICE) as
|
|
VibratorManager
|
|
vibratorManager.defaultVibrator
|
|
}
|
|
else {
|
|
@Suppress("DEPRECATION")
|
|
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)
|
|
if (Utils.linkPropertiesEqual(props, lastLinkProperties[network])) return
|
|
lastLinkProperties[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 = 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(
|
|
this,
|
|
hotSpotReceiver,
|
|
IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED"),
|
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
)
|
|
hotSpotReceiverRegistered = true
|
|
|
|
airplaneModeReceiver = object : BroadcastReceiver() {
|
|
override fun onReceive(ctx: Context, intent: Intent) {
|
|
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
|
val isAirplaneModeOn = intent.getBooleanExtra("state", false)
|
|
Log.d(TAG, "Airplane mode changed: $isAirplaneModeOn")
|
|
if (VERSION.SDK_INT >= 29)
|
|
updateMobileStatus()
|
|
}
|
|
}
|
|
}
|
|
ContextCompat.registerReceiver(
|
|
this,
|
|
airplaneModeReceiver,
|
|
IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
|
|
ContextCompat.RECEIVER_EXPORTED
|
|
)
|
|
airplaneModeReceiverRegistered = true
|
|
|
|
simStateReceiver = object : BroadcastReceiver() {
|
|
override fun onReceive(ctx: Context, intent: Intent) {
|
|
if (intent.action == "android.intent.action.SIM_STATE_CHANGED") {
|
|
Log.d(TAG, "SIM state changed")
|
|
if (VERSION.SDK_INT >= 29)
|
|
updateMobileStatus()
|
|
}
|
|
}
|
|
}
|
|
ContextCompat.registerReceiver(
|
|
this,
|
|
simStateReceiver,
|
|
IntentFilter("android.intent.action.SIM_STATE_CHANGED"),
|
|
ContextCompat.RECEIVER_EXPORTED
|
|
)
|
|
simStateReceiverRegistered = 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(this@BaresipService, 1000L, 3)
|
|
}
|
|
BluetoothHeadset.STATE_DISCONNECTED -> {
|
|
Log.d(TAG, "Bluetooth headset is disconnected")
|
|
if (audioFocusRequest != null)
|
|
stopBluetoothSco(this@BaresipService)
|
|
}
|
|
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val bluetoothFilter = IntentFilter()
|
|
bluetoothFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
|
bluetoothFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
|
|
bluetoothFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
|
|
ContextCompat.registerReceiver(
|
|
this,
|
|
bluetoothReceiver,
|
|
bluetoothFilter,
|
|
ContextCompat.RECEIVER_EXPORTED
|
|
)
|
|
bluetoothReceiverRegistered = true
|
|
|
|
telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
|
|
if (VERSION.SDK_INT >= 31)
|
|
telephonyCallback = object : TelephonyCallback(), TelephonyCallback.ServiceStateListener {
|
|
override fun onServiceStateChanged(serviceState: ServiceState) {
|
|
updateMobileStatusFromServiceState(serviceState.state)
|
|
}
|
|
}
|
|
else {
|
|
@Suppress("DEPRECATION")
|
|
phoneStateListener = object : PhoneStateListener() {
|
|
@Deprecated("Deprecated in Java")
|
|
override fun onServiceStateChanged(serviceState: ServiceState) {
|
|
updateMobileStatusFromServiceState(serviceState.state)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
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" -> {
|
|
|
|
val sender = intent?.getStringExtra("sender")
|
|
val body = intent?.getStringExtra("body")
|
|
val time = intent?.getLongExtra("time", 0L) ?: 0L
|
|
|
|
if (sender != null && body != null && time != 0L) {
|
|
Handler(Looper.getMainLooper()).post {
|
|
if (isNativeReady) {
|
|
val mobileUa = uas.value.find { it.account.isMobile }
|
|
if (mobileUa != null)
|
|
handleIncomingMessage(mobileUa.uap, sender, body, time)
|
|
else
|
|
synchronized(pendingMessages) {
|
|
pendingMessages.add(PendingMessage(sender, body, time))
|
|
}
|
|
}
|
|
else
|
|
synchronized(pendingMessages) {
|
|
pendingMessages.add(PendingMessage(sender, body, time))
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isStartReceived || isServiceRunning) return START_STICKY
|
|
|
|
showStatusNotification()
|
|
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(this, a, "$filesPath/$a")
|
|
}
|
|
else
|
|
Log.i(TAG, "Asset '$a' already copied")
|
|
if (a == "config")
|
|
Config.initialize(this)
|
|
}
|
|
|
|
Thread {
|
|
if (contactsMode != "android")
|
|
Contact.restoreBaresipContacts()
|
|
if (contactsMode != "baresip") {
|
|
Contact.loadAndroidContacts(this)
|
|
registerAndroidContactsObserver()
|
|
}
|
|
Contact.contactsUpdate()
|
|
|
|
CallHistoryNew.restore()
|
|
Blocked.restore()
|
|
BlockRule.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()
|
|
}.start()
|
|
|
|
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")
|
|
|
|
registerPhoneAccount()
|
|
|
|
Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable")
|
|
|
|
val userAgent = Config.variable("user_agent")
|
|
val software = if (userAgent != "")
|
|
userAgent
|
|
else
|
|
"baresip v${BuildConfig.VERSION_NAME} " +
|
|
"(Android ${VERSION.RELEASE}/${System.getProperty("os.arch") ?: "?"})"
|
|
Thread {
|
|
baresipStart(
|
|
filesPath,
|
|
addresses.removePrefix(";"),
|
|
logLevel,
|
|
software
|
|
)
|
|
}.start()
|
|
|
|
isServiceRunning = true
|
|
|
|
if (linkAddresses.isEmpty())
|
|
toast(getString(R.string.no_network), Toast.LENGTH_LONG)
|
|
|
|
if (!aecAvailable)
|
|
toast(getString(R.string.no_aec), Toast.LENGTH_LONG)
|
|
|
|
if (VERSION.SDK_INT >= 29) {
|
|
val baresipService = Intent(this, BaresipService::class.java)
|
|
baresipService.action = "Check Roles"
|
|
startService(baresipService)
|
|
}
|
|
}
|
|
|
|
"Notification Dismissed" ->
|
|
updateStatusNotification()
|
|
|
|
"Start Content Observer" ->
|
|
registerAndroidContactsObserver()
|
|
|
|
"Stop Content Observer" ->
|
|
unRegisterAndroidContactsObserver()
|
|
|
|
"Call Answer" -> {
|
|
val callp = intent!!.getLongExtra("callp", 0L)
|
|
val call = Call.ofCallp(callp)
|
|
stopRinging()
|
|
stopMediaPlayer()
|
|
setCallVolume()
|
|
proximitySensing(proximitySensing)
|
|
call?.answer()
|
|
updateStatusNotification()
|
|
}
|
|
|
|
"Call Reject" -> {
|
|
val callp = intent!!.getLongExtra("callp", 0L)
|
|
val call = Call.ofCallp(callp)
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
stopRinging()
|
|
stopMediaPlayer()
|
|
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.reject()
|
|
}
|
|
}
|
|
|
|
"Call Hangup" -> {
|
|
val callp = intent!!.getLongExtra("callp", 0L)
|
|
Log.d(TAG, "onStartCommand Hangup action for $callp")
|
|
val call = Call.ofCallp(callp)
|
|
call?.hangup(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()
|
|
|
|
"Check Roles" -> {
|
|
if (isNativeReady)
|
|
addMobileUserAgent()
|
|
}
|
|
|
|
"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(this)) {
|
|
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(this).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)
|
|
if (VERSION.SDK_INT < 29 && ua.account.isMobile) {
|
|
Log.d(TAG, "Removing Mobile account on API < 29")
|
|
CallHistoryNew.clear(ua.account.aor)
|
|
Message.clearMessagesOfAor(ua.account.aor)
|
|
Api.ua_destroy(uap)
|
|
Account.saveAccounts()
|
|
return
|
|
}
|
|
|
|
ua.status = if (ua.account.isMobile) {
|
|
val isAirplaneModeOn = Utils.isAirplaneModeOn(this)
|
|
val isSimReady = isSimReady()
|
|
if (VERSION.SDK_INT >= 29) {
|
|
if (Utils.pstnAccountHandle(this) == null || !isSimReady || isAirplaneModeOn)
|
|
R.drawable.circle_white
|
|
else
|
|
circleRed.getValue(colorblind)
|
|
} else {
|
|
R.drawable.circle_white
|
|
}
|
|
} else 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()
|
|
setCallVolume()
|
|
ensureCommunicationMode()
|
|
proximitySensing(proximitySensing)
|
|
}
|
|
|
|
"call ringing" -> {
|
|
playRingBack()
|
|
return
|
|
}
|
|
|
|
"call progress" -> {
|
|
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
|
stopMediaPlayer()
|
|
else
|
|
playRingBack()
|
|
return
|
|
}
|
|
|
|
"incoming call" -> {
|
|
speakerPhone = speakerPhoneAuto
|
|
val peerUri = ev[1]
|
|
var blockedCall = false
|
|
val toastMsg = if (Call.isAnyCallActive(this))
|
|
String.format(
|
|
getString(R.string.call_auto_rejected),
|
|
Utils.friendlyUri(this, peerUri, ua.account)
|
|
)
|
|
else if (ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri) {
|
|
blockedCall = true
|
|
String.format(
|
|
getString(R.string.call_blocked),
|
|
Utils.friendlyUri(this, peerUri, ua.account)
|
|
)
|
|
}
|
|
else if (ua.account.blockHidden && peerUri.contains("anonymous")) {
|
|
blockedCall = true
|
|
getString(R.string.hidden_call_blocked)
|
|
}
|
|
else if (isBlocked(peerUri)) {
|
|
blockedCall = true
|
|
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 (blockedCall) {
|
|
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()
|
|
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()
|
|
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}")
|
|
}
|
|
return
|
|
}
|
|
|
|
"call answered" -> {
|
|
stopMediaPlayer()
|
|
ConnectionService.connections[callp]?.setActive()
|
|
ensureCommunicationMode()
|
|
Handler(Looper.getMainLooper()).post {
|
|
if (call != null) {
|
|
if (call.status.value == "incoming")
|
|
call.status.value = "answered"
|
|
else
|
|
return@post
|
|
}
|
|
}
|
|
}
|
|
|
|
"call redirect" ->
|
|
stopMediaPlayer()
|
|
|
|
"call established" -> {
|
|
ensureCommunicationMode()
|
|
stopMediaPlayer()
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
Log.d(TAG, "AoR $aor call $callp established")
|
|
Handler(Looper.getMainLooper()).post {
|
|
if (call != null) {
|
|
call.status.value = "connected"
|
|
call.onhold = false
|
|
call.startTime = GregorianCalendar()
|
|
if (call.conferenceCall)
|
|
Api.cmd_exec("conference")
|
|
}
|
|
updateStatusNotification()
|
|
proximitySensing(proximitySensing)
|
|
if (isMicMuted)
|
|
setMicMute(true)
|
|
}
|
|
if (!isMainVisible)
|
|
return
|
|
}
|
|
|
|
"call update" -> {
|
|
Handler(Looper.getMainLooper()).post {
|
|
if (call != null) {
|
|
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.milliseconds)
|
|
call.resume()
|
|
}
|
|
}
|
|
call.held = newHeldState
|
|
if (newHeldState) {
|
|
// Peer put us on hold
|
|
call.showOnHoldNotice.value = true
|
|
call.callOnHold.value = true
|
|
if (connection?.state != Connection.STATE_HOLDING)
|
|
connection?.setOnHold()
|
|
}
|
|
else {
|
|
// Peer un-held us
|
|
call.showOnHoldNotice.value = false
|
|
if (!call.onhold) {
|
|
call.callOnHold.value = false
|
|
if (connection?.state != Connection.STATE_ACTIVE)
|
|
connection?.setActive()
|
|
}
|
|
}
|
|
if (call.state() == Api.CALL_STATE_EARLY)
|
|
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
|
stopMediaPlayer()
|
|
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" -> {
|
|
Handler(Looper.getMainLooper()).post {
|
|
if (call != null) {
|
|
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(this, 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(
|
|
this,
|
|
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(this, 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(
|
|
this,
|
|
ACCEPT_REQ_CODE,
|
|
acceptIntent,
|
|
piFlags
|
|
)
|
|
val denyIntent = Intent(this, BaresipService::class.java)
|
|
denyIntent.action = "Transfer Deny"
|
|
denyIntent.putExtra("callp", callp)
|
|
val denyPendingIntent = PendingIntent.getService(
|
|
this,
|
|
DENY_REQ_CODE,
|
|
denyIntent,
|
|
piFlags)
|
|
nb.addAction(
|
|
R.drawable.ic_notification_call,
|
|
getString(R.string.accept),
|
|
acceptPendingIntent
|
|
)
|
|
nb.addAction(
|
|
R.drawable.ic_notification_call_end,
|
|
getString(R.string.deny),
|
|
denyPendingIntent
|
|
)
|
|
nm.notify(TRANSFER_NOTIFICATION_ID, nb.build())
|
|
return
|
|
}
|
|
}
|
|
|
|
"transfer failed" -> {
|
|
Log.d(TAG, "AoR $aor call $callp transfer failed: ${ev[1]}")
|
|
stopMediaPlayer()
|
|
call!!.referTo = ""
|
|
if (Utils.isVisible())
|
|
toast("${getString(R.string.transfer_failed)}: ${ev[1].trim()}")
|
|
if (!isMainVisible)
|
|
return
|
|
}
|
|
|
|
"call closed" -> {
|
|
Log.d(TAG, "AoR $aor call $callp is closed prm: ${ev[1]}")
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
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))
|
|
connection.safeDestroy()
|
|
ConnectionService.connections.remove(callp)
|
|
}
|
|
if (call != null) {
|
|
call.terminated.value = true
|
|
call.remove()
|
|
val noMoreCalls = synchronized(calls) { !Call.inCall() }
|
|
stopRinging()
|
|
stopMediaPlayer()
|
|
if (noMoreCalls) {
|
|
proximitySensing(false)
|
|
abandonAudioFocus(this)
|
|
}
|
|
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 = synchronized(calls) { calls.any { it.ua == ua } }
|
|
if (noMoreCalls)
|
|
releaseAudioEffects()
|
|
updateStatusNotification()
|
|
if (isConference && !hasOtherCalls) {
|
|
Log.d(TAG, "Last conference call closed, scheduling mixminus unload")
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
val conferenceActive = synchronized(calls) {
|
|
calls.any { it.conferenceCall }
|
|
}
|
|
if (!conferenceActive) {
|
|
Log.d(TAG, "Unloading mixminus module")
|
|
Api.module_unload("mixminus")
|
|
}
|
|
}, 1000)
|
|
}
|
|
val reason = ev[1]
|
|
val tone = ev[2]
|
|
if (tone == "busy")
|
|
playBusy()
|
|
else
|
|
ensureCommunicationMode()
|
|
if (call.dir == "out")
|
|
call.rejected = call.startTime == null &&
|
|
!reason.startsWith("408") &&
|
|
!reason.startsWith("480") &&
|
|
!reason.startsWith("Connection reset by")
|
|
val missed = call.dir == "in" && call.startTime == null && !call.rejected
|
|
if (ua.account.callHistory) {
|
|
val completedElsewhere = missed && ev[2].startsWith("SIP") &&
|
|
ev[2].contains(";cause=200")
|
|
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.milliseconds)
|
|
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(this, 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(
|
|
this,
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
val blockedHidden = ua.account.blockHidden && peerUri.contains("anonymous")
|
|
val blockedUnknown = ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri
|
|
|
|
if (blockedHidden || blockedUnknown) {
|
|
Log.d(TAG, "Auto-rejecting incoming message by $uap from $peerUri")
|
|
Blocked(
|
|
ua.account.aor,
|
|
peerUri,
|
|
"message",
|
|
GregorianCalendar().timeInMillis
|
|
).add()
|
|
toast(if (blockedHidden)
|
|
getString(R.string.hidden_message_blocked)
|
|
else
|
|
String.format(
|
|
getString(R.string.message_blocked),
|
|
Utils.friendlyUri(this, peerUri, ua.account)
|
|
)
|
|
)
|
|
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
|
|
}
|
|
|
|
handleIncomingMessage(uap, peerUri, text, System.currentTimeMillis())
|
|
}
|
|
|
|
@SuppressLint("UnspecifiedImmutableFlag")
|
|
fun handleIncomingMessage(uap: Long, peerUri: String, text: String, timeStamp: Long) {
|
|
|
|
val ua = UserAgent.ofUap(uap)
|
|
if (ua == null) {
|
|
Log.w(TAG, "handleIncomingMessage did not find ua $uap")
|
|
return
|
|
}
|
|
|
|
val aor = ua.account.aor
|
|
|
|
if ((ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri) ||
|
|
(ua.account.blockHidden && peerUri.contains("anonymous")) ||
|
|
isBlocked(peerUri)) {
|
|
Log.d(TAG, "Auto-rejecting blocked message from $peerUri")
|
|
toast(
|
|
if (ua.account.blockHidden && peerUri.contains("anonymous"))
|
|
getString(R.string.hidden_message_blocked)
|
|
else
|
|
String.format(
|
|
getString(R.string.message_blocked),
|
|
Utils.friendlyUri(this, peerUri, ua.account)
|
|
)
|
|
)
|
|
Blocked(
|
|
ua.account.aor,
|
|
peerUri,
|
|
"message",
|
|
GregorianCalendar().timeInMillis
|
|
).add()
|
|
return
|
|
}
|
|
|
|
// Check for duplicates
|
|
val lastMsg = messages.lastOrNull { m -> m.aor == aor }
|
|
if (lastMsg != null && lastMsg.timeStamp == timeStamp && lastMsg.peerUri == peerUri &&
|
|
lastMsg.message == text) {
|
|
Log.d(TAG, "Omit duplicate message from $peerUri")
|
|
return
|
|
}
|
|
|
|
Log.d(TAG, "Message event for $uap from $peerUri at $timeStamp")
|
|
Message(
|
|
aor,
|
|
peerUri,
|
|
text,
|
|
timeStamp,
|
|
MESSAGE_DOWN,
|
|
0,
|
|
"",
|
|
true
|
|
).add()
|
|
ua.account.unreadMessages = true
|
|
|
|
if (!Utils.isVisible()) {
|
|
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
|
|
val intent = Intent(this, 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(
|
|
this,
|
|
MESSAGE_REQ_CODE,
|
|
intent,
|
|
piFlags
|
|
)
|
|
|
|
val sender = createPerson(this, peerUri, ua.account)
|
|
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, sender)
|
|
|
|
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)
|
|
.addPerson(sender)
|
|
|
|
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()
|
|
|
|
val saveIntent = Intent(this, BaresipService::class.java)
|
|
saveIntent.action = "Message Save"
|
|
saveIntent.putExtra("uap", uap)
|
|
.putExtra("time", timeStamp.toString())
|
|
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()
|
|
|
|
val deleteIntent = Intent(this, BaresipService::class.java)
|
|
deleteIntent.action = "Message Delete"
|
|
deleteIntent.putExtra("uap", uap)
|
|
.putExtra("time", timeStamp.toString())
|
|
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 ua = UserAgent.ofUap(uap)
|
|
if (ua != null && ua.account.isMobile && Utils.isUssd(uri)) {
|
|
sendUssd(Utils.uriUserPart(uri))
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if (Call.hasTelecomCall())
|
|
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 == 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 == 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
|
|
|
|
Handler(Looper.getMainLooper()).post {
|
|
addMobileUserAgent()
|
|
if (VERSION.SDK_INT >= 29)
|
|
updateMobileStatus()
|
|
|
|
val mobileUa = uas.value.find { it.account.isMobile }
|
|
if (mobileUa != null)
|
|
synchronized(pendingMessages) {
|
|
for (m in pendingMessages)
|
|
handleIncomingMessage(mobileUa.uap, m.sender, m.body, m.time)
|
|
pendingMessages.clear()
|
|
}
|
|
|
|
// Process any calls that arrived while baresip is starting
|
|
InCallService.instance?.let { inCallService ->
|
|
for (call in inCallService.calls) {
|
|
val aor = call.details.intentExtras?.getString("aor")
|
|
if (Call.ofCallp(call.hashCode().toLong()) == null) {
|
|
Log.d(TAG, "Processing pending external call ${call.hashCode()}")
|
|
handleExternalCall(call, aor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
registerTelephony()
|
|
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()
|
|
}
|
|
|
|
fun registerTelephony() {
|
|
if (VERSION.SDK_INT >= 31 && !telephonyCallbackRegistered)
|
|
try {
|
|
telephonyManager.registerTelephonyCallback(mainExecutor, telephonyCallback)
|
|
telephonyCallbackRegistered = true
|
|
Log.d(TAG, "Registered TelephonyCallback")
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to register TelephonyCallback: ${e.message}")
|
|
}
|
|
else if (!telephonyCallbackRegistered) {
|
|
@Suppress("DEPRECATION")
|
|
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE)
|
|
telephonyCallbackRegistered = true
|
|
Log.d(TAG, "Registered PhoneStateListener")
|
|
}
|
|
}
|
|
|
|
@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(this, MainActivity::class.java)
|
|
.setAction(Intent.ACTION_MAIN)
|
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
|
val pi = PendingIntent.getActivity(
|
|
this,
|
|
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}")
|
|
// Fallback to standard notification if background start is denied
|
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
|
}
|
|
}
|
|
|
|
fun updateStatusNotification() {
|
|
|
|
Handler(Looper.getMainLooper()).post {
|
|
val activeCall = synchronized(calls) {
|
|
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(this, MainActivity::class.java)
|
|
.setAction(Intent.ACTION_MAIN)
|
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
|
|
|
val pi = PendingIntent.getActivity(
|
|
this,
|
|
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 (!isNotificationInCall) {
|
|
// Only call startForeground when the FIRST call starts.
|
|
if (VERSION.SDK_INT >= 29)
|
|
startForeground(
|
|
STATUS_NOTIFICATION_ID,
|
|
notification,
|
|
foregroundServiceType(activeCall)
|
|
)
|
|
else
|
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
|
isNotificationInCall = true
|
|
}
|
|
else
|
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
|
}
|
|
else if (isNotificationInCall) {
|
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
// Only call startForeground to drop the "Call" type when the LAST call ends.
|
|
if (VERSION.SDK_INT >= 29)
|
|
startForeground(STATUS_NOTIFICATION_ID, notification, foregroundServiceType())
|
|
else
|
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
|
isNotificationInCall = false
|
|
}
|
|
else
|
|
// Already in standby, just keep the notification current.
|
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to update notification: ${e.message}")
|
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun foregroundServiceType(activeCall: Call? = null): Int {
|
|
var type = 0
|
|
if (VERSION.SDK_INT >= 29) {
|
|
if (activeCall != null) {
|
|
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
if (VERSION.SDK_INT >= 30) {
|
|
if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)
|
|
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
}
|
|
}
|
|
}
|
|
if (VERSION.SDK_INT >= 34)
|
|
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
// Fallback for API 29-33 when no active call (specialUse not available)
|
|
if (type == 0 && VERSION.SDK_INT >= 29)
|
|
type = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
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}")
|
|
}
|
|
}
|
|
|
|
fun handleExternalCall(telecomCall: android.telecom.Call, preferredAor: String? = null) {
|
|
val rawUri = telecomCall.details.handle?.toString() ?: getString(R.string.unknown)
|
|
val uri = Utils.uriUnescape(rawUri)
|
|
|
|
if (uas.value.isEmpty()) {
|
|
Log.e(TAG, "No User Agents available to handle external call")
|
|
return
|
|
}
|
|
|
|
val ua = preferredAor?.let { UserAgent.ofAor(it) }
|
|
?: uas.value.find { it.account.isMobile }
|
|
?: uas.value[0]
|
|
|
|
val telecomState = if (VERSION.SDK_INT >= 31)
|
|
telecomCall.details.state
|
|
else
|
|
@Suppress("DEPRECATION") telecomCall.state
|
|
|
|
val isIncoming = telecomState == android.telecom.Call.STATE_RINGING
|
|
Log.d(TAG, "Handling external call " +
|
|
"${if (isIncoming) "from" else "to"} $uri (preferredAor=$preferredAor)")
|
|
|
|
val initialStatus = when (telecomState) {
|
|
android.telecom.Call.STATE_RINGING -> "incoming"
|
|
android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing"
|
|
else -> "connected"
|
|
}
|
|
|
|
if (isIncoming) {
|
|
val e164Uri = e164Uri(uri, ua.account.countryCode)
|
|
val presentation = telecomCall.details.handlePresentation
|
|
if (ua.account.blockHidden &&
|
|
(presentation == TelecomManager.PRESENTATION_RESTRICTED ||
|
|
presentation == TelecomManager.PRESENTATION_UNKNOWN)) {
|
|
Log.d(TAG, "Auto-rejecting incoming PSTN hidden call")
|
|
telecomCall.disconnect()
|
|
toast(getString(R.string.hidden_call_blocked))
|
|
if (ua.account.callHistory)
|
|
Blocked(
|
|
ua.account.aor,
|
|
uri,
|
|
"invite",
|
|
GregorianCalendar().timeInMillis
|
|
).add()
|
|
return
|
|
}
|
|
if (ua.account.blockUnknown && Contact.contactName(e164Uri) == e164Uri) {
|
|
Log.d(TAG, "Auto-rejecting incoming PSTN call from $uri")
|
|
telecomCall.disconnect()
|
|
toast(
|
|
String.format(getString(R.string.call_blocked),
|
|
Utils.friendlyUri(this, uri, ua.account))
|
|
)
|
|
if (ua.account.callHistory)
|
|
Blocked(
|
|
ua.account.aor,
|
|
uri,
|
|
"invite",
|
|
GregorianCalendar().timeInMillis
|
|
).add()
|
|
return
|
|
}
|
|
if (isBlocked(uri)) {
|
|
Log.d(TAG, "Auto-rejecting incoming PSTN call from $uri by block rule")
|
|
telecomCall.disconnect()
|
|
toast(
|
|
String.format(getString(R.string.call_blocked),
|
|
Utils.friendlyUri(this, uri, ua.account))
|
|
)
|
|
if (ua.account.callHistory)
|
|
Blocked(
|
|
ua.account.aor,
|
|
uri,
|
|
"invite",
|
|
GregorianCalendar().timeInMillis
|
|
).add()
|
|
return
|
|
}
|
|
}
|
|
|
|
val call = Call.ExternalCall(
|
|
telecomCall,
|
|
ua,
|
|
uri,
|
|
if (telecomState == android.telecom.Call.STATE_RINGING) "in" else "out",
|
|
initialStatus
|
|
)
|
|
|
|
telecomCall.registerCallback(object : android.telecom.Call.Callback() {
|
|
override fun onStateChanged(call: android.telecom.Call, state: Int) {
|
|
super.onStateChanged(call, state)
|
|
val newStatus = when (state) {
|
|
android.telecom.Call.STATE_RINGING -> "incoming"
|
|
android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING ->
|
|
"outgoing"
|
|
android.telecom.Call.STATE_ACTIVE -> "connected"
|
|
android.telecom.Call.STATE_DISCONNECTED, android.telecom.Call.STATE_DISCONNECTING ->
|
|
"closed"
|
|
android.telecom.Call.STATE_HOLDING -> {
|
|
calls.find { it.callp == call.hashCode().toLong() }?.onhold = true
|
|
"connected"
|
|
}
|
|
else -> "connected"
|
|
}
|
|
calls.find { it.callp == call.hashCode().toLong() }?.let {
|
|
if (it.status.value != newStatus) {
|
|
it.status.value = newStatus
|
|
if (newStatus == "connected") {
|
|
it.startTime = GregorianCalendar()
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
updateStatusNotification()
|
|
}
|
|
postServiceEvent(ServiceEvent(
|
|
"call update",
|
|
arrayListOf(it.ua.uap, it.callp),
|
|
System.nanoTime())
|
|
)
|
|
if (newStatus == "closed")
|
|
handleExternalCallRemoved(call)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
synchronized(calls) {
|
|
calls.add(call)
|
|
}
|
|
setCallVolume()
|
|
ensureCommunicationMode()
|
|
|
|
if (isIncoming) {
|
|
handleIncomingCall(call)
|
|
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) {
|
|
Log.d(TAG, "Auto-answering external call ${call.callp}")
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
call.answer()
|
|
}, 2000)
|
|
}
|
|
}
|
|
else
|
|
postServiceEvent(ServiceEvent(
|
|
"call outgoing",
|
|
arrayListOf(ua.uap, call.callp),
|
|
System.nanoTime())
|
|
)
|
|
}
|
|
|
|
fun handleExternalCallRemoved(telecomCall: android.telecom.Call) {
|
|
val callp = telecomCall.hashCode().toLong()
|
|
nm.cancel(CALL_NOTIFICATION_ID)
|
|
val call = calls.find { it.callp == callp }
|
|
if (call != null) {
|
|
val uap = call.ua.uap
|
|
stopRinging()
|
|
stopMediaPlayer()
|
|
if (call.ua.account.callHistory) {
|
|
val historyPeerUri = e164Uri(call.peerUri, call.ua.account.countryCode)
|
|
val history = CallHistoryNew(call.ua.account.aor, historyPeerUri, call.dir)
|
|
history.stopTime = GregorianCalendar()
|
|
history.startTime = call.startTime
|
|
history.rejected = call.rejected
|
|
history.add()
|
|
if (call.dir == "in" && call.startTime == null && !call.rejected)
|
|
call.ua.account.missedCalls = true
|
|
}
|
|
synchronized(calls) {
|
|
calls.remove(call)
|
|
}
|
|
postServiceEvent(
|
|
ServiceEvent("call closed", arrayListOf(uap, callp), System.nanoTime())
|
|
)
|
|
}
|
|
if (!Call.inCall()) {
|
|
proximitySensing(false)
|
|
stopMediaPlayer()
|
|
}
|
|
updateStatusNotification()
|
|
messageUpdate.postValue(System.currentTimeMillis())
|
|
}
|
|
|
|
fun addMobileUserAgent() {
|
|
if (VERSION.SDK_INT >= 29) {
|
|
|
|
val mobileAccountHandle = Utils.pstnAccountHandle(this)
|
|
val existingMobileUa = uas.value.find { it.account.isMobile }
|
|
|
|
if (existingMobileUa == null && mobileAccountHandle == null)
|
|
return
|
|
|
|
val mobileAor = "sip:mobile@pstn"
|
|
|
|
val isAirplaneModeOn = Utils.isAirplaneModeOn(this)
|
|
val isSimReady = isSimReady()
|
|
val status = if (mobileAccountHandle == null || !isSimReady)
|
|
R.drawable.circle_white
|
|
else if (existingMobileUa?.status == circleGreen.getValue(colorblind))
|
|
circleGreen.getValue(colorblind)
|
|
else if (isAirplaneModeOn)
|
|
R.drawable.circle_white
|
|
else
|
|
circleRed.getValue(colorblind)
|
|
|
|
if (existingMobileUa != null) {
|
|
if (existingMobileUa.status != status) {
|
|
Log.d(TAG, "Updating existing Mobile UA status to $status")
|
|
existingMobileUa.updateStatus(status)
|
|
updateStatusNotification()
|
|
if (isMainVisible)
|
|
registrationUpdate.postValue(System.currentTimeMillis())
|
|
}
|
|
return
|
|
}
|
|
|
|
Log.d(TAG, "Injecting new virtual Mobile account: $mobileAor")
|
|
val account = Account(0L, mobileAor)
|
|
account.isMobile = true
|
|
account.nickName = "Mobile"
|
|
account.regint = 0
|
|
account.telProvider = ""
|
|
val mobileUa = UserAgent(0L, account)
|
|
mobileUa.status = status
|
|
|
|
val updatedUas = uas.value.toMutableList()
|
|
updatedUas.add(mobileUa)
|
|
uas.value = updatedUas.toList()
|
|
uasStatus.value = UserAgent.statusMap()
|
|
|
|
registerTelephony()
|
|
Account.saveAccounts()
|
|
CallHistoryNew.save()
|
|
Message.save()
|
|
updateStatusNotification()
|
|
}
|
|
}
|
|
|
|
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
|
|
Handler(Looper.getMainLooper()).post {
|
|
Toast.makeText(this, message, length).show()
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.Q)
|
|
private fun updateMobileStatus(newStatus: Int? = null) {
|
|
uas.value.find { it.account.isMobile }?.let { ua ->
|
|
val isAirplaneModeOn = Utils.isAirplaneModeOn(this)
|
|
val isSimReady = isSimReady()
|
|
val mobileAccountHandle = Utils.pstnAccountHandle(this)
|
|
val status = newStatus ?: if (mobileAccountHandle == null || !isSimReady)
|
|
R.drawable.circle_white
|
|
else if (ua.status == circleGreen.getValue(colorblind))
|
|
ua.status
|
|
else if (isAirplaneModeOn)
|
|
R.drawable.circle_white
|
|
else if (ua.status == R.drawable.circle_white)
|
|
// Show "Red" (not yet in service)
|
|
circleRed.getValue(colorblind)
|
|
else
|
|
ua.status
|
|
if (ua.status != status) {
|
|
Log.d(TAG, "Updating Mobile status to $status")
|
|
ua.updateStatus(status)
|
|
updateStatusNotification()
|
|
if (isMainVisible)
|
|
registrationUpdate.postValue(System.currentTimeMillis())
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun updateMobileStatusFromServiceState(state: Int) {
|
|
if (state == previousMobileServiceState) return
|
|
previousMobileServiceState = state
|
|
if (VERSION.SDK_INT >= 29) {
|
|
val isAirplaneModeOn = Utils.isAirplaneModeOn(this)
|
|
val isSimReady = isSimReady()
|
|
val mobileAccountHandle = Utils.pstnAccountHandle(this)
|
|
val status = if (state == ServiceState.STATE_IN_SERVICE && isSimReady && mobileAccountHandle != null)
|
|
circleGreen.getValue(colorblind)
|
|
else if (mobileAccountHandle == null || isAirplaneModeOn || !isSimReady)
|
|
R.drawable.circle_white
|
|
else
|
|
circleRed.getValue(colorblind)
|
|
Log.d(TAG, "Mobile service state changed: $state, updating status to $status (Airplane=$isAirplaneModeOn)")
|
|
updateMobileStatus(status)
|
|
}
|
|
}
|
|
|
|
@SuppressLint("MissingPermission")
|
|
private fun sendUssd(ussdCode: String) {
|
|
val uap = uas.value.find { it.account.isMobile }?.uap ?: 0L
|
|
val tm = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
|
|
try {
|
|
tm.sendUssdRequest(ussdCode, object : TelephonyManager.UssdResponseCallback() {
|
|
override fun onReceiveUssdResponse(
|
|
telephonyManager: TelephonyManager,
|
|
request: String,
|
|
response: CharSequence
|
|
) {
|
|
super.onReceiveUssdResponse(telephonyManager, request, response)
|
|
Log.d(TAG, "USSD response for $request: $response")
|
|
postServiceEvent(ServiceEvent("ussd response", arrayListOf(uap, request, response.toString()), System.nanoTime()))
|
|
}
|
|
|
|
override fun onReceiveUssdResponseFailed(
|
|
telephonyManager: TelephonyManager,
|
|
request: String,
|
|
failureCode: Int
|
|
) {
|
|
super.onReceiveUssdResponseFailed(telephonyManager, request, failureCode)
|
|
Log.d(TAG, "USSD response for $request failed with code $failureCode")
|
|
val error = when (failureCode) {
|
|
TelephonyManager.USSD_RETURN_FAILURE -> getString(R.string.ussd_failed)
|
|
TelephonyManager.USSD_ERROR_SERVICE_UNAVAIL -> getString(R.string.ussd_service_unavailable)
|
|
else -> getString(R.string.ussd_unknown_error)
|
|
}
|
|
postServiceEvent(ServiceEvent("ussd fail", arrayListOf(uap, request, error), System.nanoTime()))
|
|
}
|
|
}, Handler(Looper.getMainLooper()))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Exception sending USSD request: ${e.message}")
|
|
toast(getString(R.string.ussd_failed))
|
|
}
|
|
}
|
|
|
|
@SuppressLint("FullScreenIntentPolicy")
|
|
fun handleIncomingCall(call: Call) {
|
|
val ua = call.ua
|
|
val peerUri = call.peerUri
|
|
val callp = call.callp
|
|
|
|
val callerNumber = Utils.uriUserPart(peerUri)
|
|
if (call !is Call.ExternalCall && shouldStartRinging(callerNumber))
|
|
startRinging()
|
|
|
|
if (!Utils.isVisible()) {
|
|
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
val intent = Intent(this, 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(
|
|
this,
|
|
CALL_REQ_CODE,
|
|
intent,
|
|
piFlags
|
|
)
|
|
|
|
val diverterUri = call.diverterUri()
|
|
val contentText = if (diverterUri != "")
|
|
"${getString(R.string.is_calling)} (${getString(R.string.diverted_by)} ${Utils.friendlyUri(this, diverterUri, ua.account)})"
|
|
else
|
|
getString(R.string.is_calling)
|
|
val caller = createPerson(this, peerUri, ua.account)
|
|
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
|
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(contentText)
|
|
.setWhen(System.currentTimeMillis())
|
|
.setShowWhen(true)
|
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
.addPerson(caller)
|
|
.setFullScreenIntent(pi, true)
|
|
|
|
val answerIntent = Intent(this, MainActivity::class.java)
|
|
.putExtra("action", "call answer")
|
|
.putExtra("callp", callp)
|
|
val api = PendingIntent.getActivity(
|
|
this,
|
|
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(
|
|
caller,
|
|
rpi,
|
|
api
|
|
))
|
|
nm.notify(CALL_NOTIFICATION_ID, nb.build())
|
|
}
|
|
|
|
postServiceEvent(ServiceEvent(
|
|
"call incoming",
|
|
arrayListOf(ua.uap, callp),
|
|
System.nanoTime()
|
|
))
|
|
}
|
|
|
|
private fun createPerson(ctx: Context, peerUri: String, account: Account): Person {
|
|
val peer = Utils.friendlyUri(ctx, peerUri, account)
|
|
val contact = Contact.findContact(peerUri)
|
|
val contactColor = contact?.color() ?: "#B0B0B0"
|
|
val initial = if (peer.isNotEmpty()) peer.take(1) else "?"
|
|
val textAvatarBitmap = Utils.createTextAvatar(initial, contactColor)
|
|
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
|
|
if (contact is Contact.BaresipContact) {
|
|
if (contact.avatarImage != null)
|
|
icon = IconCompat.createWithBitmap(contact.avatarImage!!.toCircle())
|
|
}
|
|
else if (contact is Contact.AndroidContact)
|
|
if (contact.thumbnailUri != null)
|
|
try {
|
|
val source = ImageDecoder.createSource(contentResolver, contact.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")
|
|
}
|
|
return Person.Builder().setName(peer).setIcon(icon).build()
|
|
}
|
|
|
|
private fun startRinging() {
|
|
am.mode = AudioManager.MODE_RINGTONE
|
|
rt!!.isLooping = true
|
|
rt!!.play()
|
|
if (shouldVibrate()) {
|
|
val effect = VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE)
|
|
vbTimer = Timer()
|
|
vbTimer!!.schedule(object : TimerTask() {
|
|
override fun run() {
|
|
if (VERSION.SDK_INT >= 33)
|
|
vibrator.vibrate(
|
|
effect,
|
|
android.os.VibrationAttributes.Builder()
|
|
.setUsage(android.os.VibrationAttributes.USAGE_RINGTONE)
|
|
.build()
|
|
)
|
|
else {
|
|
val attributes = AudioAttributes.Builder()
|
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
.build()
|
|
@Suppress("DEPRECATION")
|
|
vibrator.vibrate(effect, attributes)
|
|
}
|
|
}
|
|
}, 500L, 2000L)
|
|
}
|
|
}
|
|
|
|
private fun shouldStartRinging(callerNumber: String): Boolean {
|
|
val currentFilter = nm.currentInterruptionFilter
|
|
if (currentFilter <= NotificationManager.INTERRUPTION_FILTER_ALL)
|
|
return true
|
|
val channel = nm.getNotificationChannel(HIGH_CHANNEL_ID)
|
|
if (channel != null && channel.canBypassDnd())
|
|
return true
|
|
return isStarredContact(callerNumber)
|
|
}
|
|
|
|
private fun shouldVibrate(): Boolean {
|
|
// 1. If the phone is in Silent mode, never vibrate
|
|
if (am.ringerMode == AudioManager.RINGER_MODE_SILENT) return false
|
|
// 2. If the phone is in Vibrate mode, always vibrate
|
|
if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) return true
|
|
// 3. If the phone is in Normal (Ringing) mode:
|
|
// First, check if the ringer volume is actually non-zero
|
|
if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false
|
|
// Finally, check the system "Vibrate for calls" setting.
|
|
return try {
|
|
@Suppress("DEPRECATION")
|
|
Settings.System.getInt(
|
|
contentResolver,
|
|
Settings.System.VIBRATE_WHEN_RINGING,
|
|
0
|
|
) != 0
|
|
} catch (_: Exception) {
|
|
// If the setting can't be read, default to FALSE.
|
|
false
|
|
}
|
|
}
|
|
|
|
private fun isStarredContact(callerNumber: String): Boolean {
|
|
if (contactsMode == "baresip" || callerNumber.isBlank())
|
|
return false
|
|
val phoneUri = Uri.withAppendedPath(
|
|
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
|
|
Uri.encode(callerNumber)
|
|
)
|
|
val projection = arrayOf(ContactsContract.PhoneLookup.STARRED)
|
|
try {
|
|
val cursor = contentResolver.query(phoneUri, projection, null, null, null)
|
|
cursor?.use {
|
|
if (it.moveToFirst()) {
|
|
val isStarred = it.getInt(0) == 1
|
|
if (isStarred)
|
|
Log.d(TAG, "Caller '$callerNumber' is a starred contact.")
|
|
return isStarred
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Could not query contacts for starred status: ${e.message}")
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun stopRinging() {
|
|
rt!!.stop()
|
|
if (vbTimer != null) {
|
|
vbTimer!!.cancel()
|
|
vbTimer = null
|
|
}
|
|
}
|
|
|
|
@SuppressLint("DiscouragedApi")
|
|
private fun playRingBack() {
|
|
if (mediaPlayer == null) {
|
|
val name = "ringback_$toneCountry"
|
|
val resourceId = 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 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 == 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) {
|
|
if (Call.hasTelecomCall() || 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.hasTelecomCall()
|
|
if (!hasTelecom && am.mode != MODE_IN_COMMUNICATION && am.mode != 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 (InCallService.instance != null) {
|
|
Log.d(TAG, "Using InCallService for audio route: $speakerPhone")
|
|
@Suppress("DEPRECATION")
|
|
InCallService.instance!!.setAudioRoute(
|
|
if (speakerPhone)
|
|
android.telecom.CallAudioState.ROUTE_SPEAKER
|
|
else
|
|
android.telecom.CallAudioState.ROUTE_EARPIECE
|
|
)
|
|
}
|
|
else 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) {
|
|
setMicMute(false)
|
|
postServiceEvent(
|
|
ServiceEvent(
|
|
"mic muted,false",
|
|
arrayListOf(0L, 0L),
|
|
System.nanoTime()
|
|
)
|
|
)
|
|
}
|
|
if (!Call.hasTelecomCall())
|
|
resetCallVolume()
|
|
}
|
|
}
|
|
cleanupRunnable = runnable
|
|
handler.postDelayed(runnable, 500)
|
|
}
|
|
|
|
fun toggleSpeakerphone() {
|
|
speakerPhone = !speakerPhone
|
|
// Notify the UI to update the icon color
|
|
postServiceEvent(
|
|
ServiceEvent("speaker update,$speakerPhone", arrayListOf(0L, 0L), System.nanoTime())
|
|
)
|
|
// Apply the new audio route
|
|
ensureCommunicationMode()
|
|
}
|
|
|
|
@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
|
|
synchronized(dnsServers) {
|
|
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)
|
|
}
|
|
|
|
fun isSimReady(): Boolean {
|
|
val tm = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
|
|
return tm.simState == TelephonyManager.SIM_STATE_READY
|
|
}
|
|
|
|
private fun cleanService() {
|
|
if (isServiceClean) return
|
|
if (simStateReceiverRegistered) {
|
|
try {
|
|
unregisterReceiver(simStateReceiver)
|
|
} catch (_: IllegalArgumentException) {}
|
|
simStateReceiverRegistered = false
|
|
}
|
|
if (hotSpotReceiverRegistered) {
|
|
try {
|
|
unregisterReceiver(hotSpotReceiver)
|
|
} catch (_: IllegalArgumentException) {}
|
|
hotSpotReceiverRegistered = false
|
|
}
|
|
if (bluetoothReceiverRegistered) {
|
|
try {
|
|
unregisterReceiver(bluetoothReceiver)
|
|
} catch (_: IllegalArgumentException) {}
|
|
bluetoothReceiverRegistered = false
|
|
}
|
|
if (airplaneModeReceiverRegistered) {
|
|
try {
|
|
unregisterReceiver(airplaneModeReceiver)
|
|
} catch (_: IllegalArgumentException) {}
|
|
airplaneModeReceiverRegistered = false
|
|
}
|
|
if (telephonyCallbackRegistered) {
|
|
if (VERSION.SDK_INT >= 31)
|
|
try {
|
|
telephonyManager.unregisterTelephonyCallback(telephonyCallback)
|
|
} catch (_: Exception) {}
|
|
else {
|
|
@Suppress("DEPRECATION")
|
|
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
|
|
}
|
|
telephonyCallbackRegistered = false
|
|
}
|
|
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()
|
|
abandonAudioFocus(this)
|
|
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)
|
|
try {
|
|
cm.unregisterNetworkCallback(networkCallback)
|
|
} catch (_: Exception) {}
|
|
if (androidContactsObserverRegistered) {
|
|
try {
|
|
contentResolver.unregisterContentObserver(androidContactsObserver)
|
|
} catch (_: Exception) {}
|
|
androidContactsObserverRegistered = false
|
|
}
|
|
releaseAudioEffects()
|
|
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
|
|
|
|
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 blockRules = mutableListOf<BlockRule>()
|
|
|
|
fun isBlocked(uri: String): Boolean {
|
|
for (rule in blockRules)
|
|
if (rule.matches(uri)) return true
|
|
return false
|
|
}
|
|
|
|
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: AudioFocusRequest? = null
|
|
|
|
private fun releaseAudioEffects() {
|
|
aec?.enabled = false
|
|
aec?.release()
|
|
aec = null
|
|
agc?.enabled = false
|
|
agc?.release()
|
|
agc = null
|
|
ns?.enabled = false
|
|
ns?.release()
|
|
ns = null
|
|
recorderSessionId = 0
|
|
}
|
|
|
|
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 setMicMute(mute: Boolean) {
|
|
instance?.let {
|
|
val am = it.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
am.isMicrophoneMute = mute
|
|
}
|
|
isMicMuted = mute
|
|
Api.calls_mute(mute)
|
|
}
|
|
|
|
fun postServiceEvent(event: ServiceEvent) {
|
|
synchronized(serviceEvents) {
|
|
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 = AudioAttributes.Builder()
|
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
.build()
|
|
audioFocusRequest =
|
|
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
|
.setAudioAttributes(attributes)
|
|
.setOnAudioFocusChangeListener { }
|
|
.build()
|
|
if (am.requestAudioFocus(audioFocusRequest!!) ==
|
|
AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
Log.d(TAG, "requestAudioFocus granted")
|
|
else {
|
|
Log.w(TAG, "requestAudioFocus denied")
|
|
audioFocusRequest = null
|
|
}
|
|
return audioFocusRequest != null
|
|
}
|
|
|
|
fun abandonAudioFocus(ctx: Context) {
|
|
if (audioFocusRequest != null) {
|
|
Log.d(TAG, "Abandoning audio focus")
|
|
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
if (am.abandonAudioFocusRequest(audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
audioFocusRequest = null
|
|
else
|
|
Log.e(TAG, "Failed to abandon audio focus")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|