From a0e06eb066961d829cf0a3ae4e5eeaacd2570e6d Mon Sep 17 00:00:00 2001
From: Simon Morlat <simon.morlat@linphone.org>
Date: Wed, 19 Mar 2025 13:14:12 +0100
Subject: [PATCH] In order to workaround issues with certain PBX, add a
 property [sip]/pause_before_transfer to request calls to be automatically
 paused before requesting a transfer.

---
 src/call/call.cpp                        | 36 ++++++++++++++++++++++--
 src/conference/session/call-session.cpp  |  5 ----
 src/conference/session/call-session.h    |  1 -
 src/conference/session/media-session.cpp |  2 +-
 tester/call_multi_tester.c               | 35 +++++++++++++++++++----
 5 files changed, 64 insertions(+), 15 deletions(-)

diff --git a/src/call/call.cpp b/src/call/call.cpp
index a0580811cc..b962861f2f 100644
--- a/src/call/call.cpp
+++ b/src/call/call.cpp
@@ -18,7 +18,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <bctoolbox/defs.h>
+#include "bctoolbox/defs.h"
 
 #include "c-wrapper/c-wrapper.h"
 #include "call.h"
@@ -1001,11 +1001,41 @@ LinphoneStatus Call::transfer(const shared_ptr<Call> &dest) {
 }
 
 LinphoneStatus Call::transfer(const string &dest) {
-	return getActiveSession()->transfer(dest);
+	auto address = getCore()->interpretUrl(dest, true);
+	return transfer(*address);
 }
 
 LinphoneStatus Call::transfer(const Address &dest) {
-	return getActiveSession()->transfer(dest);
+	LinphoneStatus ret = 0;
+	int pauseBeforeTransfer =
+	    linphone_config_get_int(linphone_core_get_config(getCore()->getCCore()), "sip", "pause_before_transfer", 0);
+	if (pauseBeforeTransfer && getState() != CallSession::State::Paused) {
+		lInfo() << *this << " must be paused before transfer.";
+		ret = pause();
+		if (ret == 0) {
+			/* request a future action to be executed when reaching the Paused state */
+			getActiveSession()->addPendingAction([this, dest]() {
+				switch (getState()) {
+					case CallSession::State::Paused:
+						lInfo() << "Call is now paused, requesting the transfer.";
+						getActiveSession()->transfer(dest);
+						break;
+					case CallSession::State::Pausing:
+						return -1; // try again
+						break;
+					default:
+						// unexpected
+						lWarning() << "The call could not be paused, transfer request aborted.";
+						break;
+				}
+				/* Even in failure case we return 0 because we don't want the action to be re-tried. */
+				return 0;
+			});
+		}
+	} else {
+		ret = getActiveSession()->transfer(dest);
+	}
+	return ret;
 }
 
 LinphoneStatus Call::updateFromConference(const MediaSessionParams *msp) {
diff --git a/src/conference/session/call-session.cpp b/src/conference/session/call-session.cpp
index b6045f1bb3..d08b8cb6cf 100644
--- a/src/conference/session/call-session.cpp
+++ b/src/conference/session/call-session.cpp
@@ -1842,11 +1842,6 @@ LinphoneStatus CallSession::transfer(const Address &address) {
 	return 0;
 }
 
-LinphoneStatus CallSession::transfer(const string &dest) {
-	auto address = getCore()->interpretUrl(dest, true);
-	return transfer(*address);
-}
-
 LinphoneStatus CallSession::update(const CallSessionParams *csp,
                                    const UpdateMethod method,
                                    const string &subject,
diff --git a/src/conference/session/call-session.h b/src/conference/session/call-session.h
index d48c6425c2..79651523f8 100644
--- a/src/conference/session/call-session.h
+++ b/src/conference/session/call-session.h
@@ -144,7 +144,6 @@ public:
 	LinphoneStatus terminate(const LinphoneErrorInfo *ei = nullptr);
 	LinphoneStatus transfer(const std::shared_ptr<CallSession> &dest);
 	LinphoneStatus transfer(const Address &dest);
-	LinphoneStatus transfer(const std::string &dest);
 	LinphoneStatus update(const CallSessionParams *csp,
 	                      const UpdateMethod method = UpdateMethod::Default,
 	                      const std::string &subject = "",
diff --git a/src/conference/session/media-session.cpp b/src/conference/session/media-session.cpp
index 6bd8a71543..6c20061c6f 100644
--- a/src/conference/session/media-session.cpp
+++ b/src/conference/session/media-session.cpp
@@ -23,7 +23,7 @@
 
 #include <algorithm>
 
-#include <bctoolbox/defs.h>
+#include "bctoolbox/defs.h"
 
 #include "mediastreamer2/mediastream.h"
 #include "mediastreamer2/mseventqueue.h"
diff --git a/tester/call_multi_tester.c b/tester/call_multi_tester.c
index ccc89eb63c..e31c10ab1c 100644
--- a/tester/call_multi_tester.c
+++ b/tester/call_multi_tester.c
@@ -253,7 +253,7 @@ static void incoming_call_accepted_when_outgoing_call_in_outgoing_ringing_early_
 	incoming_call_accepted_when_outgoing_call_in_state(LinphoneCallOutgoingEarlyMedia);
 }
 
-static void _simple_call_transfer(bool_t transferee_is_default_account) {
+static void _simple_call_transfer(bool_t transferee_is_default_account, bool_t pause_before_transfer) {
 	LinphoneCoreManager *marie = linphone_core_manager_new("marie_dual_proxy_rc");
 	LinphoneCoreManager *pauline = linphone_core_manager_new("pauline_tcp_rc");
 	LinphoneCoreManager *laure = linphone_core_manager_new(get_laure_rc());
@@ -269,6 +269,10 @@ static void _simple_call_transfer(bool_t transferee_is_default_account) {
 
 	char *marie_identity = linphone_address_as_string(marie->identity);
 
+	if (pause_before_transfer) {
+		linphone_config_set_int(linphone_core_get_config(pauline->lc), "sip", "pause_before_transfer", 1);
+	}
+
 	// Marie calls Pauline
 	BC_ASSERT_TRUE(call(marie, pauline));
 	marie_calling_pauline = linphone_core_get_current_call(marie->lc);
@@ -293,10 +297,26 @@ static void _simple_call_transfer(bool_t transferee_is_default_account) {
 	// Pauline transfers call from Marie to Laure
 	linphone_call_transfer(pauline_called_by_marie, laure_identity);
 	bctbx_free(laure_identity);
+	if (pause_before_transfer) {
+		BC_ASSERT_TRUE(wait_for_list(lcs, &pauline->stat.number_of_LinphoneCallPausing, 1, 2000));
+		BC_ASSERT_TRUE(wait_for_list(lcs, &pauline->stat.number_of_LinphoneCallPaused, 1, 10000));
+		BC_ASSERT_TRUE(wait_for_list(lcs, &marie->stat.number_of_LinphoneCallPausedByRemote, 1, 10000));
+	}
+
 	BC_ASSERT_TRUE(wait_for_list(lcs, &marie->stat.number_of_LinphoneCallRefered, 1, 2000));
+	if (!pause_before_transfer) {
+		BC_ASSERT_EQUAL(pauline->stat.number_of_LinphoneCallPausing, 0, int, "%i");
+		BC_ASSERT_EQUAL(pauline->stat.number_of_LinphoneCallPaused, 0, int, "%i");
+		BC_ASSERT_EQUAL(marie->stat.number_of_LinphoneCallPausedByRemote, 0, int, "%i");
+	}
+
 	// marie pausing pauline
 	BC_ASSERT_TRUE(wait_for_list(lcs, &marie->stat.number_of_LinphoneCallPausing, 1, 2000));
-	BC_ASSERT_TRUE(wait_for_list(lcs, &pauline->stat.number_of_LinphoneCallPausedByRemote, 1, 2000));
+	if (!pause_before_transfer) {
+		BC_ASSERT_TRUE(wait_for_list(lcs, &pauline->stat.number_of_LinphoneCallPausedByRemote, 1, 2000));
+	} else {
+		// Pauline's call is already in Paused state, it won't transition to PausedByRemote.
+	}
 	BC_ASSERT_TRUE(wait_for_list(lcs, &marie->stat.number_of_LinphoneCallPaused, 1, 2000));
 	// marie calling laure
 	BC_ASSERT_TRUE(wait_for_list(lcs, &marie->stat.number_of_LinphoneCallOutgoingProgress, 1, 2000));
@@ -343,11 +363,15 @@ end:
 }
 
 static void simple_call_transfer(void) {
-	_simple_call_transfer(TRUE);
+	_simple_call_transfer(TRUE, FALSE);
 }
 
 static void simple_call_transfer_from_non_default_account(void) {
-	_simple_call_transfer(FALSE);
+	_simple_call_transfer(FALSE, FALSE);
+}
+
+static void simple_call_transfer_with_pause_before(void) {
+	_simple_call_transfer(TRUE, TRUE);
 }
 
 static void unattended_call_transfer(void) {
@@ -1535,7 +1559,7 @@ end:
 	linphone_core_manager_destroy(pauline);
 }
 
-test_t multi_call_tests[] = {
+static test_t multi_call_tests[] = {
     TEST_NO_TAG("Call waiting indication", call_waiting_indication),
     TEST_NO_TAG("Call waiting indication with privacy", call_waiting_indication_with_privacy),
     TEST_NO_TAG("Second call rejected if first one in progress", second_call_rejected_if_first_one_in_progress),
@@ -1555,6 +1579,7 @@ test_t multi_call_tests[] = {
     // call_with_ice_negotiations_ending_while_accepting_call_back_to_back, "ICE"),
     TEST_NO_TAG("Simple call transfer", simple_call_transfer),
     TEST_NO_TAG("Simple call transfer from non default account", simple_call_transfer_from_non_default_account),
+    TEST_NO_TAG("Simple call transfer with pause before", simple_call_transfer_with_pause_before),
     TEST_NO_TAG("Unattended call transfer", unattended_call_transfer),
     TEST_NO_TAG("Unattended call transfer with error", unattended_call_transfer_with_error),
     TEST_NO_TAG("Call transfer existing outgoing call", call_transfer_existing_call_outgoing_call),
-- 
GitLab