Added initial support for attended call transfer

Use red colored spinner text when account has active call
This commit is contained in:
Juha Heinanen
2022-03-11 11:35:05 +02:00
parent 05f601a4dd
commit 6406f15617
10 changed files with 231 additions and 60 deletions

View File

@ -1277,6 +1277,21 @@ Java_com_tutpro_baresip_Call_call_1has_1video(JNIEnv *env, jobject thiz, jlong c
return call_has_video((struct call *)call) ? true : false;
}
JNIEXPORT jboolean JNICALL
Java_com_tutpro_baresip_Call_call_1replaces(JNIEnv *env, jobject thiz, jlong call)
{
return call_supported((struct call *)call, REPLACES) ? true : false;
}
JNIEXPORT jboolean JNICALL
Java_com_tutpro_baresip_Call_call_1replace_1transfer(JNIEnv *env, jobject thiz, jlong xferCall, jlong call)
{
re_thread_enter();
int res = call_replace_transfer((struct call *)xferCall, (struct call *)call);
re_thread_leave();
return res == 0 ? true : false;
}
JNIEXPORT jint JNICALL
Java_com_tutpro_baresip_Api_message_1send(JNIEnv *env, jobject thiz, jlong ua, jstring jPeer, jstring jMsg, jstring jTime)
{

View File

@ -838,13 +838,18 @@ class BaresipService: Service() {
Log.d(TAG, "AoR $aor call $callp is closed")
stopRinging()
stopMediaPlayer()
/* when (ev[2]) {
"busy" ->
playMedia(R.raw.busy, 1)
"error" ->
playMedia(R.raw.error, 1)
} */
call!!.remove()
val newCall = call!!.newCall
if (newCall != null) {
newCall.onHoldCall = null
call.newCall = null
}
val onHoldCall = call.onHoldCall
if (onHoldCall != null) {
onHoldCall.newCall = null
onHoldCall.referTo = ""
call.onHoldCall = null
}
call.remove()
if (Call.calls().size == 0) {
resetCallVolume()
am.isSpeakerphoneOn = false
@ -911,7 +916,7 @@ class BaresipService: Service() {
return
}
val reason = ev[1].trim()
if ((reason != "") && (Call.uaCalls(ua, "").size == 0)) {
if ((reason != "") && (ua.calls().isEmpty())) {
if (reason[0].isDigit())
toast("${getString(R.string.call_failed)}: $reason")
else

View File

@ -8,6 +8,8 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
var onhold = false
var held = false
var onHoldCall: Call? = null
var newCall: Call? = null
var rejected = false
var security = 0
var zid = ""
@ -15,31 +17,39 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
var referTo = ""
fun add() {
BaresipService.calls.add(this)
BaresipService.calls.add(0, this)
}
fun remove() {
BaresipService.calls.remove(this)
}
fun connect(uri: String): Int {
return call_connect(callp, uri)
fun connect(uri: String): Boolean {
return call_connect(callp, uri) == 0
}
fun hold(): Int {
return call_hold(callp, true)
fun hold(): Boolean {
return call_hold(callp, true) == 0
}
fun resume(): Int {
return call_hold(callp, false)
fun resume(): Boolean {
return call_hold(callp, false) == 0
}
fun transfer(uri: String): Int {
fun transfer(uri: String): Boolean {
val err = call_hold(callp, true)
if (err != 0)
return err
return false
referTo = uri
return call_transfer(callp, uri)
return call_transfer(callp, uri) == 0
}
fun executeTransfer(): Boolean {
return if (this.onHoldCall != null) {
call_hold(callp, true)
call_replace_transfer(this.onHoldCall!!.callp, callp)
} else
false
}
fun sendDigit(digit: Char): Int {
@ -54,7 +64,6 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return call_duration(callp)
}
fun stats(stream: String): String {
return call_stats(callp, stream)
}
@ -63,6 +72,10 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return call_audio_codecs(callp)
}
fun replaces(): Boolean {
return call_replaces(callp)
}
init {
if (ua.account.mediaEnc != "") security = R.drawable.box_red
}
@ -78,6 +91,8 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
private external fun call_duration(callp: Long): Int
private external fun call_stats(callp: Long, stream: String): String
private external fun call_has_video(callp: Long): Boolean
private external fun call_replaces(callp: Long): Boolean
private external fun call_replace_transfer(xfer_callp: Long, callp: Long): Boolean
companion object {
@ -85,13 +100,6 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return BaresipService.calls
}
fun uaCalls(ua: UserAgent, dir: String): ArrayList<Call> {
val result = ArrayList<Call>()
for (c in BaresipService.calls)
if ((c.ua == ua) && ((dir == "") || c.dir == dir)) result.add(c)
return result
}
fun ofCallp(callp: Long): Call? {
for (c in BaresipService.calls)
if (c.callp == callp) return c
@ -104,12 +112,5 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return null
}
fun isStatus(status: String): Boolean {
for (call in BaresipService.calls)
if (call.status == status)
return true
return false
}
}
}

View File

@ -3,6 +3,7 @@ package com.tutpro.baresip
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.KeyguardManager
import android.app.NotificationManager
import android.content.*
@ -75,6 +76,7 @@ class MainActivity : AppCompatActivity() {
private var speakerIcon: MenuItem? = null
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private var executeTransferDialog: androidx.appcompat.app.AlertDialog? = null
private lateinit var accountsRequest: ActivityResultLauncher<Intent>
private lateinit var chatRequests: ActivityResultLauncher<Intent>
@ -297,7 +299,8 @@ class MainActivity : AppCompatActivity() {
setTitle(R.string.info)
setMessage(getString(R.string.call_is_secure))
setPositiveButton(getString(R.string.unverify)) { dialog, _ ->
val calls = Call.uaCalls(UserAgent.uas()[aorSpinner.selectedItemPosition], "")
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val calls = ua.calls()
if (calls.size > 0) {
if (Api.cmd_exec("zrtp_unverify " + calls[0].zid) != 0) {
Log.e(TAG, "Command 'zrtp_unverify ${calls[0].zid}' failed")
@ -329,7 +332,7 @@ class MainActivity : AppCompatActivity() {
hangupButton.setOnClickListener {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val aor = ua.account.aor
val uaCalls = Call.uaCalls(ua, "")
val uaCalls = ua.calls()
if (uaCalls.size > 0) {
val call = uaCalls[uaCalls.size - 1]
val callp = call.callp
@ -344,7 +347,7 @@ class MainActivity : AppCompatActivity() {
answerButton.setOnClickListener {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val aor = ua.account.aor
val callp = Call.uaCalls(ua, "in")[0].callp
val callp = ua.calls("in")[0].callp
Log.d(TAG, "AoR $aor answering call $callp from ${callUri.text}")
answerButton.isEnabled = false
rejectButton.isEnabled = false
@ -359,7 +362,7 @@ class MainActivity : AppCompatActivity() {
rejectButton.setOnClickListener {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val aor = ua.account.aor
val call = Call.uaCalls(ua, "in")[0]
val call = ua.calls("in")[0]
val callp = call.callp
Log.d(TAG, "AoR $aor rejecting call $callp from ${callUri.text}")
answerButton.isEnabled = false
@ -371,7 +374,7 @@ class MainActivity : AppCompatActivity() {
holdButton.setOnClickListener {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val aor = ua.account.aor
val call = Call.uaCalls(ua, "")[0]
val call = ua.calls()[0]
if (call.onhold) {
Log.d(TAG, "AoR $aor resuming call ${call.callp} with ${callUri.text}")
call.resume()
@ -391,7 +394,7 @@ class MainActivity : AppCompatActivity() {
infoButton.setOnClickListener {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val calls = Call.uaCalls(ua, "")
val calls = ua.calls()
if (calls.size > 0) {
val call = calls[0]
val stats = call.stats("audio")
@ -982,6 +985,7 @@ class MainActivity : AppCompatActivity() {
"call incoming", "call outgoing" -> {
val callp = params[1] as Long
if (BaresipService.isMainVisible) {
uaAdapter.notifyDataSetChanged()
if (aor != aorSpinner.tag)
spinToAor(aor)
showCall(ua, Call.ofCallp(callp))
@ -1110,6 +1114,14 @@ class MainActivity : AppCompatActivity() {
showCall(ua)
}
"call closed" -> {
uaAdapter.notifyDataSetChanged()
val call = ua.currentCall()
if (call != null) {
call.resume()
startCallTimer(call)
} else {
callTimer.stop()
}
if (aor == aorSpinner.tag) {
callUri.inputType = InputType.TYPE_CLASS_TEXT +
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
@ -1122,7 +1134,6 @@ class MainActivity : AppCompatActivity() {
}
if (speakerIcon != null)
speakerIcon!!.setIcon(R.drawable.speaker_off)
callTimer.stop()
if ((Build.VERSION.SDK_INT >= 22 && kgm.isDeviceLocked) ||
(Build.VERSION.SDK_INT < 22 && kgm.isKeyguardLocked && kgm.isKeyguardSecure))
Utils.setShowWhenLocked(this, false)
@ -1371,14 +1382,18 @@ class MainActivity : AppCompatActivity() {
}
private fun makeTransfer(ua: UserAgent) {
val layout = LayoutInflater.from(this)
.inflate(R.layout.call_transfer_dialog, findViewById(android.R.id.content),
false)
.inflate(R.layout.call_transfer_dialog, findViewById(android.R.id.content), false)
val blind = layout.findViewById(R.id.blind) as CheckBox
val attended = layout.findViewById(R.id.attended) as CheckBox
val transferUri = layout.findViewById(R.id.transferUri) as AutoCompleteTextView
transferUri.setAdapter(ArrayAdapter(this, android.R.layout.select_dialog_item,
Contact.contactNames()))
transferUri.threshold = 2
transferUri.requestFocus()
val builder = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
with(builder) {
setView(layout)
@ -1398,8 +1413,19 @@ class MainActivity : AppCompatActivity() {
Utils.alertView(this@MainActivity, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_or_tel_uri), uri))
} else {
if (Call.uaCalls(ua, "").size > 0) {
Call.uaCalls(ua, "")[0].transfer(uri)
val call = ua.currentCall()
if (call != null) {
if (attended.isChecked) {
if (call.hold()) {
call.referTo = uri
call(ua, uri, call)
}
} else {
if (!call.transfer(uri)) {
Utils.alertView(this@MainActivity, getString(R.string.notice),
String.format(getString(R.string.transfer_failed)))
}
}
showCall(ua)
}
}
@ -1411,6 +1437,27 @@ class MainActivity : AppCompatActivity() {
}
}
val alertDialog = builder.create()
val call = ua.currentCall() ?: return
val blindOrAttended = layout.findViewById(R.id.blindOrAttended) as RelativeLayout
if (call.replaces()) {
blind.setOnClickListener {
if (blind.isChecked) {
attended.isChecked = false
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).text = getString(R.string.transfer)
}
}
attended.setOnClickListener {
if (attended.isChecked) {
blind.isChecked = false
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).text = getString(R.string.call)
}
}
blindOrAttended.visibility = View.VISIBLE
} else {
blindOrAttended.visibility = View.GONE
}
// This needs to be done after dialog has been created and before it is shown
alertDialog.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
alertDialog.show()
@ -1608,20 +1655,23 @@ class MainActivity : AppCompatActivity() {
}
private fun call(ua: UserAgent, uri: String): Boolean {
private fun call(ua: UserAgent, uri: String, onHoldCall: Call? = null): Boolean {
if (ua.account.aor != aorSpinner.tag)
spinToAor(ua.account.aor)
val callp = Api.ua_call_alloc(ua.uap, 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", Utils.dtmfWatcher(callp))
call.onHoldCall = onHoldCall
call.add()
val err = call.connect(uri)
if (err == 0) {
if (onHoldCall != null)
onHoldCall.newCall = call
if (call.connect(uri)) {
showCall(ua)
true
} else {
Log.w(TAG, "call_connect $callp failed with error $err")
showCall(ua)
Log.w(TAG, "call_connect $callp failed")
false
}
} else {
@ -1637,13 +1687,12 @@ class MainActivity : AppCompatActivity() {
val newCall = Call(newCallp, ua, uri, "out", "transferring",
Utils.dtmfWatcher(newCallp))
newCall.add()
val err = newCall.connect(uri)
if (err == 0) {
if (newCall.connect(uri)) {
if (ua.account.aor != aorSpinner.tag)
spinToAor(ua.account.aor)
showCall(ua)
} else {
Log.w(TAG, "call_connect $newCallp failed with error $err")
Log.w(TAG, "call_connect $newCallp failed")
call.notifySipfrag(500, "Call Error")
}
} else {
@ -1712,6 +1761,7 @@ class MainActivity : AppCompatActivity() {
hangupButton.visibility = View.INVISIBLE
hangupButton.isEnabled = false
} else {
uaAdapter.notifyDataSetChanged()
callButton.visibility = View.INVISIBLE
callButton.isEnabled = false
hangupButton.visibility = View.VISIBLE
@ -1728,7 +1778,8 @@ class MainActivity : AppCompatActivity() {
}
private fun showCall(ua: UserAgent, showCall: Call? = null) {
if (Call.uaCalls(ua, "").size == 0) {
val call = showCall ?: ua.currentCall()
if (call == null) {
swipeRefresh.isEnabled = true
callTitle.text = getString(R.string.outgoing_call_to_dots)
callTimer.visibility = View.INVISIBLE
@ -1757,7 +1808,6 @@ class MainActivity : AppCompatActivity() {
onHoldNotice.visibility = View.GONE
} else {
swipeRefresh.isEnabled = false
val call = showCall ?: Call.uaCalls(ua, "")[0]
callUri.isFocusable = false
when (call.status) {
"outgoing", "transferring", "answered" -> {
@ -1813,9 +1863,26 @@ class MainActivity : AppCompatActivity() {
Utils.aorDomain(ua.account.aor)))
transferButton.isEnabled = true
}
callTimer.base = SystemClock.elapsedRealtime() - (call.duration() * 1000L)
callTimer.stop()
callTimer.start()
if (call.onHoldCall != null) {
val builder = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
with(builder) {
setTitle(getString(R.string.execute_transfer))
setPositiveButton(getText(R.string.yes)) { dialog, _ ->
dialog.dismiss()
call.executeTransfer()
}
setNeutralButton(getText(R.string.no)) { dialog, _ ->
dialog.dismiss()
Api.ua_hangup(ua.uap, call.callp, 0, "")
}
}
executeTransferDialog = builder.create()
executeTransferDialog!!.show()
} else {
executeTransferDialog?.dismiss()
executeTransferDialog = null
}
startCallTimer(call)
callTimer.visibility = View.VISIBLE
if (ua.account.mediaEnc == "") {
securityButton.visibility = View.INVISIBLE
@ -1981,13 +2048,19 @@ class MainActivity : AppCompatActivity() {
private fun saveCallUri() {
if (UserAgent.uas().isNotEmpty() && aorSpinner.selectedItemPosition >= 0) {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
if (Call.uaCalls(ua, "").size == 0)
if (ua.calls().isEmpty())
ua.account.resumeUri = callUri.text.toString()
else
ua.account.resumeUri = ""
}
}
private fun startCallTimer(call: Call) {
callTimer.stop()
callTimer.base = SystemClock.elapsedRealtime() - (call.duration() * 1000L)
callTimer.start()
}
companion object {
var accountRequest: ActivityResultLauncher<Intent>? = null

View File

@ -7,10 +7,11 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat.getColor
import java.util.ArrayList
class UaSpinnerAdapter(cxt: Context, private val uas: ArrayList<UserAgent>,
class UaSpinnerAdapter(private val cxt: Context, private val uas: ArrayList<UserAgent>,
private val images: ArrayList<Int>) :
ArrayAdapter<Int>(cxt, android.R.layout.simple_spinner_item, images) {
@ -43,8 +44,11 @@ class UaSpinnerAdapter(cxt: Context, private val uas: ArrayList<UserAgent>,
viewHolder = rowView.tag as ViewHolder
}
viewHolder.textView.text = uas[position].account.aor.split(":")[1]
val ua = uas[position]
viewHolder.textView.text = ua.account.aor.split(":")[1]
viewHolder.textView.textSize = 17f
if (ua.calls().isNotEmpty())
viewHolder.textView.setTextColor(getColor(cxt, R.color.colorRed))
viewHolder.imageView.setImageResource(images[position])
return rowView

View File

@ -19,6 +19,20 @@ class UserAgent(val uap: Long) {
BaresipService.status[BaresipService.uas.indexOf(this)] = status
}
fun calls(dir: String = ""): ArrayList<Call> {
val result = ArrayList<Call>()
for (c in BaresipService.calls)
if ((c.ua == this) && ((dir == "") || c.dir == dir)) result.add(c)
return result
}
fun currentCall(): Call? {
for (c in BaresipService.calls)
if (c.ua == this)
return c
return null
}
companion object {
fun uas(): ArrayList<UserAgent> {

View File

@ -123,6 +123,7 @@ object Utils {
val params = uriParams(u)
if (uri.startsWith("<") && (uri.endsWith(">")))
u = uri.substring(1).substringBeforeLast(">")
u = u.substringBefore("?")
u = u.replace(":5060", "")
u = u.replace(";transport=udp", "", true)
if (u.split(":").size == 3 ||

View File

@ -11,15 +11,67 @@
android:paddingTop="?dialogPreferredPadding"
android:paddingStart="?dialogPreferredPadding"
android:paddingEnd="?dialogPreferredPadding"
android:paddingBottom="8dp"
android:textSize="20sp"
android:text="@string/call_transfer"
android:textColor="@color/colorPrimary" />
<RelativeLayout
android:id="@+id/blindOrAttended"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="?dialogPreferredPadding"
android:paddingEnd="?dialogPreferredPadding"
android:visibility="gone"
android:orientation="horizontal" >
<TextView
android:id="@+id/blindTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="16sp"
android:text="@string/blind" >
</TextView>
<CheckBox
android:id="@+id/blind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/blindTitle"
android:checked="true" >
</CheckBox>
<TextView
android:id="@+id/attendedTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/attended"
android:textSize="16sp"
android:text="@string/attended" >
</TextView>
<CheckBox
android:id="@+id/attended"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:checked="false" >
</CheckBox>
</RelativeLayout>
<TextView
android:id="@+id/transferDestination"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="?dialogPreferredPadding"
android:paddingEnd="?dialogPreferredPadding"
android:paddingStart="?dialogPreferredPadding"
android:paddingBottom="12dp"
android:textSize="16sp"
android:text="@string/transfer_destination"
android:textColor="@color/colorStrong" />

View File

@ -452,8 +452,11 @@
<string name="call_is_on_hold">Puhelu on pidossa</string>
<string name="mic">Mikrofoni päälle/pois</string>
<string name="call_transfer">Puhelun siirto</string>
<string name="blind">Sokeasti</string>
<string name="attended">Osallistu</string>
<string name="transfer_destination">Siirron kohde</string>
<string name="transfer">Siirto</string>
<string name="execute_transfer">Suorita siirto</string>
<string name="transfer_failed">Siirto epäonnistui</string>
<string name="dtmf">DTMF</string>
<string name="call_info">Puhelutiedot</string>

View File

@ -412,8 +412,11 @@
<string name="call_is_on_hold">Call is on hold</string>
<string name="mic">Microphone On/Off</string>
<string name="call_transfer">Call Transfer</string>
<string name="blind">Blind</string>
<string name="attended">Attended</string>
<string name="transfer_destination">Transfer destination</string>
<string name="transfer">Transfer</string>
<string name="execute_transfer">Execute Transfer</string>
<string name="transfer_failed">Transfer Failed</string>
<string name="dtmf">DTMF</string>
<string name="call_info">Call Info</string>