diff --git a/app/build.gradle b/app/build.gradle index f4a492c3..682a6a26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId = 'com.tutpro.baresip' minSdkVersion 21 targetSdkVersion 27 - versionCode = 26 - versionName = '3.0.5' + versionCode = 27 + versionName = '3.1.0' externalNativeBuild { cmake { cFlags '-DHAVE_INTTYPES_H' diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index e00276b2..9f845e54 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -131,6 +131,9 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev, else re_snprintf(event_buf, sizeof event_buf, "%s", "unknown menc event"); break; + case UA_EVENT_CALL_TRANSFER: + re_snprintf(event_buf, sizeof event_buf, "call transfer,%s", prm); + break; case UA_EVENT_CALL_CLOSED: play = mem_deref(play); if (call_scode(call)) { @@ -881,6 +884,31 @@ Java_com_tutpro_baresip_MainActivity_ua_1connect(JNIEnv *env, jobject thiz, return (*env)->NewStringUTF(env, call_buf); } +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1call_1alloc(JNIEnv *env, jobject thiz, + jstring javaUA, jstring javaXCall) { + struct call *xcall, *call = NULL; + struct ua *ua; + int err; + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + const char *native_xcall = (*env)->GetStringUTFChars(env, javaXCall, 0); + char call_buf[64]; + LOGD("allocating new call for ua %s xcall %s\n", native_ua, native_xcall); + ua = (struct ua *)strtoul(native_ua, NULL, 10); + xcall = (struct call *)strtoul(native_xcall, NULL, 10); + re_thread_enter(); + err = ua_call_alloc(&call, ua, VIDMODE_OFF, NULL, xcall, call_localuri(xcall), true); + if (err) { + LOGW("call allocation for ua %s failed with error %d\n", native_ua, err); + call_buf[0] = '\0'; + } else { + sprintf(call_buf, "%lu", (unsigned long)call); + } + re_thread_leave(); + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + (*env)->ReleaseStringUTFChars(env, javaXCall, native_xcall); + return (*env)->NewStringUTF(env, call_buf); +} JNIEXPORT void JNICALL Java_com_tutpro_baresip_MainActivity_ua_1answer(JNIEnv *env, jobject thiz, @@ -900,28 +928,45 @@ Java_com_tutpro_baresip_MainActivity_ua_1answer(JNIEnv *env, jobject thiz, } JNIEXPORT jint JNICALL -Java_com_tutpro_baresip_MainActivity_ua_1hold_1answer(JNIEnv *env, jobject thiz, - jstring javaUA, jstring javaCall) { - const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); +Java_com_tutpro_baresip_MainActivity_call_1connect(JNIEnv *env, jobject thiz, jstring javaCall, + jstring javaPeer) { const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); - int ret; - LOGD("answering call %s/%s\n", native_ua, native_call); - struct ua *ua = (struct ua *) strtoul(native_ua, NULL, 10); - (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); - struct call *call; - if (strlen(native_call) > 0) - call = (struct call *) strtoul(native_call, NULL, 10); - else - call = NULL; - (*env)->ReleaseStringUTFChars(env, javaCall, native_call); - play = mem_deref(play); - audio_set_source(call_audio(ua_call(uag_current())), NULL, NULL); + const char *native_peer = (*env)->GetStringUTFChars(env, javaPeer, 0); + struct call *call = (struct call *)strtoul(native_call, NULL, 10); + LOGD("connecting call %s to %s\n", native_call, native_peer); re_thread_enter(); - call_hold(ua_call(uag_current()), true); - ua_answer(ua, call); - ret = ua_hold_answer(ua, call); + struct pl pl; + pl_set_str(&pl, native_peer); + int err = call_connect(call, &pl); re_thread_leave(); - return ret; + if (err) LOGW("call_connect error: %d\n", err); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + (*env)->ReleaseStringUTFChars(env, javaPeer, native_peer); + return err; +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_call_1start_1audio(JNIEnv *env, jobject thiz, jstring javaCall) { + const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); + struct call *call = (struct call *)strtoul(native_call, NULL, 10); + LOGD("starting audio of call %s\n", native_call); + re_thread_enter(); + audio_start(call_audio(call)); + re_thread_leave(); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + return; +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_call_1stop_1audio(JNIEnv *env, jobject thiz, jstring javaCall) { + const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); + struct call *call = (struct call *)strtoul(native_call, NULL, 10); + LOGD("stopping audio of call %s\n", native_call); + re_thread_enter(); + audio_stop(call_audio(call)); + re_thread_leave(); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + return; } JNIEXPORT jint JNICALL @@ -932,27 +977,26 @@ Java_com_tutpro_baresip_MainActivity_call_1hold(JNIEnv *env, jobject thiz, int ret; LOGD("holding call %s\n", native_call); (*env)->ReleaseStringUTFChars(env, javaCall, native_call); - struct audio *au = call_audio(call); - audio_set_hold(au, true); - audio_set_source(au, NULL, NULL); re_thread_enter(); + struct audio *au = call_audio(call); + //audio_set_hold(au, true); + // audio_set_source(au, NULL, NULL); ret = call_hold(call, true); re_thread_leave(); return ret; } JNIEXPORT jint JNICALL -Java_com_tutpro_baresip_MainActivity_call_1unhold(JNIEnv *env, jobject thiz, - jstring javaCall) { +Java_com_tutpro_baresip_MainActivity_call_1unhold(JNIEnv *env, jobject thiz, jstring javaCall) { const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); struct call *call = (struct call *) strtoul(native_call, NULL, 10); int ret; LOGD("unholding call %s\n", native_call); (*env)->ReleaseStringUTFChars(env, javaCall, native_call); - struct audio *au = call_audio(call); - audio_set_hold(au, false); - audio_set_source(au, "opensles", "nil"); re_thread_enter(); + struct audio *au = call_audio(call); + // audio_set_hold(au, false); + // audio_set_source(au, "opensles", "nil"); ret = call_hold(call, false); re_thread_leave(); return ret; @@ -988,8 +1032,10 @@ Java_com_tutpro_baresip_MainActivity_call_1send_1digit(JNIEnv *env, jobject thiz const uint16_t native_digit = digit; struct call *call = (struct call *)strtoul(native_call, NULL, 10); LOGD("sending DTMF digit '%c' to call %s\n", (char)native_digit, native_call); + re_thread_enter(); int res = call_send_digit(call, (char)native_digit); if (!res) res = call_send_digit(call, KEYCODE_REL); + re_thread_leave(); (*env)->ReleaseStringUTFChars(env, javaCall, native_call); return res; } @@ -1005,7 +1051,9 @@ Java_com_tutpro_baresip_MessageActivity_message_1send(JNIEnv *env, jobject thiz, const char *native_time = (*env)->GetStringUTFChars(env, javaTime, 0); LOGD("sending message from ua %s to %s at %s\n", native_ua, native_peer, native_time); ua = (struct ua *)strtoul(native_ua, NULL, 10); + re_thread_enter(); int err = message_send(ua, native_peer, native_msg, send_resp_handler, (void *)native_time); + re_thread_leave(); if (err) { LOGW("message_send failed with error %d\n", err); } @@ -1015,6 +1063,23 @@ Java_com_tutpro_baresip_MessageActivity_message_1send(JNIEnv *env, jobject thiz, return err; } +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_call_1notify_1sipfrag(JNIEnv *env, jobject thiz, + jstring javaCall, jint code, jstring reason) { + const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); + struct call *call = (struct call *)strtoul(native_call, NULL, 10); + const uint16_t native_code = code; + const char *native_reason = (*env)->GetStringUTFChars(env, reason, 0); + LOGD("notifying call %s/%s\n", native_call, native_reason); + re_thread_enter(); + (void)call_notify_sipfrag(call, native_code, native_reason); + re_thread_leave(); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + (*env)->ReleaseStringUTFChars(env, reason, native_reason); + return; +} + + JNIEXPORT void JNICALL Java_com_tutpro_baresip_ContactsActivity_00024Companion_contacts_1remove(JNIEnv *env, jobject thiz) { struct le *le; diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 6980ddb6..2e9fdf6e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -78,7 +78,7 @@ class Account(val accp: String) { else res = res + ";vm_uri=\"$vmUri\"" - res = res + ";ptime=20;regint=${regint};regq=0.5;pubint=0;answermode=manual;call_transfer=no" + res = res + ";ptime=20;regint=${regint};regq=0.5;pubint=0;answermode=manual;call_transfer=yes" return res } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index be56436a..455160f0 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -121,80 +121,7 @@ class MainActivity : AppCompatActivity() { val ua = uas[position] Log.d("Baresip", "Setting $aor current") uag_current_set(uas[position].uap) - val callsOut = Call.uaCalls(calls, ua, "out") - val callsIn = Call.uaCalls(calls, ua, "in") - if ((callsOut.size == 0) && (callsIn.size == 0)) { - callTitle.text = "Outgoing call to ..." - callUri.text.clear() - callUri.hint = "Callee" - securityButton.visibility = View.INVISIBLE - callButton.visibility = View.VISIBLE - hangupButton.visibility = View.INVISIBLE - answerButton.visibility = View.INVISIBLE - rejectButton.visibility = View.INVISIBLE - dtmf.visibility = View.INVISIBLE - } else { - val call: Call - if (callsOut.size > 0) { - callTitle.text = "Outgoing call to ..." - call = callsOut[0] - } else { - callTitle.text = "Incoming call from ..." - call = callsIn[0] - } - callUri.setText(call.peerURI) - when (call.status) { - "outgoing" -> { - securityButton.visibility = View.INVISIBLE - callButton.visibility = View.INVISIBLE - hangupButton.visibility = View.VISIBLE - answerButton.visibility = View.INVISIBLE - rejectButton.visibility = View.INVISIBLE - holdButton.visibility = View.INVISIBLE - dtmf.visibility = View.INVISIBLE - } - "incoming" -> { - securityButton.visibility = View.INVISIBLE - callButton.visibility = View.INVISIBLE - hangupButton.visibility = View.INVISIBLE - answerButton.visibility = View.VISIBLE - answerButton.isEnabled = true - rejectButton.visibility = View.VISIBLE - rejectButton.isEnabled = true - holdButton.visibility = View.INVISIBLE - dtmf.visibility = View.INVISIBLE - if (answerCall == call.callp) { - answerCall = "" - answerButton.performClick() - } - if (rejectCall == call.callp) { - rejectCall = "" - rejectButton.performClick() - moveTaskToBack(true) - } - } - "connected" -> { - securityButton.setImageResource(call.security) - setSecurityButtonTag(securityButton, call.security) - if ((acc.mediaenc == "zrtp") || (acc.mediaenc == "dtls_srtpf")) - securityButton.visibility = View.VISIBLE - else - securityButton.visibility = View.INVISIBLE - callButton.visibility = View.INVISIBLE - hangupButton.visibility = View.VISIBLE - answerButton.visibility = View.INVISIBLE - rejectButton.visibility = View.INVISIBLE - if (call.onhold) { - holdButton.setImageResource(R.drawable.play) - } else { - holdButton.setImageResource(R.drawable.pause) - } - holdButton.visibility = View.VISIBLE - dtmf.visibility = View.VISIBLE - dtmf.requestFocus() - } - } - } + showCall(ua) if (acc.vmUri != "") { if (acc.vmNew > 0) voicemailButton.setImageResource(R.drawable.voicemail_new) @@ -305,10 +232,13 @@ class MainActivity : AppCompatActivity() { hangupButton.setOnClickListener { val ua = uas[aorSpinner.selectedItemPosition] val aor = ua.account.aor - val callp = Call.uaCalls(calls, ua,"")[0].callp - Log.d("Baresip", "AoR $aor hanging up call $callp with ${callUri.text}") - hangupButton.isEnabled = false - ua_hangup(ua.uap, callp, 0, "") + val uaCalls = Call.uaCalls(calls, ua, "") + if (uaCalls.size > 0) { + val callp = uaCalls[uaCalls.size - 1].callp + Log.d("Baresip", "AoR $aor hanging up call $callp with ${callUri.text}") + hangupButton.isEnabled = false + ua_hangup(ua.uap, callp, 0, "") + } } answerButton.setOnClickListener { @@ -521,6 +451,10 @@ class MainActivity : AppCompatActivity() { call.onhold = false call.security = R.drawable.box_red if (ua == uas[aorSpinner.selectedItemPosition]) { + if (call.dir == "in") + callTitle.text = "Incoming call from ..." + else + callTitle.text = "Outgoing call to ..." if (acc.mediaenc == "") { securityButton.visibility = View.INVISIBLE } else { @@ -619,6 +553,29 @@ class MainActivity : AppCompatActivity() { securityButton.visibility = View.VISIBLE } } + "call transfer" -> { + val callp = params[1] + val call = Call.find(calls, callp) + if (call == null) { + Log.e("Baresip", "Call $callp to be transfered is not found") + return + } + val transferDialog = AlertDialog.Builder(this) + transferDialog.setMessage("Do you want to transfer this call to ${ev[1]}?") + transferDialog.setPositiveButton("Yes") { dialog, _ -> + if (call in calls) + transfer(ua, call, ev[1]) + else + call(ua, ev[1]) + dialog.dismiss() + } + transferDialog.setNegativeButton("No") { dialog, _ -> + if (call in calls) + call_notify_sipfrag(callp, 603, "Decline") + dialog.dismiss() + } + transferDialog.create().show() + } "call closed" -> { val callp = params[1] val call = Call.find(calls, callp) @@ -629,20 +586,12 @@ class MainActivity : AppCompatActivity() { Log.d("Baresip", "Removing call ${uap}/${callp}/" + call.peerURI) stopRinging() if (ua == uas[aorSpinner.selectedItemPosition]) { - callTitle.text = "Outgoing call to ..." - callUri.setText("") - callUri.hint = "Callee" - securityButton.visibility = View.INVISIBLE - callButton.visibility = View.VISIBLE - hangupButton.visibility = View.INVISIBLE - answerButton.visibility = View.INVISIBLE - rejectButton.visibility = View.INVISIBLE - holdButton.visibility = View.INVISIBLE - dtmf.visibility = View.INVISIBLE - imm.hideSoftInputFromWindow(dtmf.windowToken, 0) dtmf.removeTextChangedListener(call.dtmfWatcher) } calls.remove(call) + if (ua == uas[aorSpinner.selectedItemPosition]) { + showCall(ua) + } if (!call.hasHistory) { if (CallHistory.aorHistory(history, aor) > HISTORY_SIZE) CallHistory.aorRemoveHistory(history, aor) @@ -654,7 +603,12 @@ class MainActivity : AppCompatActivity() { callsButton.setImageResource(R.drawable.calls_missed) } } - if (calls.size == 0) am.mode = AudioManager.MODE_NORMAL + if (calls.size == 0) { + am.mode = AudioManager.MODE_NORMAL + } else { + val uaCalls = Call.uaCalls(calls, ua, "") + if (uaCalls.size > 0) call_start_audio(uaCalls[uaCalls.size - 1].callp) + } if (am.isSpeakerphoneOn) { am.isSpeakerphoneOn = false val speakerIcon = findViewById(R.id.speakerIcon) as ActionMenuItemView @@ -977,6 +931,29 @@ class MainActivity : AppCompatActivity() { } } + private fun transfer(ua: UserAgent, call: Call, uri: String) { + val newCallp = ua_call_alloc(ua.uap, call.callp) + if (newCallp != "") { + Log.d("Baresip", "Adding outgoing call ${ua.uap}/$newCallp/$uri") + val newCall = Call(newCallp, ua, uri, "out", "transferring", + dtmfWatcher(newCallp)) + calls.add(newCall) + call_stop_audio(call.callp) + call_start_audio(newCallp) + val err = call_connect(newCallp, uri) + if (err == 0) { + if (ua == uas[aorSpinner.selectedItemPosition]) showCall(ua) + } else { + call_start_audio(call.callp) + Log.w("Baresip", "call_connect $newCallp failed with error $err") + call_notify_sipfrag(call.callp, 500, "Call Error") + } + } else { + Log.w("Baresip", "ua_call_alloc ${ua.uap}/${call.callp} failed") + call_notify_sipfrag(call.callp, 500, "Call Error") + } + } + private fun dtmfWatcher(callp: String): TextWatcher { return object : TextWatcher { override fun beforeTextChanged(sequence: CharSequence, start: Int, count: Int, after: Int) {} @@ -1010,6 +987,88 @@ class MainActivity : AppCompatActivity() { } } + private fun showCall(ua: UserAgent) { + if (Call.uaCalls(calls, ua, "").size == 0) { + callTitle.text = "Outgoing call to ..." + callUri.text.clear() + callUri.hint = "Callee" + securityButton.visibility = View.INVISIBLE + callButton.visibility = View.VISIBLE + hangupButton.visibility = View.INVISIBLE + answerButton.visibility = View.INVISIBLE + rejectButton.visibility = View.INVISIBLE + holdButton.visibility = View.INVISIBLE + dtmf.visibility = View.INVISIBLE + } else { + val callsOut = Call.uaCalls(calls, ua, "out") + val callsIn = Call.uaCalls(calls, ua, "in") + val call: Call + if (callsOut.size > 0) { + call = callsOut[callsOut.size - 1] + if (call.status == "transferring") + callTitle.text = "Transferring call to ..." + else + callTitle.text = "Outgoing call to ..." + } else { + callTitle.text = "Incoming call from ..." + call = callsIn[callsIn.size - 1] + } + callUri.setText(call.peerURI) + when (call.status) { + "outgoing", "transferring" -> { + securityButton.visibility = View.INVISIBLE + callButton.visibility = View.INVISIBLE + hangupButton.visibility = View.VISIBLE + answerButton.visibility = View.INVISIBLE + rejectButton.visibility = View.INVISIBLE + holdButton.visibility = View.INVISIBLE + dtmf.visibility = View.INVISIBLE + } + "incoming" -> { + securityButton.visibility = View.INVISIBLE + callButton.visibility = View.INVISIBLE + hangupButton.visibility = View.INVISIBLE + answerButton.visibility = View.VISIBLE + answerButton.isEnabled = true + rejectButton.visibility = View.VISIBLE + rejectButton.isEnabled = true + holdButton.visibility = View.INVISIBLE + dtmf.visibility = View.INVISIBLE + if (answerCall == call.callp) { + answerCall = "" + answerButton.performClick() + } + if (rejectCall == call.callp) { + rejectCall = "" + rejectButton.performClick() + moveTaskToBack(true) + } + } + "connected" -> { + securityButton.setImageResource(call.security) + setSecurityButtonTag(securityButton, call.security) + if ((ua.account.mediaenc == "zrtp") || (ua.account.mediaenc == "dtls_srtpf")) + securityButton.visibility = View.VISIBLE + else + securityButton.visibility = View.INVISIBLE + callButton.visibility = View.INVISIBLE + hangupButton.visibility = View.VISIBLE + hangupButton.isEnabled = true + answerButton.visibility = View.INVISIBLE + rejectButton.visibility = View.INVISIBLE + if (call.onhold) { + holdButton.setImageResource(R.drawable.play) + } else { + holdButton.setImageResource(R.drawable.pause) + } + holdButton.visibility = View.VISIBLE + dtmf.visibility = View.VISIBLE + dtmf.requestFocus() + } + } + } + } + private fun requestAudioFocus(stream: Int) { if (!audioFocused) { val res = am.requestAudioFocus(null, stream, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) @@ -1047,12 +1106,16 @@ class MainActivity : AppCompatActivity() { external fun uag_current_set(uap: String) external fun ua_connect(uap: String, peer_uri: String): String + external fun ua_call_alloc(uap: String, xcallp: String): String external fun ua_answer(uap: String, callp: String) - external fun ua_hold_answer(uap: String, callp: String): Int external fun ua_hangup(uap: String, callp: String, code: Int, reason: String) external fun call_hold(callp: String): Int external fun call_unhold(callp: String): Int external fun call_send_digit(callp: String, digit: Char): Int + external fun call_connect(callp: String, peer_uri: String): Int + external fun call_notify_sipfrag(callp: String, code: Int, reason: String) + external fun call_start_audio(callp: String) + external fun call_stop_audio(callp: String) external fun reload_config(): Int companion object {