Added attented transfer + setting to skip dialer when adding new call or transfering a call + fixed toast instead of snack for dialer transfer message

This commit is contained in:
Sylvain Berfini 2023-03-14 17:33:35 +01:00
parent 74fd59a541
commit 09bde054d0
12 changed files with 109 additions and 26 deletions

View file

@ -15,6 +15,7 @@ Group changes to describe their impact on the project, as follows:
### Added ### Added
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts) - Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts)
- Confirmation dialog before removing account - Confirmation dialog before removing account
- Attended transfer instead of blind transfer if there is more than 1 call
### Changed ### Changed
- Account EXPIRES is now set to 1 month instead of 1 year for sip.linphone.org accounts - Account EXPIRES is now set to 1 month instead of 1 year for sip.linphone.org accounts

View file

@ -66,7 +66,7 @@ fun popupTo(
/* Main activity related */ /* Main activity related */
internal fun MainActivity.navigateToDialer(args: Bundle?) { internal fun MainActivity.navigateToDialer(args: Bundle? = null) {
findNavController(R.id.nav_host_fragment).navigate( findNavController(R.id.nav_host_fragment).navigate(
R.id.action_global_dialerFragment, R.id.action_global_dialerFragment,
args, args,
@ -90,6 +90,14 @@ internal fun MainActivity.navigateToChatRoom(localAddress: String?, peerAddress:
) )
} }
internal fun MainActivity.navigateToContacts() {
findNavController(R.id.nav_host_fragment).navigate(
R.id.action_global_masterContactsFragment,
null,
popupTo(R.id.masterContactsFragment, true)
)
}
internal fun MainActivity.navigateToContact(contactId: String?) { internal fun MainActivity.navigateToContact(contactId: String?) {
val deepLink = "linphone-android://contact/view/$contactId" val deepLink = "linphone-android://contact/view/$contactId"
findNavController(R.id.nav_host_fragment).navigate( findNavController(R.id.nav_host_fragment).navigate(

View file

@ -339,9 +339,15 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
intent.hasExtra("Dialer") -> { intent.hasExtra("Dialer") -> {
Log.i("[Main Activity] Found dialer intent extra, go to dialer") Log.i("[Main Activity] Found dialer intent extra, go to dialer")
val args = Bundle() val isTransfer = intent.getBooleanExtra("Transfer", false)
args.putBoolean("Transfer", intent.getBooleanExtra("Transfer", false)) sharedViewModel.pendingCallTransfer = isTransfer
navigateToDialer(args) navigateToDialer()
}
intent.hasExtra("Contacts") -> {
Log.i("[Main Activity] Found contacts intent extra, go to contacts list")
val isTransfer = intent.getBooleanExtra("Transfer", false)
sharedViewModel.pendingCallTransfer = isTransfer
navigateToContacts()
} }
else -> { else -> {
val core = coreContext.core val core = coreContext.core

View file

@ -31,7 +31,6 @@ import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
@ -152,8 +151,8 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
viewModel.onMessageToNotifyEvent.observe( viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
it.consume { id -> it.consume { resourceId ->
Toast.makeText(requireContext(), id, Toast.LENGTH_SHORT).show() (requireActivity() as MainActivity).showSnackBar(resourceId)
} }
} }
@ -162,18 +161,21 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
return return
} }
if (arguments?.containsKey("Transfer") == true) {
sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false
Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}")
}
if (arguments?.containsKey("URI") == true) { if (arguments?.containsKey("URI") == true) {
val address = arguments?.getString("URI") ?: "" val address = arguments?.getString("URI") ?: ""
Log.i("[Dialer] Found URI to call: $address") Log.i("[Dialer] Found URI to call: $address")
val skipAutoCall = arguments?.getBoolean("SkipAutoCallStart") ?: false val skipAutoCall = arguments?.getBoolean("SkipAutoCallStart") ?: false
if (corePreferences.callRightAway && !skipAutoCall) { if (corePreferences.skipDialerForNewCallAndTransfer) {
Log.i("[Dialer] Call right away setting is enabled, start the call to $address") if (sharedViewModel.pendingCallTransfer) {
Log.i("[Dialer] We were asked to skip dialer so starting new call to [$address] now")
viewModel.transferCallTo(address)
} else {
Log.i("[Dialer] We were asked to skip dialer so starting transfer to [$address] now")
viewModel.directCall(address)
}
} else if (corePreferences.callRightAway && !skipAutoCall) {
Log.i("[Dialer] Call right away setting is enabled, start the call to [$address]")
viewModel.directCall(address) viewModel.directCall(address)
} else { } else {
sharedViewModel.dialerUri = address sharedViewModel.dialerUri = address
@ -184,6 +186,8 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}") Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}")
viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer
viewModel.autoInitiateVideoCalls.value = coreContext.core.videoActivationPolicy.automaticallyInitiate
checkForUpdate() checkForUpdate()
checkPermissions() checkPermissions()

View file

@ -98,6 +98,13 @@ class DialerViewModel : LogsUploadViewModel() {
atLeastOneCall.value = core.callsNb > 0 atLeastOneCall.value = core.callsNb > 0
} }
override fun onTransferStateChanged(core: Core, transfered: Call, callState: Call.State) {
if (callState == Call.State.OutgoingProgress) {
// Will work for both blind & attended transfer
onMessageToNotifyEvent.value = Event(org.linphone.R.string.dialer_transfer_succeded)
}
}
override fun onNetworkReachable(core: Core, reachable: Boolean) { override fun onNetworkReachable(core: Core, reachable: Boolean) {
val address = addressWaitingNetworkToBeCalled.orEmpty() val address = addressWaitingNetworkToBeCalled.orEmpty()
if (reachable && address.isNotEmpty()) { if (reachable && address.isNotEmpty()) {
@ -207,13 +214,7 @@ class DialerViewModel : LogsUploadViewModel() {
fun transferCall(): Boolean { fun transferCall(): Boolean {
val addressToCall = enteredUri.value.orEmpty() val addressToCall = enteredUri.value.orEmpty()
return if (addressToCall.isNotEmpty()) { return if (addressToCall.isNotEmpty()) {
onMessageToNotifyEvent.value = Event( transferCallTo(addressToCall)
if (coreContext.transferCallTo(addressToCall)) {
org.linphone.R.string.dialer_transfer_succeded
} else {
org.linphone.R.string.dialer_transfer_failed
}
)
eraseAll() eraseAll()
true true
} else { } else {
@ -222,6 +223,12 @@ class DialerViewModel : LogsUploadViewModel() {
} }
} }
fun transferCallTo(addressToCall: String) {
if (!coreContext.transferCallTo(addressToCall)) {
onMessageToNotifyEvent.value = Event(org.linphone.R.string.dialer_transfer_failed)
}
}
fun switchCamera() { fun switchCamera() {
coreContext.switchCamera() coreContext.switchCamera()
} }

View file

@ -249,7 +249,11 @@ class ConferenceCallFragment : GenericFragment<VoipConferenceCallFragmentBinding
it.consume { isCallTransfer -> it.consume { isCallTransfer ->
val intent = Intent() val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java) intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Dialer", true) if (corePreferences.skipDialerForNewCallAndTransfer) {
intent.putExtra("Contacts", true)
} else {
intent.putExtra("Dialer", true)
}
intent.putExtra("Transfer", isCallTransfer) intent.putExtra("Transfer", isCallTransfer)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)

View file

@ -161,7 +161,11 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
it.consume { isCallTransfer -> it.consume { isCallTransfer ->
val intent = Intent() val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java) intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Dialer", true) if (corePreferences.skipDialerForNewCallAndTransfer) {
intent.putExtra("Contacts", true)
} else {
intent.putExtra("Dialer", true)
}
intent.putExtra("Transfer", isCallTransfer) intent.putExtra("Transfer", isCallTransfer)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)

View file

@ -79,6 +79,8 @@ class ControlsViewModel : ViewModel() {
val showTakeSnapshotButton = MutableLiveData<Boolean>() val showTakeSnapshotButton = MutableLiveData<Boolean>()
val attendedTransfer = MutableLiveData<Boolean>()
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy { val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
@ -117,6 +119,7 @@ class ControlsViewModel : ViewModel() {
Log.i("[Call Controls] State changed: $state") Log.i("[Call Controls] State changed: $state")
isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia
isIncomingEarlyMediaVideo.value = state == Call.State.IncomingEarlyMedia && call.remoteParams?.isVideoEnabled == true isIncomingEarlyMediaVideo.value = state == Call.State.IncomingEarlyMedia && call.remoteParams?.isVideoEnabled == true
attendedTransfer.value = core.callsNb > 1
if (state == Call.State.StreamsRunning) { if (state == Call.State.StreamsRunning) {
if (!call.currentParams.isVideoEnabled && fullScreenMode.value == true) { if (!call.currentParams.isVideoEnabled && fullScreenMode.value == true) {
@ -412,7 +415,44 @@ class ControlsViewModel : ViewModel() {
goToConferenceLayoutSettingsEvent.value = Event(true) goToConferenceLayoutSettingsEvent.value = Event(true)
} }
fun goToDialerForCallTransfer() { fun transferCall() {
// In case there is more than 1 call, transfer will be attended instead of blind
if (coreContext.core.callsNb > 1) {
attendedTransfer()
} else {
goToDialerForCallTransfer()
}
}
private fun attendedTransfer() {
val core = coreContext.core
val currentCall = core.currentCall
if (currentCall == null) {
Log.e("[Call Controls] Can't do an attended transfer without a current call")
return
}
if (core.callsNb <= 1) {
Log.e("[Call Controls] Need at least two calls to do an attended transfer")
return
}
val callToTransferTo = core.calls.findLast {
it.state == Call.State.Paused
}
if (callToTransferTo == null) {
Log.e("[Call Controls] Couldn't find a call in Paused state to transfer current call to")
return
}
Log.i("[Call Controls] Doing an attended transfer between active call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${callToTransferTo.remoteAddress.asStringUriOnly()}]")
val result = callToTransferTo.transferToAnother(currentCall)
if (result != 0) {
Log.e("[Call Controls] Attended transfer failed!")
}
}
private fun goToDialerForCallTransfer() {
goToDialerEvent.value = Event(true) goToDialerEvent.value = Event(true)
} }

View file

@ -317,6 +317,13 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "call_right_away", value) config.setBool("app", "call_right_away", value)
} }
// Will send user to contacts list directly
var skipDialerForNewCallAndTransfer: Boolean
get() = config.getBool("app", "skip_dialer_for_new_call_and_transfer", false)
set(value) {
config.setBool("app", "skip_dialer_for_new_call_and_transfer", value)
}
var automaticallyStartCallRecording: Boolean var automaticallyStartCallRecording: Boolean
get() = config.getBool("app", "auto_start_call_record", false) get() = config.getBool("app", "auto_start_call_record", false)
set(value) { set(value) {

View file

@ -108,8 +108,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:onClick="@{() -> controlsViewModel.goToDialerForCallTransfer()}" android:onClick="@{() -> controlsViewModel.transferCall()}"
android:text="@string/call_action_transfer_call" android:text="@{controlsViewModel.attendedTransfer ? @string/call_action_attended_transfer_call : @string/call_action_transfer_call, default=@string/call_action_transfer_call}"
android:visibility="@{conferenceViewModel.conferenceExists ? View.GONE : View.VISIBLE, default=gone}" android:visibility="@{conferenceViewModel.conferenceExists ? View.GONE : View.VISIBLE, default=gone}"
app:drawableTopCompat="@drawable/icon_call_forward" app:drawableTopCompat="@drawable/icon_call_forward"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View file

@ -768,4 +768,5 @@
<string name="account_setting_delete_dialog_title">Voulez-vous supprimer votre compte ?</string> <string name="account_setting_delete_dialog_title">Voulez-vous supprimer votre compte ?</string>
<string name="account_setting_delete_generic_confirmation_dialog">Votre compte sera supprimé localement.\nPour le supprimer de manière définitive, rendez-vous sur le site internet de votre fournisseur SIP.</string> <string name="account_setting_delete_generic_confirmation_dialog">Votre compte sera supprimé localement.\nPour le supprimer de manière définitive, rendez-vous sur le site internet de votre fournisseur SIP.</string>
<string name="account_setting_delete_sip_linphone_org_confirmation_dialog">Votre compte sera supprimé localement.\nPour le supprimer de manière définitive, rendez-vous sur notre plateforme de gestion des comptes :</string> <string name="account_setting_delete_sip_linphone_org_confirmation_dialog">Votre compte sera supprimé localement.\nPour le supprimer de manière définitive, rendez-vous sur notre plateforme de gestion des comptes :</string>
<string name="call_action_attended_transfer_call">Transfert supervisé</string>
</resources> </resources>

View file

@ -348,6 +348,7 @@
<string name="call_action_statistics">Call statistics</string> <string name="call_action_statistics">Call statistics</string>
<string name="call_action_add_call">Start new call</string> <string name="call_action_add_call">Start new call</string>
<string name="call_action_transfer_call">Transfer call</string> <string name="call_action_transfer_call">Transfer call</string>
<string name="call_action_attended_transfer_call">Attended transfer</string>
<string name="call_context_action_resume">Resume call</string> <string name="call_context_action_resume">Resume call</string>
<string name="call_context_action_pause">Pause call</string> <string name="call_context_action_pause">Pause call</string>
<string name="call_context_action_transfer">Transfer call</string> <string name="call_context_action_transfer">Transfer call</string>