diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index a2ab76c8..7195c077 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -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) { diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 4f5ee232..c94336b5 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -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 diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 2b190446..ecdb3878 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -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 { - val result = ArrayList() - 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 - } - } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index 42b8b958..24597f51 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -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 + private var executeTransferDialog: androidx.appcompat.app.AlertDialog? = null private lateinit var accountsRequest: ActivityResultLauncher private lateinit var chatRequests: ActivityResultLauncher @@ -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? = null diff --git a/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt index 26a19410..33ceac7a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt @@ -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, +class UaSpinnerAdapter(private val cxt: Context, private val uas: ArrayList, private val images: ArrayList) : ArrayAdapter(cxt, android.R.layout.simple_spinner_item, images) { @@ -43,8 +44,11 @@ class UaSpinnerAdapter(cxt: Context, private val uas: ArrayList, 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 diff --git a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt index b505ec40..b86c9b78 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt @@ -19,6 +19,20 @@ class UserAgent(val uap: Long) { BaresipService.status[BaresipService.uas.indexOf(this)] = status } + fun calls(dir: String = ""): ArrayList { + val result = ArrayList() + 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 { diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index eb79433a..3eef0819 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -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 || diff --git a/app/src/main/res/layout/call_transfer_dialog.xml b/app/src/main/res/layout/call_transfer_dialog.xml index 18283f8d..133aa939 100644 --- a/app/src/main/res/layout/call_transfer_dialog.xml +++ b/app/src/main/res/layout/call_transfer_dialog.xml @@ -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" /> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index cf5d43f5..fb051b24 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -452,8 +452,11 @@ Puhelu on pidossa Mikrofoni päälle/pois Puhelun siirto + Sokeasti + Osallistu Siirron kohde Siirto + Suorita siirto Siirto epäonnistui DTMF Puhelutiedot diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a895496e..22107ff3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -412,8 +412,11 @@ Call is on hold Microphone On/Off Call Transfer + Blind + Attended Transfer destination Transfer + Execute Transfer Transfer Failed DTMF Call Info