Merge branch 'calling_app'
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
tools:ignore="ForegroundServicesPolicy" />
|
tools:ignore="ForegroundServicesPolicy" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
@@ -25,6 +26,8 @@
|
|||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:ignore="FullScreenIntentPolicy" />
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:ignore="FullScreenIntentPolicy" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@@ -83,8 +86,21 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".BaresipService"
|
android:name=".BaresipService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="microphone|phoneCall"
|
android:foregroundServiceType="microphone|phoneCall|specialUse"
|
||||||
android:stopWithTask="false" >
|
android:stopWithTask="false" >
|
||||||
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Maintaining SIP registration for incoming VoIP calls" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".ConnectionService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.telecom.ConnectionService" />
|
||||||
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|||||||
@@ -1414,6 +1414,14 @@ JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1ismuted(
|
|||||||
return audio_ismuted(call_audio((struct call *)call));
|
return audio_ismuted(call_audio((struct call *)call));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1supported(
|
||||||
|
JNIEnv *env, jobject obj, jlong call, jint tags)
|
||||||
|
{
|
||||||
|
(void)env;
|
||||||
|
(void)obj;
|
||||||
|
return call_supported((struct call *)call, tags);
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1transfer(
|
JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1transfer(
|
||||||
JNIEnv *env, jobject obj, jlong call, jstring jPeer)
|
JNIEnv *env, jobject obj, jlong call, jstring jPeer)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ object Api {
|
|||||||
|
|
||||||
const val CALL_STATE_EARLY = 4
|
const val CALL_STATE_EARLY = 4
|
||||||
|
|
||||||
|
const val REPLACES = 1
|
||||||
|
|
||||||
external fun account_set_display_name(acc: Long, dn: String): Int
|
external fun account_set_display_name(acc: Long, dn: String): Int
|
||||||
external fun account_display_name(acc: Long): String
|
external fun account_display_name(acc: Long): String
|
||||||
external fun account_aor(acc: Long): String
|
external fun account_aor(acc: Long): String
|
||||||
@@ -104,6 +106,8 @@ object Api {
|
|||||||
external fun call_replace_transfer(xfer_callp: Long, callp: Long): Boolean
|
external fun call_replace_transfer(xfer_callp: Long, callp: Long): Boolean
|
||||||
external fun call_peer_uri(callp: Long): String
|
external fun call_peer_uri(callp: Long): String
|
||||||
external fun call_diverter_uri(callp: Long): String
|
external fun call_diverter_uri(callp: Long): String
|
||||||
|
|
||||||
|
external fun call_supported(callp: Long, header: Int): Boolean
|
||||||
external fun call_destroy(callp: Long)
|
external fun call_destroy(callp: Long)
|
||||||
|
|
||||||
external fun calls_mute(mute: Boolean)
|
external fun calls_mute(mute: Boolean)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.tutpro.baresip
|
package com.tutpro.baresip
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.Manifest.permission.RECORD_AUDIO
|
import android.Manifest.permission.RECORD_AUDIO
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
@@ -8,24 +7,20 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothHeadset
|
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.graphics.ImageDecoder
|
import android.graphics.ImageDecoder
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioDeviceInfo
|
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioManager.MODE_IN_COMMUNICATION
|
import android.media.AudioManager.MODE_IN_COMMUNICATION
|
||||||
import android.media.AudioManager.MODE_NORMAL
|
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.media.Ringtone
|
import android.media.Ringtone
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
@@ -51,6 +46,8 @@ import android.os.VibratorManager
|
|||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
|
import android.telecom.DisconnectCause
|
||||||
|
import android.telecom.PhoneAccountHandle
|
||||||
import android.telecom.TelecomManager
|
import android.telecom.TelecomManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
@@ -60,7 +57,6 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationCompat.MessagingStyle
|
import androidx.core.app.NotificationCompat.MessagingStyle
|
||||||
import androidx.core.app.Person
|
import androidx.core.app.Person
|
||||||
@@ -69,9 +65,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.media.AudioAttributesCompat
|
|
||||||
import androidx.media.AudioFocusRequestCompat
|
|
||||||
import androidx.media.AudioManagerCompat
|
|
||||||
import com.tutpro.baresip.Utils.toCircle
|
import com.tutpro.baresip.Utils.toCircle
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -98,12 +91,10 @@ class BaresipService: Service() {
|
|||||||
private lateinit var pm: PowerManager
|
private lateinit var pm: PowerManager
|
||||||
private lateinit var wm: WifiManager
|
private lateinit var wm: WifiManager
|
||||||
private lateinit var tm: TelecomManager
|
private lateinit var tm: TelecomManager
|
||||||
private lateinit var btm: BluetoothManager
|
|
||||||
private lateinit var vibrator: Vibrator
|
private lateinit var vibrator: Vibrator
|
||||||
private lateinit var partialWakeLock: PowerManager.WakeLock
|
private lateinit var partialWakeLock: PowerManager.WakeLock
|
||||||
private lateinit var proximityWakeLock: PowerManager.WakeLock
|
private lateinit var proximityWakeLock: PowerManager.WakeLock
|
||||||
private lateinit var wifiLock: WifiManager.WifiLock
|
private lateinit var wifiLock: WifiManager.WifiLock
|
||||||
private lateinit var bluetoothReceiver: BroadcastReceiver
|
|
||||||
private lateinit var hotSpotReceiver: BroadcastReceiver
|
private lateinit var hotSpotReceiver: BroadcastReceiver
|
||||||
private lateinit var androidContactsObserver: ContentObserver
|
private lateinit var androidContactsObserver: ContentObserver
|
||||||
private lateinit var networkCallback: ConnectivityManager.NetworkCallback
|
private lateinit var networkCallback: ConnectivityManager.NetworkCallback
|
||||||
@@ -119,8 +110,8 @@ class BaresipService: Service() {
|
|||||||
private var hotSpotAddresses = mapOf<String, String>()
|
private var hotSpotAddresses = mapOf<String, String>()
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var androidContactsObserverRegistered = false
|
private var androidContactsObserverRegistered = false
|
||||||
private var bluetoothReceiverRegistered = false
|
|
||||||
private var hotSpotReceiverRegistered = false
|
private var hotSpotReceiverRegistered = false
|
||||||
|
private var isNotificationInCall = false
|
||||||
private var isServiceClean = false
|
private var isServiceClean = false
|
||||||
|
|
||||||
@SuppressLint("WakelockTimeout")
|
@SuppressLint("WakelockTimeout")
|
||||||
@@ -128,6 +119,7 @@ class BaresipService: Service() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Log.i(TAG, "BaresipService onCreate")
|
Log.i(TAG, "BaresipService onCreate")
|
||||||
|
instance = this
|
||||||
|
|
||||||
intent = Intent("com.tutpro.baresip.EVENT")
|
intent = Intent("com.tutpro.baresip.EVENT")
|
||||||
intent.setPackage("com.tutpro.baresip")
|
intent.setPackage("com.tutpro.baresip")
|
||||||
@@ -279,9 +271,7 @@ class BaresipService: Service() {
|
|||||||
hotSpotReceiverRegistered = true
|
hotSpotReceiverRegistered = true
|
||||||
|
|
||||||
tm = getSystemService(TELECOM_SERVICE) as TelecomManager
|
tm = getSystemService(TELECOM_SERVICE) as TelecomManager
|
||||||
|
registerPhoneAccount()
|
||||||
btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
|
|
||||||
btAdapter = btm.adapter
|
|
||||||
|
|
||||||
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||||
"com.tutpro.baresip:proximity_wakelock")
|
"com.tutpro.baresip:proximity_wakelock")
|
||||||
@@ -291,74 +281,8 @@ class BaresipService: Service() {
|
|||||||
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip")
|
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip")
|
||||||
else
|
else
|
||||||
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Baresip")
|
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Baresip")
|
||||||
|
|
||||||
wifiLock.setReferenceCounted(false)
|
wifiLock.setReferenceCounted(false)
|
||||||
|
|
||||||
bluetoothReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(ctx: Context, intent: Intent) {
|
|
||||||
when (intent.action) {
|
|
||||||
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED -> {
|
|
||||||
val state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
|
|
||||||
BluetoothHeadset.STATE_DISCONNECTED)
|
|
||||||
when (state) {
|
|
||||||
BluetoothHeadset.STATE_CONNECTED -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset is connected")
|
|
||||||
if (audioFocusRequest != null)
|
|
||||||
startBluetoothSco(applicationContext, 1000L, 3)
|
|
||||||
}
|
|
||||||
BluetoothHeadset.STATE_DISCONNECTED -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset is disconnected")
|
|
||||||
if (audioFocusRequest != null)
|
|
||||||
stopBluetoothSco(applicationContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED -> {
|
|
||||||
val state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
|
|
||||||
BluetoothHeadset.STATE_AUDIO_DISCONNECTED)
|
|
||||||
when (state) {
|
|
||||||
BluetoothHeadset.STATE_AUDIO_CONNECTED -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset audio is connected")
|
|
||||||
}
|
|
||||||
BluetoothHeadset.STATE_AUDIO_DISCONNECTED -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset audio is disconnected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
|
|
||||||
val state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
|
|
||||||
AudioManager.SCO_AUDIO_STATE_DISCONNECTED)
|
|
||||||
when (state) {
|
|
||||||
AudioManager.SCO_AUDIO_STATE_CONNECTING -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset SCO is connecting")
|
|
||||||
}
|
|
||||||
AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset SCO is connected")
|
|
||||||
}
|
|
||||||
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset SCO is disconnected")
|
|
||||||
resetCallVolume()
|
|
||||||
}
|
|
||||||
AudioManager.SCO_AUDIO_STATE_ERROR -> {
|
|
||||||
Log.d(TAG, "Bluetooth headset SCO state ERROR")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btAdapter != null) {
|
|
||||||
val filter = IntentFilter()
|
|
||||||
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
|
||||||
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
|
|
||||||
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
|
|
||||||
ContextCompat.registerReceiver(applicationContext, bluetoothReceiver, filter,
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED)
|
|
||||||
bluetoothReceiverRegistered = true
|
|
||||||
}
|
|
||||||
|
|
||||||
androidContactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
androidContactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||||
override fun onChange(self: Boolean) {
|
override fun onChange(self: Boolean) {
|
||||||
Log.d(TAG, "Android contacts change")
|
Log.d(TAG, "Android contacts change")
|
||||||
@@ -566,6 +490,18 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"Call Hangup" -> {
|
||||||
|
val callp = intent!!.getLongExtra("callp", 0L)
|
||||||
|
Log.d(TAG, "onStartCommand Hangup action for $callp")
|
||||||
|
val connection = ConnectionService.connections[callp]
|
||||||
|
if (connection != null) {
|
||||||
|
connection.onDisconnect() // This calls Api.ua_hangup(..., 0, "")
|
||||||
|
} else {
|
||||||
|
val call = Call.ofCallp(callp)
|
||||||
|
if (call != null) Api.ua_hangup(call.ua.uap, callp, 0, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"Transfer Deny" -> {
|
"Transfer Deny" -> {
|
||||||
val callp = intent!!.getLongExtra("callp", 0L)
|
val callp = intent!!.getLongExtra("callp", 0L)
|
||||||
val call = Call.ofCallp(callp)
|
val call = Call.ofCallp(callp)
|
||||||
@@ -686,6 +622,7 @@ class BaresipService: Service() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.d(TAG, "onDestroy at Baresip Service")
|
Log.d(TAG, "onDestroy at Baresip Service")
|
||||||
cleanService()
|
cleanService()
|
||||||
|
instance = null
|
||||||
if (isServiceRunning)
|
if (isServiceRunning)
|
||||||
sendBroadcast(Intent("com.tutpro.baresip.Restart"))
|
sendBroadcast(Intent("com.tutpro.baresip.Restart"))
|
||||||
}
|
}
|
||||||
@@ -845,19 +782,20 @@ class BaresipService: Service() {
|
|||||||
break
|
break
|
||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
setCallVolume()
|
setCallVolume()
|
||||||
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
|
|
||||||
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
|
|
||||||
proximitySensing(proximitySensing)
|
proximitySensing(proximitySensing)
|
||||||
}
|
}
|
||||||
"call ringing" -> {
|
"call ringing" -> {
|
||||||
|
ConnectionService.connections[callp]?.setRinging()
|
||||||
playRingBack()
|
playRingBack()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
"call progress" -> {
|
"call progress" -> {
|
||||||
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
else
|
else {
|
||||||
|
ConnectionService.connections[callp]?.setRinging()
|
||||||
playRingBack()
|
playRingBack()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
"incoming call" -> {
|
"incoming call" -> {
|
||||||
@@ -871,8 +809,6 @@ class BaresipService: Service() {
|
|||||||
Utils.friendlyUri(this, peerUri, ua.account))
|
Utils.friendlyUri(this, peerUri, ua.account))
|
||||||
else if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO)))
|
else if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO)))
|
||||||
getString(R.string.no_calls)
|
getString(R.string.no_calls)
|
||||||
else if (!requestAudioFocus(applicationContext))
|
|
||||||
getString(R.string.audio_focus_denied)
|
|
||||||
else
|
else
|
||||||
""
|
""
|
||||||
if (toastMsg != "") {
|
if (toastMsg != "") {
|
||||||
@@ -915,86 +851,21 @@ class BaresipService: Service() {
|
|||||||
"call incoming" -> {
|
"call incoming" -> {
|
||||||
val peerUri = ev[1]
|
val peerUri = ev[1]
|
||||||
Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
|
Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
|
||||||
|
|
||||||
|
if (Call.ofCallp(callp) == null)
|
||||||
Call(callp, ua, peerUri, "in", "incoming").add()
|
Call(callp, ua, peerUri, "in", "incoming").add()
|
||||||
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
|
|
||||||
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
|
val extras = android.os.Bundle()
|
||||||
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) {
|
extras.putLong("uap", uap)
|
||||||
val newIntent = Intent(this, MainActivity::class.java)
|
extras.putLong("callp", callp)
|
||||||
newIntent.flags =
|
extras.putString("peerUri", peerUri)
|
||||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
newIntent.putExtra("action", "call answer")
|
|
||||||
newIntent.putExtra("callp", callp)
|
|
||||||
startActivity(newIntent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val channelId = HIGH_CHANNEL_ID
|
|
||||||
val callerNumber = peerUri.split(":")[1].split("@")[0]
|
|
||||||
if (shouldStartRinging(callerNumber))
|
|
||||||
startRinging()
|
|
||||||
if (!Utils.isVisible()) {
|
|
||||||
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
|
||||||
.putExtra("action", "call show")
|
|
||||||
.putExtra("callp", callp)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
|
|
||||||
val nb = NotificationCompat.Builder(this, channelId)
|
|
||||||
val caller = Utils.friendlyUri(this, peerUri, ua.account)
|
|
||||||
val callerContact = Contact.findContact(peerUri)
|
|
||||||
val personBuilder = Person.Builder().setName(caller)
|
|
||||||
val contactColor = callerContact?.color() ?: "#B0B0B0"
|
|
||||||
val initial = if (caller.isNotEmpty()) caller.take(1) else "?"
|
|
||||||
val textAvatarBitmap = Utils.createTextAvatar(initial,contactColor)
|
|
||||||
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
|
|
||||||
if (callerContact is Contact.BaresipContact) {
|
|
||||||
if (callerContact.avatarImage != null)
|
|
||||||
icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle())
|
|
||||||
}
|
|
||||||
else if (callerContact is Contact.AndroidContact) {
|
|
||||||
if (callerContact.thumbnailUri != null) {
|
|
||||||
try {
|
try {
|
||||||
val source = ImageDecoder.createSource(contentResolver,
|
tm.addNewIncomingCall(getPhoneAccountHandle(this), extras)
|
||||||
callerContact.thumbnailUri!!)
|
|
||||||
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
|
||||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
|
||||||
// decoder.setTargetSize(256, 256)
|
|
||||||
}
|
|
||||||
icon = IconCompat.createWithBitmap(bitmap.toCircle())
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load Android contact avatar: $e")
|
Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
val person = personBuilder.setIcon(icon).build()
|
|
||||||
nb.setSmallIcon(R.drawable.ic_notification_call)
|
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
|
||||||
.setContentIntent(pi)
|
|
||||||
.setCategory(Notification.CATEGORY_CALL)
|
|
||||||
.setAutoCancel(false)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setContentText(getString(R.string.is_calling))
|
|
||||||
.setWhen(System.currentTimeMillis())
|
|
||||||
.setShowWhen(true)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
||||||
if (VERSION.SDK_INT < 34 || nm.canUseFullScreenIntent())
|
|
||||||
nb.setFullScreenIntent(pi, true)
|
|
||||||
val answerIntent = Intent(applicationContext, MainActivity::class.java)
|
|
||||||
.putExtra("action", "call answer")
|
|
||||||
.putExtra("callp", callp)
|
|
||||||
val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE,
|
|
||||||
answerIntent, piFlags)
|
|
||||||
val rejectIntent = Intent(this, BaresipService::class.java)
|
|
||||||
rejectIntent.action = "Call Reject"
|
|
||||||
rejectIntent.putExtra("callp", callp)
|
|
||||||
val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags)
|
|
||||||
nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api))
|
|
||||||
nm.notify(CALL_NOTIFICATION_ID, nb.build())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
"call answered" -> {
|
"call answered" -> {
|
||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
if (call!!.status.value == "incoming")
|
if (call!!.status.value == "incoming")
|
||||||
@@ -1006,28 +877,32 @@ class BaresipService: Service() {
|
|||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
}
|
}
|
||||||
"call established" -> {
|
"call established" -> {
|
||||||
|
ConnectionService.connections[callp]?.setActive()
|
||||||
nm.cancel(CALL_NOTIFICATION_ID)
|
nm.cancel(CALL_NOTIFICATION_ID)
|
||||||
Log.d(TAG, "AoR $aor call $callp established in mode ${am.mode}")
|
Log.d(TAG, "AoR $aor call $callp established")
|
||||||
if (am.mode != MODE_IN_COMMUNICATION)
|
|
||||||
am.mode = MODE_IN_COMMUNICATION
|
|
||||||
call!!.status.value = "connected"
|
call!!.status.value = "connected"
|
||||||
call.onhold = false
|
call.onhold = false
|
||||||
if (ua.account.callHistory)
|
|
||||||
call.startTime = GregorianCalendar()
|
call.startTime = GregorianCalendar()
|
||||||
|
updateStatusNotification()
|
||||||
if (!isMainVisible)
|
if (!isMainVisible)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
"call update" -> {
|
"call update" -> {
|
||||||
call!!.held = when (ev[1].toInt()) {
|
val held = when (ev[1].toInt()) {
|
||||||
Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true
|
Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true
|
||||||
else /* Api.SDP_SENDONLY, Api.SDP_SENDRECV */ -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
call!!.held = held
|
||||||
|
val connection = ConnectionService.connections[callp]
|
||||||
|
if (held) connection?.setOnHold() else connection?.setActive()
|
||||||
if (call.state() == Api.CALL_STATE_EARLY) {
|
if (call.state() == Api.CALL_STATE_EARLY) {
|
||||||
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
|
||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
else
|
else {
|
||||||
|
ConnectionService.connections[callp]?.setRinging()
|
||||||
playRingBack()
|
playRingBack()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!isMainVisible || call.status.value != "connected")
|
if (!isMainVisible || call.status.value != "connected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1090,6 +965,11 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
"call closed" -> {
|
"call closed" -> {
|
||||||
Log.d(TAG, "AoR $aor call $callp is closed prm: ${ev[1]}")
|
Log.d(TAG, "AoR $aor call $callp is closed prm: ${ev[1]}")
|
||||||
|
ConnectionService.connections[callp]?.let {
|
||||||
|
it.setDisconnected(DisconnectCause(DisconnectCause.REMOTE))
|
||||||
|
it.destroy()
|
||||||
|
ConnectionService.connections.remove(callp)
|
||||||
|
}
|
||||||
nm.cancel(CALL_NOTIFICATION_ID)
|
nm.cancel(CALL_NOTIFICATION_ID)
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
stopRinging()
|
stopRinging()
|
||||||
@@ -1112,16 +992,15 @@ class BaresipService: Service() {
|
|||||||
call.onHoldCall = null
|
call.onHoldCall = null
|
||||||
}
|
}
|
||||||
call.remove()
|
call.remove()
|
||||||
|
updateStatusNotification()
|
||||||
if (call.conferenceCall && ua.calls().isEmpty())
|
if (call.conferenceCall && ua.calls().isEmpty())
|
||||||
Api.module_unload("mixminus")
|
Api.module_unload("mixminus")
|
||||||
val reason = ev[1]
|
val reason = ev[1]
|
||||||
val tone = ev[2]
|
val tone = ev[2]
|
||||||
if (tone == "busy") {
|
if (tone == "busy")
|
||||||
playBusy()
|
playBusy()
|
||||||
}
|
|
||||||
else if (!Call.inCall()) {
|
else if (!Call.inCall()) {
|
||||||
resetCallVolume()
|
resetCallVolume()
|
||||||
abandonAudioFocus(applicationContext)
|
|
||||||
proximitySensing(false)
|
proximitySensing(false)
|
||||||
}
|
}
|
||||||
if (call.dir == "out")
|
if (call.dir == "out")
|
||||||
@@ -1410,6 +1289,40 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun runCall(uap: Long, uri: String, conferenceCall: Boolean, onHoldCallp: Long) {
|
||||||
|
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()
|
||||||
|
if (onHoldCall != null)
|
||||||
|
onHoldCall.newCall = call
|
||||||
|
if (!call.connect(uri)) {
|
||||||
|
Log.w(TAG, "call_connect $callp failed")
|
||||||
|
ConnectionService.onCallClosed(callp)
|
||||||
|
call.remove()
|
||||||
|
call.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ConnectionService.pendingOutgoingConnection?.let {
|
||||||
|
it.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
|
||||||
|
it.destroy()
|
||||||
|
ConnectionService.pendingOutgoingConnection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, audioDelay)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@Keep
|
@Keep
|
||||||
fun started() {
|
fun started() {
|
||||||
@@ -1466,7 +1379,6 @@ class BaresipService: Service() {
|
|||||||
if (VERSION.SDK_INT >= 31)
|
if (VERSION.SDK_INT >= 31)
|
||||||
snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
|
snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||||
snb.setOngoing(true)
|
snb.setOngoing(true)
|
||||||
snb.setCategory(Notification.CATEGORY_SERVICE)
|
|
||||||
val notification = snb.build()
|
val notification = snb.build()
|
||||||
notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or
|
notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or
|
||||||
Notification.FLAG_ONGOING_EVENT
|
Notification.FLAG_ONGOING_EVENT
|
||||||
@@ -1494,22 +1406,14 @@ class BaresipService: Service() {
|
|||||||
.setContentIntent(pi)
|
.setContentIntent(pi)
|
||||||
.setDeleteIntent(dpi)
|
.setDeleteIntent(dpi)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||||
.setCustomContentView(notificationLayout)
|
.setCustomContentView(notificationLayout)
|
||||||
val notification = buildStatusNotification()
|
val notification = buildStatusNotification()
|
||||||
try {
|
try {
|
||||||
if (VERSION.SDK_INT >= 29)
|
if (VERSION.SDK_INT >= 34)
|
||||||
startForeground(
|
startForeground(STATUS_NOTIFICATION_ID, notification,
|
||||||
STATUS_NOTIFICATION_ID, notification,
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
if (VERSION.SDK_INT >= 30) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)
|
|
||||||
FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
||||||
else
|
|
||||||
FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
||||||
}
|
|
||||||
else
|
|
||||||
FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
startForeground(STATUS_NOTIFICATION_ID, notification)
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1517,13 +1421,61 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatusNotification() {
|
fun updateStatusNotification() {
|
||||||
|
val activeCall = Call.calls().find {
|
||||||
|
it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered"
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(this, LOW_CHANNEL_ID)
|
||||||
|
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||||
|
.setAction(Intent.ACTION_MAIN)
|
||||||
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
|
||||||
|
val pi = PendingIntent.getActivity(applicationContext, STATUS_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val deleteIntent = Intent(this, BaresipService::class.java).setAction("Notification Dismissed")
|
||||||
|
val dpi = PendingIntent.getService(this, STATUS_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
|
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification_b)
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.setDeleteIntent(dpi)
|
||||||
|
.setOngoing(true)
|
||||||
|
|
||||||
|
if (activeCall != null) {
|
||||||
|
val peerUri = activeCall.peerUri
|
||||||
|
val caller = Utils.friendlyUri(this, peerUri, activeCall.ua.account)
|
||||||
|
val person = Person.Builder().setName(caller).build()
|
||||||
|
|
||||||
|
val hangupIntent = Intent(this, BaresipService::class.java)
|
||||||
|
hangupIntent.action = "Call Hangup"
|
||||||
|
hangupIntent.putExtra("callp", activeCall.callp)
|
||||||
|
val hpi = PendingIntent.getService(this, REJECT_REQ_CODE, hangupIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
|
builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, hpi))
|
||||||
|
.setCategory(Notification.CATEGORY_CALL)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setWhen(activeCall.startTime?.timeInMillis ?: System.currentTimeMillis())
|
||||||
|
.setUsesChronometer(true)
|
||||||
|
.setContentText(if (activeCall.onhold) getString(R.string.call_is_on_hold) else getString(R.string.call_is_connected))
|
||||||
|
} else {
|
||||||
|
// IMPORTANT: Explicitly clear the style first to ensure CallStyle is removed
|
||||||
|
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)
|
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
|
||||||
for (i: Int in 0..3) {
|
for (i in 0..3) {
|
||||||
val resId = when (i) {
|
val resId = when (i) {
|
||||||
0-> R.id.status0
|
0 -> R.id.status0
|
||||||
1-> R.id.status1
|
1 -> R.id.status1
|
||||||
2-> R.id.status2
|
2 -> R.id.status2
|
||||||
else -> R.id.status3
|
else -> R.id.status3
|
||||||
}
|
}
|
||||||
if (i < uas.value.size) {
|
if (i < uas.value.size) {
|
||||||
@@ -1537,19 +1489,59 @@ class BaresipService: Service() {
|
|||||||
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
|
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
|
||||||
else
|
else
|
||||||
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
|
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
|
||||||
snb.setCustomContentView(notificationLayout)
|
|
||||||
// Don't know why, but without the delay the notification is not always updated
|
builder.setCustomContentView(notificationLayout)
|
||||||
Timer().schedule(250) {
|
|
||||||
nm.notify(STATUS_NOTIFICATION_ID, buildStatusNotification())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
val type = if (VERSION.SDK_INT >= 30) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)
|
||||||
|
FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||||
|
else
|
||||||
|
FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||||
|
} else 0
|
||||||
|
if (VERSION.SDK_INT >= 29)
|
||||||
|
startForeground(STATUS_NOTIFICATION_ID, notification, type)
|
||||||
|
else
|
||||||
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
||||||
|
isNotificationInCall = true
|
||||||
|
} else {
|
||||||
|
if (isNotificationInCall) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
isNotificationInCall = false
|
||||||
|
if (VERSION.SDK_INT >= 34)
|
||||||
|
startForeground(STATUS_NOTIFICATION_ID, notification,
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
else if (VERSION.SDK_INT >= 30)
|
||||||
|
startForeground(STATUS_NOTIFICATION_ID, notification, 0)
|
||||||
|
else
|
||||||
|
startForeground(STATUS_NOTIFICATION_ID, notification)
|
||||||
|
} else {
|
||||||
|
// Use standard notify for standby updates.
|
||||||
|
// This is much more stable for background registration.
|
||||||
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to update foreground notification: ${e.message}")
|
||||||
|
nm.notify(STATUS_NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
updatePartialWakeLock()
|
updatePartialWakeLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("WakelockTimeout")
|
@SuppressLint("WakelockTimeout")
|
||||||
private fun updatePartialWakeLock() {
|
private fun updatePartialWakeLock() {
|
||||||
val isAnyUaActive = uasStatus.value.values.any { it != R.drawable.circle_white }
|
val isAnyUaActive = uasStatus.value.values.any { it != R.drawable.circle_white }
|
||||||
val isAnyCallActive = calls.isNotEmpty()
|
val needsToStayAwake = isAnyUaActive || calls.isNotEmpty()
|
||||||
val needsToStayAwake = isAnyUaActive || isAnyCallActive
|
|
||||||
try {
|
try {
|
||||||
if (needsToStayAwake) {
|
if (needsToStayAwake) {
|
||||||
if (!partialWakeLock.isHeld) {
|
if (!partialWakeLock.isHeld) {
|
||||||
@@ -1565,6 +1557,7 @@ class BaresipService: Service() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error managing partialWakeLock: ${e.message}")
|
Log.e(TAG, "Error managing partialWakeLock: ${e.message}")
|
||||||
}
|
}
|
||||||
|
updatePartialWakeLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
|
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
|
||||||
@@ -1573,6 +1566,84 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("FullScreenIntentPolicy")
|
||||||
|
fun handleIncomingCall(call: Call) {
|
||||||
|
val ua = call.ua
|
||||||
|
val peerUri = call.peerUri
|
||||||
|
val callp = call.callp
|
||||||
|
|
||||||
|
val callerNumber = peerUri.split(":")[1].split("@")[0]
|
||||||
|
if (shouldStartRinging(callerNumber))
|
||||||
|
startRinging()
|
||||||
|
|
||||||
|
if (!Utils.isVisible()) {
|
||||||
|
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||||
|
.putExtra("action", "call show")
|
||||||
|
.putExtra("callp", callp)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
|
||||||
|
|
||||||
|
val channelId = HIGH_CHANNEL_ID
|
||||||
|
val nb = NotificationCompat.Builder(this, channelId)
|
||||||
|
val caller = Utils.friendlyUri(this, peerUri, ua.account)
|
||||||
|
val callerContact = Contact.findContact(peerUri)
|
||||||
|
val personBuilder = Person.Builder().setName(caller)
|
||||||
|
val contactColor = callerContact?.color() ?: "#B0B0B0"
|
||||||
|
val initial = if (caller.isNotEmpty()) caller.take(1) else "?"
|
||||||
|
val textAvatarBitmap = Utils.createTextAvatar(initial, contactColor)
|
||||||
|
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
|
||||||
|
|
||||||
|
if (callerContact is Contact.BaresipContact) {
|
||||||
|
if (callerContact.avatarImage != null)
|
||||||
|
icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle())
|
||||||
|
} else if (callerContact is Contact.AndroidContact) {
|
||||||
|
if (callerContact.thumbnailUri != null) {
|
||||||
|
try {
|
||||||
|
val source = ImageDecoder.createSource(contentResolver, callerContact.thumbnailUri!!)
|
||||||
|
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
||||||
|
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||||
|
}
|
||||||
|
icon = IconCompat.createWithBitmap(bitmap.toCircle())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load Android contact avatar: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val person = personBuilder.setIcon(icon).build()
|
||||||
|
nb.setSmallIcon(R.drawable.ic_notification_call)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.setCategory(Notification.CATEGORY_CALL)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setContentText(getString(R.string.is_calling))
|
||||||
|
.setWhen(System.currentTimeMillis())
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
|
||||||
|
if (VERSION.SDK_INT < 34 || nm.canUseFullScreenIntent())
|
||||||
|
nb.setFullScreenIntent(pi, true)
|
||||||
|
|
||||||
|
val answerIntent = Intent(applicationContext, MainActivity::class.java)
|
||||||
|
.putExtra("action", "call answer")
|
||||||
|
.putExtra("callp", callp)
|
||||||
|
val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE, answerIntent, piFlags)
|
||||||
|
|
||||||
|
val rejectIntent = Intent(this, BaresipService::class.java)
|
||||||
|
rejectIntent.action = "Call Reject"
|
||||||
|
rejectIntent.putExtra("callp", callp)
|
||||||
|
val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags)
|
||||||
|
|
||||||
|
nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api))
|
||||||
|
nm.notify(CALL_NOTIFICATION_ID, nb.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
postServiceEvent(ServiceEvent("call incoming", arrayListOf(ua.uap, callp), System.nanoTime()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun startRinging() {
|
private fun startRinging() {
|
||||||
am.mode = AudioManager.MODE_RINGTONE
|
am.mode = AudioManager.MODE_RINGTONE
|
||||||
rt!!.isLooping = true
|
rt!!.isLooping = true
|
||||||
@@ -1621,6 +1692,7 @@ class BaresipService: Service() {
|
|||||||
// First, check if the ringer volume is actually non-zero
|
// First, check if the ringer volume is actually non-zero
|
||||||
if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false
|
if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false
|
||||||
// Finally, check the system "Vibrate for calls" setting.
|
// Finally, check the system "Vibrate for calls" setting.
|
||||||
|
// Although deprecated, it is the standard way to check the "Also vibrate for calls" toggle.
|
||||||
return try {
|
return try {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
|
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
|
||||||
@@ -1695,7 +1767,6 @@ class BaresipService: Service() {
|
|||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
if (!Call.inCall()) {
|
if (!Call.inCall()) {
|
||||||
resetCallVolume()
|
resetCallVolume()
|
||||||
abandonAudioFocus(applicationContext)
|
|
||||||
proximitySensing(false)
|
proximitySensing(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1761,15 +1832,9 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNetwork() {
|
private fun updateNetwork() {
|
||||||
|
|
||||||
/* for (n in allNetworks)
|
|
||||||
Log.d(TAG, "NETWORK $n with caps ${cm.getNetworkCapabilities(n)} and props " +
|
|
||||||
"${cm.getLinkProperties(n)} is active ${isNetworkActive(n)}") */
|
|
||||||
|
|
||||||
updateDnsServers()
|
updateDnsServers()
|
||||||
|
|
||||||
val addresses = linkAddresses()
|
val addresses = linkAddresses()
|
||||||
|
|
||||||
Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses")
|
Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses")
|
||||||
|
|
||||||
var added = 0
|
var added = 0
|
||||||
@@ -1780,6 +1845,7 @@ class BaresipService: Service() {
|
|||||||
else
|
else
|
||||||
added++
|
added++
|
||||||
}
|
}
|
||||||
|
|
||||||
var removed = 0
|
var removed = 0
|
||||||
for (a in linkAddresses)
|
for (a in linkAddresses)
|
||||||
if (!addresses.containsKey(a.key)) {
|
if (!addresses.containsKey(a.key)) {
|
||||||
@@ -1895,19 +1961,36 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun registerPhoneAccount() {
|
||||||
|
val phoneAccountHandle = getPhoneAccountHandle(this)
|
||||||
|
val phoneAccount = android.telecom.PhoneAccount.builder(phoneAccountHandle, getString(R.string.app_name))
|
||||||
|
.setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED)
|
||||||
|
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP)
|
||||||
|
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL)
|
||||||
|
.build()
|
||||||
|
tm.registerPhoneAccount(phoneAccount)
|
||||||
|
}
|
||||||
|
|
||||||
private fun cleanService() {
|
private fun cleanService() {
|
||||||
if (!isServiceClean) {
|
if (!isServiceClean) {
|
||||||
if (bluetoothReceiverRegistered) {
|
try {
|
||||||
this.unregisterReceiver(bluetoothReceiver)
|
|
||||||
bluetoothReceiverRegistered = false
|
|
||||||
}
|
|
||||||
if (hotSpotReceiverRegistered) {
|
if (hotSpotReceiverRegistered) {
|
||||||
unregisterReceiver(hotSpotReceiver)
|
applicationContext.unregisterReceiver(hotSpotReceiver)
|
||||||
hotSpotReceiverRegistered = false
|
hotSpotReceiverRegistered = false
|
||||||
}
|
}
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
Log.e(TAG, "hotSpotReceiver was not registered with applicationContext")
|
||||||
|
}
|
||||||
|
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()
|
stopRinging()
|
||||||
stopMediaPlayer()
|
stopMediaPlayer()
|
||||||
abandonAudioFocus(applicationContext)
|
|
||||||
uas.value = emptyList()
|
uas.value = emptyList()
|
||||||
uasStatus.value = emptyMap()
|
uasStatus.value = emptyMap()
|
||||||
callHistory.clear()
|
callHistory.clear()
|
||||||
@@ -1940,6 +2023,7 @@ class BaresipService: Service() {
|
|||||||
@SuppressLint("MutableCollectionMutableState")
|
@SuppressLint("MutableCollectionMutableState")
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
var instance: BaresipService? = null
|
||||||
var isServiceRunning = false
|
var isServiceRunning = false
|
||||||
var isStartReceived = false
|
var isStartReceived = false
|
||||||
var isConfigInitialized = false
|
var isConfigInitialized = false
|
||||||
@@ -1984,7 +2068,6 @@ class BaresipService: Service() {
|
|||||||
var dnsServers = listOf<InetAddress>()
|
var dnsServers = listOf<InetAddress>()
|
||||||
// <aor, password> of those accounts that have auth username without auth password
|
// <aor, password> of those accounts that have auth username without auth password
|
||||||
val aorPasswords = mutableMapOf<String, String>()
|
val aorPasswords = mutableMapOf<String, String>()
|
||||||
var audioFocusRequest: AudioFocusRequestCompat? = null
|
|
||||||
var aecAvailable = false
|
var aecAvailable = false
|
||||||
private var aec: AcousticEchoCanceler? = null
|
private var aec: AcousticEchoCanceler? = null
|
||||||
var agcAvailable = false
|
var agcAvailable = false
|
||||||
@@ -2001,10 +2084,15 @@ class BaresipService: Service() {
|
|||||||
private var agc: AutomaticGainControl? = null
|
private var agc: AutomaticGainControl? = null
|
||||||
private val nsAvailable = NoiseSuppressor.isAvailable()
|
private val nsAvailable = NoiseSuppressor.isAvailable()
|
||||||
private var ns: NoiseSuppressor? = null
|
private var ns: NoiseSuppressor? = null
|
||||||
private var btAdapter: BluetoothAdapter? = null
|
|
||||||
private var recorderSessionId = 0
|
private var recorderSessionId = 0
|
||||||
|
|
||||||
internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
|
internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
|
||||||
|
private const val PHONE_ACCOUNT_ID = "baresip_phone_account"
|
||||||
|
|
||||||
|
fun getPhoneAccountHandle(ctx: Context): PhoneAccountHandle {
|
||||||
|
val componentName = android.content.ComponentName(ctx, ConnectionService::class.java)
|
||||||
|
return PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID)
|
||||||
|
}
|
||||||
|
|
||||||
fun postServiceEvent(event: ServiceEvent) {
|
fun postServiceEvent(event: ServiceEvent) {
|
||||||
serviceEvents.add(event)
|
serviceEvents.add(event)
|
||||||
@@ -2015,105 +2103,5 @@ class BaresipService: Service() {
|
|||||||
Log.d(TAG, "Added service event ${event.event}")
|
Log.d(TAG, "Added service event ${event.event}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestAudioFocus(ctx: Context): Boolean {
|
|
||||||
Log.d(TAG, "Requesting audio focus")
|
|
||||||
if (audioFocusRequest != null) {
|
|
||||||
Log.d(TAG, "Already focused")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
||||||
val attributes = AudioAttributesCompat.Builder()
|
|
||||||
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
|
|
||||||
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
|
||||||
.build()
|
|
||||||
audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
|
||||||
.setAudioAttributes(attributes)
|
|
||||||
.setOnAudioFocusChangeListener { }
|
|
||||||
.build()
|
|
||||||
if (AudioManagerCompat.requestAudioFocus(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
||||||
Log.d(TAG, "requestAudioFocus granted")
|
|
||||||
if (isBluetoothHeadsetConnected(ctx))
|
|
||||||
startBluetoothSco(ctx, 250L, 3)
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "requestAudioFocus denied")
|
|
||||||
audioFocusRequest = null
|
|
||||||
}
|
|
||||||
return audioFocusRequest != null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun abandonAudioFocus(ctx: Context) {
|
|
||||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
||||||
if (audioFocusRequest != null) {
|
|
||||||
Log.d(TAG, "Abandoning audio focus")
|
|
||||||
if (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) ==
|
|
||||||
AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
||||||
audioFocusRequest = null
|
|
||||||
if (isBluetoothHeadsetConnected(ctx))
|
|
||||||
stopBluetoothSco(ctx)
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Failed to abandon audio focus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
am.mode = MODE_NORMAL
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isBluetoothHeadsetConnected(ctx: Context): Boolean {
|
|
||||||
if (VERSION.SDK_INT >= 31 &&
|
|
||||||
ActivityCompat.checkSelfPermission(ctx, Manifest.permission.BLUETOOTH_CONNECT) ==
|
|
||||||
PackageManager.PERMISSION_DENIED)
|
|
||||||
return false
|
|
||||||
return btAdapter != null && btAdapter!!.isEnabled &&
|
|
||||||
btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothAdapter.STATE_CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isBluetoothScoOn(am: AudioManager): Boolean {
|
|
||||||
return if (VERSION.SDK_INT < 31)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
am.isBluetoothScoOn
|
|
||||||
else
|
|
||||||
if (am.communicationDevice != null)
|
|
||||||
am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
|
||||||
else
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startBluetoothSco(ctx: Context, delay: Long, count: Int) {
|
|
||||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
||||||
if (isBluetoothScoOn(am)) {
|
|
||||||
Log.d(TAG, "Bluetooth SCO is already on")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Starting Bluetooth SCO at count $count")
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
if (VERSION.SDK_INT < 31) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
am.startBluetoothSco()
|
|
||||||
} else {
|
|
||||||
Utils.setCommunicationDevice(am, AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
|
|
||||||
}
|
|
||||||
if (!isBluetoothScoOn(am) && count > 1)
|
|
||||||
startBluetoothSco(ctx, delay, count - 1)
|
|
||||||
else
|
|
||||||
am.isBluetoothScoOn = true
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopBluetoothSco(ctx: Context) {
|
|
||||||
Log.d(TAG, "Stopping Bluetooth SCO")
|
|
||||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
||||||
if (!isBluetoothScoOn(am)) {
|
|
||||||
Log.d(TAG, "Bluetooth SCO is already off")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
if (VERSION.SDK_INT < 31)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
am.stopBluetoothSco()
|
|
||||||
else
|
|
||||||
am.clearCommunicationDevice()
|
|
||||||
am.isBluetoothScoOn = false
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package com.tutpro.baresip
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.telecom.CallAudioState
|
||||||
|
import android.telecom.Connection
|
||||||
|
import android.telecom.ConnectionRequest
|
||||||
|
import android.telecom.ConnectionService
|
||||||
|
import android.telecom.DisconnectCause
|
||||||
|
import android.telecom.PhoneAccountHandle
|
||||||
|
import android.telecom.TelecomManager
|
||||||
|
import android.net.Uri
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class ConnectionService : ConnectionService() {
|
||||||
|
|
||||||
|
private val TAG = "BaresipConnection"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val connections = ConcurrentHashMap<Long, BaresipConnection>()
|
||||||
|
var pendingOutgoingConnection: BaresipConnection? = null
|
||||||
|
|
||||||
|
fun promoteOutgoingConnection(callp: Long) {
|
||||||
|
pendingOutgoingConnection?.let {
|
||||||
|
it.callp = callp
|
||||||
|
connections[callp] = it
|
||||||
|
pendingOutgoingConnection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCallClosed(callp: Long) {
|
||||||
|
connections[callp]?.let {
|
||||||
|
it.setDisconnected(DisconnectCause(DisconnectCause.REMOTE))
|
||||||
|
it.destroy()
|
||||||
|
connections.remove(callp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateIncomingConnection(
|
||||||
|
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||||
|
request: ConnectionRequest?
|
||||||
|
): Connection {
|
||||||
|
val extras = request?.extras
|
||||||
|
val uap = extras?.getLong("uap") ?: 0L
|
||||||
|
val callp = extras?.getLong("callp") ?: 0L
|
||||||
|
val peerUri = extras?.getString("peerUri") ?: ""
|
||||||
|
|
||||||
|
Log.d(TAG, "onCreateIncomingConnection for $peerUri")
|
||||||
|
|
||||||
|
val connection = BaresipConnection(uap, callp)
|
||||||
|
connections[callp] = connection
|
||||||
|
connection.setAddress(Uri.fromParts("sip", peerUri, null), TelecomManager.PRESENTATION_ALLOWED)
|
||||||
|
connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD
|
||||||
|
|
||||||
|
val call = Call.ofCallp(callp)
|
||||||
|
if (call != null)
|
||||||
|
BaresipService.instance?.handleIncomingCall(call)
|
||||||
|
|
||||||
|
val ua = UserAgent.ofUap(uap)
|
||||||
|
if (ua != null) {
|
||||||
|
// Check speakerphone setting
|
||||||
|
if (BaresipService.speakerPhone) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Auto-Answer
|
||||||
|
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) {
|
||||||
|
Log.d(TAG, "Auto-answering call $callp")
|
||||||
|
connection.onAnswer()
|
||||||
|
} else {
|
||||||
|
connection.setRinging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateIncomingConnectionFailed(
|
||||||
|
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||||
|
request: ConnectionRequest?
|
||||||
|
) {
|
||||||
|
Log.e(TAG, "onCreateIncomingConnectionFailed")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOutgoingConnection(
|
||||||
|
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||||
|
request: ConnectionRequest?
|
||||||
|
): Connection {
|
||||||
|
val rootExtras = request?.extras
|
||||||
|
val nestedExtras = rootExtras?.getBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)
|
||||||
|
|
||||||
|
// Try root extras first, then fallback to nested
|
||||||
|
val uap = rootExtras?.getLong("uap", 0L).takeIf { it != 0L }
|
||||||
|
?: nestedExtras?.getLong("uap") ?: 0L
|
||||||
|
|
||||||
|
val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?:
|
||||||
|
nestedExtras?.getBoolean("conferenceCall") ?: false
|
||||||
|
|
||||||
|
val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L }
|
||||||
|
?: nestedExtras?.getLong("onHoldCallp") ?: 0L
|
||||||
|
|
||||||
|
val destination = request?.address?.schemeSpecificPart ?: ""
|
||||||
|
|
||||||
|
Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)")
|
||||||
|
|
||||||
|
val connection = BaresipConnection(uap, 0L)
|
||||||
|
pendingOutgoingConnection = connection
|
||||||
|
|
||||||
|
if (BaresipService.speakerPhone) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
|
||||||
|
connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD
|
||||||
|
|
||||||
|
// Start the SIP connection logic
|
||||||
|
if (uap != 0L) {
|
||||||
|
val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination"
|
||||||
|
BaresipService.instance?.runCall(uap, sipUri, conferenceCall, onHoldCallp)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Cannot start outgoing call: uap is 0")
|
||||||
|
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR, "No Account"))
|
||||||
|
connection.destroy()
|
||||||
|
pendingOutgoingConnection = null
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setDialing()
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOutgoingConnectionFailed(
|
||||||
|
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||||
|
request: ConnectionRequest?
|
||||||
|
) {
|
||||||
|
Log.e(TAG, "onCreateOutgoingConnectionFailed")
|
||||||
|
pendingOutgoingConnection = null
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class BaresipConnection(val uap: Long, var callp: Long) : Connection() {
|
||||||
|
|
||||||
|
override fun onAnswer() {
|
||||||
|
Log.d(TAG, "Telecom Connection onAnswer $callp")
|
||||||
|
val intent = Intent(this@ConnectionService, 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 answer")
|
||||||
|
intent.putExtra("callp", callp)
|
||||||
|
startActivity(intent)
|
||||||
|
BaresipService.instance?.updateStatusNotification()
|
||||||
|
setActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReject() {
|
||||||
|
Log.d(TAG, "Telecom Connection onReject $callp")
|
||||||
|
Api.ua_hangup(uap, callp, 486, "Rejected")
|
||||||
|
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
|
||||||
|
connections.remove(callp)
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnect() {
|
||||||
|
Log.d(TAG, "Telecom Connection onDisconnect $callp")
|
||||||
|
if (callp != 0L) {
|
||||||
|
Api.ua_hangup(uap, callp, 0, "")
|
||||||
|
connections.remove(callp)
|
||||||
|
} else {
|
||||||
|
pendingOutgoingConnection = null
|
||||||
|
}
|
||||||
|
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAbort() {
|
||||||
|
Log.d(TAG, "Telecom Connection onAbort $callp")
|
||||||
|
if (callp != 0L) {
|
||||||
|
Api.ua_hangup(uap, callp, 0, "")
|
||||||
|
connections.remove(callp)
|
||||||
|
} else {
|
||||||
|
pendingOutgoingConnection = null
|
||||||
|
}
|
||||||
|
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun onCallAudioStateChanged(state: CallAudioState?) {
|
||||||
|
super.onCallAudioStateChanged(state)
|
||||||
|
Log.d(TAG, "onCallAudioStateChanged: $state")
|
||||||
|
state?.let {
|
||||||
|
if (BaresipService.isMicMuted != it.isMuted) {
|
||||||
|
BaresipService.isMicMuted = it.isMuted
|
||||||
|
Api.calls_mute(it.isMuted)
|
||||||
|
BaresipService.postServiceEvent(
|
||||||
|
ServiceEvent("mic muted,${it.isMuted}", arrayListOf(uap, callp),
|
||||||
|
System.nanoTime())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val isSpeaker = it.route == CallAudioState.ROUTE_SPEAKER
|
||||||
|
BaresipService.postServiceEvent(
|
||||||
|
ServiceEvent("speaker,${isSpeaker}", arrayListOf(uap, callp), System.nanoTime())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHold() {
|
||||||
|
Log.d(TAG, "Telecom Connection onHold $callp")
|
||||||
|
val c = Call.ofCallp(callp)
|
||||||
|
if (c?.conferenceCall != true)
|
||||||
|
c?.hold()
|
||||||
|
setOnHold()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnhold() {
|
||||||
|
Log.d(TAG, "Telecom Connection onUnhold $callp")
|
||||||
|
val c = Call.ofCallp(callp)
|
||||||
|
if (c?.conferenceCall != true)
|
||||||
|
c?.resume()
|
||||||
|
setActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayDtmfTone(c: Char) {
|
||||||
|
Log.d(TAG, "Telecom Connection onPlayDtmfTone $c")
|
||||||
|
if (callp != 0L) {
|
||||||
|
val call = Call.ofCallp(callp)
|
||||||
|
call?.sendDigit(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,20 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
|
||||||
// This is needed in order to allow choosing baresip as default Phone app
|
// This is needed in order to allow choosing baresip as default Phone app
|
||||||
|
|
||||||
class InCallService : Service() {
|
class InCallService : Service() {
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
// TODO("Return the communication channel to the service.")
|
Log.d(TAG, "InCallService onBind with intent: ${intent.action}")
|
||||||
Log.d("Baresip", "InCallService onBind with intent: ${intent.action}")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*import android.telecom.InCallService
|
||||||
|
import android.telecom.Call
|
||||||
|
|
||||||
|
class InCallService : InCallService() {
|
||||||
|
override fun onCallAdded(call: Call) {
|
||||||
|
super.onCallAdded(call)
|
||||||
|
// This is triggered when the system wants YOU to show the call UI
|
||||||
|
Log.d("Baresip", "InCallService: Call added")
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|||||||
@@ -177,9 +177,6 @@ private var password = mutableStateOf("")
|
|||||||
private val selectItems = mutableStateOf(listOf<String>())
|
private val selectItems = mutableStateOf(listOf<String>())
|
||||||
private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
|
private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
|
||||||
private val showSelectItemDialog = mutableStateOf(false)
|
private val showSelectItemDialog = mutableStateOf(false)
|
||||||
private var callRunnable: Runnable? = null
|
|
||||||
private var callHandler: Handler = Handler(Looper.getMainLooper())
|
|
||||||
private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
|
|
||||||
|
|
||||||
fun NavGraphBuilder.mainScreenRoute(
|
fun NavGraphBuilder.mainScreenRoute(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
@@ -529,7 +526,7 @@ private fun TopAppBar(
|
|||||||
val recOffImage = Icons.Filled.VoiceOverOff
|
val recOffImage = Icons.Filled.VoiceOverOff
|
||||||
val recOnImage = Icons.Filled.RecordVoiceOver
|
val recOnImage = Icons.Filled.RecordVoiceOver
|
||||||
var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) }
|
var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) }
|
||||||
val isSpeakerOn = remember { mutableStateOf(Utils.isSpeakerPhoneOn(am)) }
|
val isSpeakerOn by viewModel.isSpeakerOn.collectAsState()
|
||||||
var menuExpanded by remember { mutableStateOf(false) }
|
var menuExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val about = stringResource(R.string.about)
|
val about = stringResource(R.string.about)
|
||||||
@@ -648,11 +645,22 @@ private fun TopAppBar(
|
|||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (Build.VERSION.SDK_INT >= 31)
|
val isCurrentlyOn = isSpeakerOn
|
||||||
Log.d(TAG, "Toggling speakerphone when dev/mode is " +
|
val aor = viewModel.selectedAor.value
|
||||||
"${am.communicationDevice!!.type}/${am.mode}")
|
val ua = uas.value.find { it.account.aor == aor }
|
||||||
isSpeakerOn.value = !Utils.isSpeakerPhoneOn(am)
|
val call = ua?.currentCall()
|
||||||
|
val connection = if (call != null) ConnectionService.connections[call.callp] else null
|
||||||
|
if (connection != null) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
connection.setAudioRoute(
|
||||||
|
if (isCurrentlyOn)
|
||||||
|
android.telecom.CallAudioState.ROUTE_EARPIECE
|
||||||
|
else
|
||||||
|
android.telecom.CallAudioState.ROUTE_SPEAKER
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
|
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
alertTitle.value = speakerPhoneTitle
|
alertTitle.value = speakerPhoneTitle
|
||||||
@@ -664,7 +672,7 @@ private fun TopAppBar(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.SpeakerPhone,
|
imageVector = Icons.Filled.SpeakerPhone,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (isSpeakerOn.value)
|
tint = if (isSpeakerOn)
|
||||||
MaterialTheme.colorScheme.error
|
MaterialTheme.colorScheme.error
|
||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.onPrimary,
|
MaterialTheme.colorScheme.onPrimary,
|
||||||
@@ -884,8 +892,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
|
|||||||
|
|
||||||
val calls by viewModel.calls.collectAsState()
|
val calls by viewModel.calls.collectAsState()
|
||||||
val selectedAor by viewModel.selectedAor.collectAsState()
|
val selectedAor by viewModel.selectedAor.collectAsState()
|
||||||
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor }
|
val filteredCalls = calls.filter { it.ua.account.aor == selectedAor && it.status.value != "disconnecting" }
|
||||||
|
|
||||||
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" }
|
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" }
|
||||||
val conferenceCall = filteredCalls.any { it.conferenceCall }
|
val conferenceCall = filteredCalls.any { it.conferenceCall }
|
||||||
|
|
||||||
@@ -1483,12 +1490,13 @@ private fun CallRow(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (call.terminated.value) return@IconButton
|
if (call.terminated.value) return@IconButton
|
||||||
call.terminated.value = true
|
call.terminated.value = true
|
||||||
abandonAudioFocus(ctx)
|
val connection = ConnectionService.connections[call.callp]
|
||||||
Log.d(
|
if (connection != null)
|
||||||
TAG,
|
connection.onDisconnect()
|
||||||
"AoR ${call.ua.account.aor} canceling call ${call.callp} with ${call.callUri.value}"
|
else {
|
||||||
)
|
Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}")
|
||||||
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -1509,10 +1517,14 @@ private fun CallRow(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (call.terminated.value) return@IconButton
|
if (call.terminated.value) return@IconButton
|
||||||
call.terminated.value = true
|
call.terminated.value = true
|
||||||
abandonAudioFocus(ctx)
|
val connection = ConnectionService.connections[call.callp]
|
||||||
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp} with ${call.callUri.value}")
|
if (connection != null)
|
||||||
|
connection.onDisconnect()
|
||||||
|
else {
|
||||||
|
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}")
|
||||||
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.CallEnd,
|
imageVector = Icons.Filled.CallEnd,
|
||||||
@@ -1522,20 +1534,28 @@ private fun CallRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!call.conferenceCall)
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.size(48.dp),
|
modifier = Modifier.size(48.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
val connection = ConnectionService.connections[call.callp]
|
||||||
if (call.onhold) {
|
if (call.onhold) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
|
"AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
|
||||||
)
|
)
|
||||||
|
if (connection != null)
|
||||||
|
connection.onUnhold()
|
||||||
|
else
|
||||||
call.resume()
|
call.resume()
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
|
"AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
|
||||||
)
|
)
|
||||||
|
if (connection != null)
|
||||||
|
connection.onHold()
|
||||||
|
else
|
||||||
call.hold()
|
call.hold()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1559,12 +1579,22 @@ private fun CallRow(
|
|||||||
enabled = call.transferButtonEnabled.value,
|
enabled = call.transferButtonEnabled.value,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (call.onHoldCall != null) {
|
if (call.onHoldCall != null) {
|
||||||
|
if (!Api.call_supported(call.callp, Api.REPLACES)) {
|
||||||
|
alertTitle.value = ctx.getString(R.string.notice)
|
||||||
|
alertMessage.value = ctx.getString(R.string.replaces_not_supported)
|
||||||
|
showAlert.value = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val connection = ConnectionService.connections[call.callp]
|
||||||
|
connection?.onHold()
|
||||||
if (!call.executeTransfer()) {
|
if (!call.executeTransfer()) {
|
||||||
alertTitle.value = ctx.getString(R.string.notice)
|
alertTitle.value = ctx.getString(R.string.notice)
|
||||||
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
||||||
showAlert.value = true
|
showAlert.value = true
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
showTransferDialog = true
|
showTransferDialog = true
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -1961,6 +1991,7 @@ private fun CallRow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OnHoldNotice() {
|
private fun OnHoldNotice() {
|
||||||
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
||||||
@@ -1972,6 +2003,7 @@ private fun OnHoldNotice() {
|
|||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun spinToAor(viewModel: ViewModel, aor: String) {
|
private fun spinToAor(viewModel: ViewModel, aor: String) {
|
||||||
@@ -2011,10 +2043,10 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean) {
|
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean,
|
||||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
onHoldCallp: Long = 0L) {
|
||||||
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
val aor = viewModel.selectedAor.value
|
||||||
val aor = ua.account.aor
|
val ua = UserAgent.ofAor(aor)!!
|
||||||
val peerUri = if (Utils.isTelNumber(uriText))
|
val peerUri = if (Utils.isTelNumber(uriText))
|
||||||
"tel:$uriText"
|
"tel:$uriText"
|
||||||
else
|
else
|
||||||
@@ -2035,44 +2067,31 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
|
|||||||
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
|
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
|
||||||
showAlert.value = true
|
showAlert.value = true
|
||||||
}
|
}
|
||||||
else if (Call.isAnyCallActive(ctx))
|
else if (Utils.isPSTNCallActive(ctx) || Call.calls().any { it.ua.account.aor != ua.account.aor } )
|
||||||
Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show()
|
Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show()
|
||||||
else if (!BaresipService.requestAudioFocus(ctx))
|
|
||||||
Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show()
|
|
||||||
else {
|
else {
|
||||||
viewModel.dialerState.callButtonsEnabled.value = false
|
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager
|
||||||
if (Build.VERSION.SDK_INT < 31) {
|
val extras = android.os.Bundle()
|
||||||
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
|
||||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
BaresipService.getPhoneAccountHandle(ctx))
|
||||||
runCall(ctx, viewModel, ua, uri, conferenceCall)
|
val callExtras = android.os.Bundle()
|
||||||
} else {
|
callExtras.putBoolean("conferenceCall", conferenceCall)
|
||||||
if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
|
callExtras.putLong("uap", ua.uap)
|
||||||
runCall(ctx, viewModel, ua, uri, conferenceCall)
|
if (onHoldCallp != 0L)
|
||||||
} else {
|
callExtras.putLong("onHoldCallp", onHoldCallp)
|
||||||
audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
|
extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
|
||||||
if (mode == AudioManager.MODE_IN_COMMUNICATION) {
|
try {
|
||||||
Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " +
|
Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}")
|
||||||
"device ${am.communicationDevice!!.type}")
|
tm.placeCall(uri.toUri(), extras)
|
||||||
if (audioModeChangedListener != null) {
|
} catch (e: SecurityException) {
|
||||||
am.removeOnModeChangedListener(audioModeChangedListener!!)
|
Log.e(TAG, "placeCall failed: ${e.message}")
|
||||||
audioModeChangedListener = null
|
|
||||||
}
|
|
||||||
runCall(ctx, viewModel, ua, uri, conferenceCall)
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
|
|
||||||
"device ${am.communicationDevice!!.type}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
am.addOnModeChangedListener(ctx.mainExecutor, audioModeChangedListener!!)
|
|
||||||
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
|
||||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun answer(ctx: Context, call: Call) {
|
private fun answer(ctx: Context, call: Call) {
|
||||||
Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}")
|
Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}")
|
||||||
|
ConnectionService.connections[call.callp]?.setActive()
|
||||||
val intent = Intent(ctx, BaresipService::class.java)
|
val intent = Intent(ctx, BaresipService::class.java)
|
||||||
intent.action = "Call Answer"
|
intent.action = "Call Answer"
|
||||||
intent.putExtra("uap", call.ua.uap)
|
intent.putExtra("uap", call.ua.uap)
|
||||||
@@ -2082,59 +2101,12 @@ private fun answer(ctx: Context, call: Call) {
|
|||||||
|
|
||||||
private fun reject(call: Call) {
|
private fun reject(call: Call) {
|
||||||
Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}")
|
Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}")
|
||||||
|
val connection = ConnectionService.connections[call.callp]
|
||||||
|
if (connection != null)
|
||||||
|
connection.onReject()
|
||||||
|
else {
|
||||||
call.rejected = true
|
call.rejected = true
|
||||||
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
|
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: String, conferenceCall: Boolean) {
|
|
||||||
callRunnable = Runnable {
|
|
||||||
callRunnable = null
|
|
||||||
val newCall = call(ctx, viewModel, ua, uri, conferenceCall)
|
|
||||||
if (newCall == null) {
|
|
||||||
BaresipService.abandonAudioFocus(ctx)
|
|
||||||
viewModel.dialerState.callButtonsEnabled.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun call(
|
|
||||||
ctx: Context,
|
|
||||||
viewModel: ViewModel,
|
|
||||||
ua: UserAgent,
|
|
||||||
uri: String,
|
|
||||||
conferenceCall: Boolean,
|
|
||||||
onHoldCall: Call? = null
|
|
||||||
): Call? {
|
|
||||||
spinToAor(viewModel, ua.account.aor)
|
|
||||||
if (conferenceCall && ua.calls().isEmpty())
|
|
||||||
Api.module_load("mixminus")
|
|
||||||
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
|
|
||||||
return if (callp != 0L) {
|
|
||||||
Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
|
|
||||||
val call = Call(callp, ua, uri, "out", "outgoing")
|
|
||||||
call.onHoldCall = onHoldCall
|
|
||||||
call.conferenceCall = conferenceCall
|
|
||||||
call.add()
|
|
||||||
if (onHoldCall != null)
|
|
||||||
onHoldCall.newCall = call
|
|
||||||
if (call.connect(uri)) {
|
|
||||||
showCall(ctx, viewModel, ua)
|
|
||||||
call
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "call_connect $callp failed")
|
|
||||||
if (onHoldCall != null)
|
|
||||||
onHoldCall.newCall = null
|
|
||||||
call.remove()
|
|
||||||
call.destroy()
|
|
||||||
showCall(ctx, viewModel, ua)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
|
|
||||||
if (conferenceCall && ua.calls().isEmpty())
|
|
||||||
Api.module_unload("mixminus")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2152,13 +2124,23 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
|
|||||||
val call = ua.currentCall()
|
val call = ua.currentCall()
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
if (attended) {
|
if (attended) {
|
||||||
if (call.hold()) {
|
val connection = ConnectionService.connections[call.callp]
|
||||||
|
val success = if (connection != null) {
|
||||||
|
connection.onHold()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
call.hold()
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
call.onhold = true
|
||||||
call.referTo = uri
|
call.referTo = uri
|
||||||
call(ctx, viewModel, ua, uri, false,call)
|
makeCall(ctx, viewModel, uri, false, call.callp)
|
||||||
showCall(ctx, viewModel, ua, call)
|
showCall(ctx, viewModel, ua, call)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
val connection = ConnectionService.connections[call.callp]
|
||||||
|
connection?.onHold()
|
||||||
if (!call.transfer(uri)) {
|
if (!call.transfer(uri)) {
|
||||||
alertTitle.value = ctx.getString(R.string.notice)
|
alertTitle.value = ctx.getString(R.string.notice)
|
||||||
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
alertMessage.value = ctx.getString(R.string.transfer_failed)
|
||||||
@@ -2205,8 +2187,9 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
|
|||||||
call.focusDtmf.value = true
|
call.focusDtmf.value = true
|
||||||
viewModel.requestShowKeyboard()
|
viewModel.requestShowKeyboard()
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "Showing call ${call.callp} from ${call.ua.account.aor} with status ${call.status.value}")
|
||||||
when (call.status.value) {
|
when (call.status.value) {
|
||||||
"outgoing", "transferring", "answered" -> {
|
"outgoing", "transferring", "answered", "disconnecting" -> {
|
||||||
call.callUriLabel.value = if (call.status.value == "answered")
|
call.callUriLabel.value = if (call.status.value == "answered")
|
||||||
ctx.getString(R.string.incoming_call_from_dots)
|
ctx.getString(R.string.incoming_call_from_dots)
|
||||||
else
|
else
|
||||||
@@ -2373,6 +2356,9 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
|
|||||||
"call update" -> {
|
"call update" -> {
|
||||||
showCall(ctx, viewModel, ua)
|
showCall(ctx, viewModel, ua)
|
||||||
}
|
}
|
||||||
|
"update calls" -> {
|
||||||
|
viewModel.updateCalls(Call.calls().toList())
|
||||||
|
}
|
||||||
"call verify" -> {
|
"call verify" -> {
|
||||||
val callp = params[1] as Long
|
val callp = params[1] as Long
|
||||||
val call = Call.ofCallp(callp)
|
val call = Call.ofCallp(callp)
|
||||||
@@ -2449,13 +2435,14 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
|
|||||||
val call = Call.ofCallp(callp)
|
val call = Call.ofCallp(callp)
|
||||||
if (call in Call.calls())
|
if (call in Call.calls())
|
||||||
Api.ua_hangup(uap, callp, 487, "Request Terminated")
|
Api.ua_hangup(uap, callp, 487, "Request Terminated")
|
||||||
call(ctx, viewModel, ua, ev[1], false)
|
makeCall(ctx, viewModel, ev[1], false)
|
||||||
showCall(ctx, viewModel, ua)
|
showCall(ctx, viewModel, ua)
|
||||||
}
|
}
|
||||||
"transfer failed" -> {
|
"transfer failed" -> {
|
||||||
showCall(ctx, viewModel, ua)
|
showCall(ctx, viewModel, ua)
|
||||||
}
|
}
|
||||||
"call closed" -> {
|
"call closed" -> {
|
||||||
|
viewModel.updateCalls(Call.calls().toList())
|
||||||
val activity = ctx as? Activity
|
val activity = ctx as? Activity
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
@@ -2490,6 +2477,16 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
|
|||||||
if (aor == viewModel.selectedAor.value)
|
if (aor == viewModel.selectedAor.value)
|
||||||
viewModel.triggerAccountUpdate()
|
viewModel.triggerAccountUpdate()
|
||||||
}
|
}
|
||||||
|
"mic muted" -> {
|
||||||
|
val muted = ev[1].toBoolean()
|
||||||
|
if (muted)
|
||||||
|
viewModel.updateMicIcon(Icons.Filled.MicOff)
|
||||||
|
else
|
||||||
|
viewModel.updateMicIcon(Icons.Filled.Mic)
|
||||||
|
}
|
||||||
|
"speaker" -> {
|
||||||
|
viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean())
|
||||||
|
}
|
||||||
else -> Log.e(TAG, "Unknown event '${ev[0]}'")
|
else -> Log.e(TAG, "Unknown event '${ev[0]}'")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2769,19 +2766,3 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
|
|||||||
downloadsOutputUri = null
|
downloadsOutputUri = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun abandonAudioFocus(ctx: Context) {
|
|
||||||
if (Build.VERSION.SDK_INT < 31) {
|
|
||||||
if (callRunnable != null) {
|
|
||||||
callHandler.removeCallbacks(callRunnable!!)
|
|
||||||
callRunnable = null
|
|
||||||
BaresipService.abandonAudioFocus(ctx)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (audioModeChangedListener != null) {
|
|
||||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
||||||
am.removeOnModeChangedListener(audioModeChangedListener!!)
|
|
||||||
audioModeChangedListener = null
|
|
||||||
BaresipService.abandonAudioFocus(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.core.text.isDigitsOnly
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import com.tutpro.baresip.Call.Companion.inCall
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -841,6 +842,12 @@ object Utils {
|
|||||||
Configuration.UI_MODE_NIGHT_YES
|
Configuration.UI_MODE_NIGHT_YES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isPSTNCallActive(ctx: Context): Boolean {
|
||||||
|
// MODE_IN_CALL indicates a PSTN call is active
|
||||||
|
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
return am.mode == AudioManager.MODE_IN_CALL
|
||||||
|
}
|
||||||
|
|
||||||
fun relativeTime(ctx: Context, time: GregorianCalendar): String {
|
fun relativeTime(ctx: Context, time: GregorianCalendar): String {
|
||||||
return if (DateUtils.isToday(time.timeInMillis)) {
|
return if (DateUtils.isToday(time.timeInMillis)) {
|
||||||
val fmt = DateFormat.getTimeInstance(DateFormat.SHORT)
|
val fmt = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
@@ -859,14 +866,6 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSpeakerPhoneOn(am: AudioManager): Boolean {
|
|
||||||
return if (Build.VERSION.SDK_INT >= 31)
|
|
||||||
am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
|
||||||
else
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
am.isSpeakerphoneOn
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setSpeakerPhone(executor: Executor, am: AudioManager, enable: Boolean) {
|
private fun setSpeakerPhone(executor: Executor, am: AudioManager, enable: Boolean) {
|
||||||
if (Build.VERSION.SDK_INT >= 31) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
val current = am.communicationDevice!!.type
|
val current = am.communicationDevice!!.type
|
||||||
@@ -895,7 +894,7 @@ object Utils {
|
|||||||
Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE")
|
Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE")
|
||||||
if (!am.setCommunicationDevice(speakerDevice))
|
if (!am.setCommunicationDevice(speakerDevice))
|
||||||
Log.e(TAG, "Could not set com device")
|
Log.e(TAG, "Could not set com device")
|
||||||
if (BaresipService.audioFocusRequest != null && am.mode == AudioManager.MODE_NORMAL) {
|
if (inCall() && am.mode == AudioManager.MODE_NORMAL) {
|
||||||
Log.d(TAG, "Setting mode to communication")
|
Log.d(TAG, "Setting mode to communication")
|
||||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||||
}
|
}
|
||||||
@@ -945,18 +944,6 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.S)
|
|
||||||
fun setCommunicationDevice(am: AudioManager, type: Int) {
|
|
||||||
val current = am.communicationDevice!!.type
|
|
||||||
Log.d(TAG, "Current com dev/mode $current/${am.mode}")
|
|
||||||
for (device in am.availableCommunicationDevices)
|
|
||||||
if (device.type == type) {
|
|
||||||
am.setCommunicationDevice(device)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Log.d(TAG, "New com dev/mode ${am.communicationDevice!!.type}/${am.mode}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearCommunicationDevice(am: AudioManager) {
|
private fun clearCommunicationDevice(am: AudioManager) {
|
||||||
if (Build.VERSION.SDK_INT >= 31) {
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
am.clearCommunicationDevice()
|
am.clearCommunicationDevice()
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class ViewModel: ViewModel() {
|
|||||||
private val _micIcon = MutableStateFlow(Icons.Filled.Mic)
|
private val _micIcon = MutableStateFlow(Icons.Filled.Mic)
|
||||||
val micIcon = _micIcon.asStateFlow()
|
val micIcon = _micIcon.asStateFlow()
|
||||||
|
|
||||||
|
private val _isSpeakerOn = MutableStateFlow(false)
|
||||||
|
val isSpeakerOn = _isSpeakerOn.asStateFlow()
|
||||||
|
|
||||||
private val _isDialpadVisible = MutableStateFlow(false)
|
private val _isDialpadVisible = MutableStateFlow(false)
|
||||||
val isDialpadVisible = _isDialpadVisible.asStateFlow()
|
val isDialpadVisible = _isDialpadVisible.asStateFlow()
|
||||||
|
|
||||||
@@ -109,6 +112,10 @@ class ViewModel: ViewModel() {
|
|||||||
_micIcon.value = icon
|
_micIcon.value = icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSpeakerPhoneStatus(on: Boolean) {
|
||||||
|
_isSpeakerOn.value = on
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleDialpadVisibility() {
|
fun toggleDialpadVisibility() {
|
||||||
_isDialpadVisible.value = !_isDialpadVisible.value
|
_isDialpadVisible.value = !_isDialpadVisible.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -521,6 +521,7 @@
|
|||||||
<string name="allow_video_send">Salli video videon lähetys puhelussa \'%1$s\'\?</string>
|
<string name="allow_video_send">Salli video videon lähetys puhelussa \'%1$s\'\?</string>
|
||||||
<string name="allow_video_recv">Salli video videon vastaanotto puhelussa \'%1$s\'\?</string>
|
<string name="allow_video_recv">Salli video videon vastaanotto puhelussa \'%1$s\'\?</string>
|
||||||
<string name="call_is_on_hold">Puhelu on pidossa</string>
|
<string name="call_is_on_hold">Puhelu on pidossa</string>
|
||||||
|
<string name="call_is_connected">Puhelu on yhdistetty</string>
|
||||||
<string name="rec_in_call">Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu
|
<string name="rec_in_call">Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu
|
||||||
ei ole yhdistetty</string>
|
ei ole yhdistetty</string>
|
||||||
<string name="call_transfer">Puhelun siirto</string>
|
<string name="call_transfer">Puhelun siirto</string>
|
||||||
@@ -530,6 +531,7 @@
|
|||||||
<string name="choose_destination_uri">Valitse kohteen URI</string>
|
<string name="choose_destination_uri">Valitse kohteen URI</string>
|
||||||
<string name="transfer">Siirto</string>
|
<string name="transfer">Siirto</string>
|
||||||
<string name="transfer_failed">Siirto epäonnistui</string>
|
<string name="transfer_failed">Siirto epäonnistui</string>
|
||||||
|
<string name="replaces_not_supported">Toinen osapuoli ei tue REPLACES ominaisuutta</string>
|
||||||
<string name="dtmf">DTMF</string>
|
<string name="dtmf">DTMF</string>
|
||||||
<string name="call_info">Puhelutiedot</string>
|
<string name="call_info">Puhelutiedot</string>
|
||||||
<string name="call_info_not_available">Ei saatavilla</string>
|
<string name="call_info_not_available">Ei saatavilla</string>
|
||||||
|
|||||||
@@ -505,6 +505,7 @@
|
|||||||
<string name="allow_video_send">Accept sending of video to \'%1$s\'\?</string>
|
<string name="allow_video_send">Accept sending of video to \'%1$s\'\?</string>
|
||||||
<string name="allow_video_recv">Accept receiving video from \'%1$s\'\?</string>
|
<string name="allow_video_recv">Accept receiving video from \'%1$s\'\?</string>
|
||||||
<string name="call_is_on_hold">Call is on hold</string>
|
<string name="call_is_on_hold">Call is on hold</string>
|
||||||
|
<string name="call_is_connected">Call is connected</string>
|
||||||
<string name="rec_in_call">Recording can be turned on or off only when call is not
|
<string name="rec_in_call">Recording can be turned on or off only when call is not
|
||||||
connected</string>
|
connected</string>
|
||||||
<string name="call_transfer">Call Transfer</string>
|
<string name="call_transfer">Call Transfer</string>
|
||||||
@@ -514,6 +515,7 @@
|
|||||||
<string name="choose_destination_uri">Choose destination URI</string>
|
<string name="choose_destination_uri">Choose destination URI</string>
|
||||||
<string name="transfer">Transfer</string>
|
<string name="transfer">Transfer</string>
|
||||||
<string name="transfer_failed">Transfer failed</string>
|
<string name="transfer_failed">Transfer failed</string>
|
||||||
|
<string name="replaces_not_supported">Peer does not support REPLACES feature</string>
|
||||||
<string name="dtmf">DTMF</string>
|
<string name="dtmf">DTMF</string>
|
||||||
<string name="call_info">Call Info</string>
|
<string name="call_info">Call Info</string>
|
||||||
<string name="call_info_not_available">No info available</string>
|
<string name="call_info_not_available">No info available</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user