diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt
index e80702c07..a56b61de1 100644
--- a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt
@@ -30,12 +30,14 @@ import androidx.lifecycle.ViewModel
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
+import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.MediaDirection
import org.linphone.core.MediaEncryption
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
+import org.linphone.utils.AudioRouteUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
@@ -105,10 +107,18 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
@WorkerThread
override fun onStateChanged(call: Call, state: Call.State?, message: String) {
+ if (CurrentCallViewModel@call != call) {
+ return
+ }
+
if (LinphoneUtils.isCallOutgoing(call.state)) {
isVideoEnabled.postValue(call.params.isVideoEnabled)
} else {
val videoEnabled = call.currentParams.isVideoEnabled
+ if (videoEnabled && isVideoEnabled.value == false) {
+ Log.i("$TAG Video enabled, routing audio to speaker")
+ AudioRouteUtils.routeAudioToSpeaker(call)
+ }
isVideoEnabled.postValue(videoEnabled)
// Toggle full screen OFF when remote disables video
@@ -117,6 +127,12 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
}
+
+ @WorkerThread
+ override fun onAudioDeviceChanged(call: Call, audioDevice: AudioDevice) {
+ Log.i("$TAG Audio device changed [$audioDevice]")
+ isSpeakerEnabled.postValue(AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed(call))
+ }
}
init {
@@ -133,8 +149,9 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
call = currentCall
Log.i("$TAG Found call [$call]")
configureCall(call)
+ isSpeakerEnabled.postValue(AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed(call))
} else {
- Log.e("$TAG Failed to find outgoing call!")
+ Log.e("$TAG Failed to find call!")
}
showSwitchCamera.postValue(coreContext.showSwitchCameraButton())
@@ -155,16 +172,20 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
@UiThread
fun answer() {
coreContext.postOnCoreThread {
- Log.i("$TAG Answering call [$call]")
- call.accept()
+ if (::call.isInitialized) {
+ Log.i("$TAG Answering call [$call]")
+ call.accept()
+ }
}
}
@UiThread
fun hangUp() {
coreContext.postOnCoreThread {
- Log.i("$TAG Terminating call [$call]")
- call.terminate()
+ if (::call.isInitialized) {
+ Log.i("$TAG Terminating call [$call]")
+ call.terminate()
+ }
}
}
@@ -187,15 +208,30 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
// TODO: request record audio permission
return
}
+
coreContext.postOnCoreThread {
- call.microphoneMuted = !call.microphoneMuted
- isMicrophoneMuted.postValue(call.microphoneMuted)
+ if (::call.isInitialized) {
+ call.microphoneMuted = !call.microphoneMuted
+ isMicrophoneMuted.postValue(call.microphoneMuted)
+ }
}
}
@UiThread
fun changeAudioOutputDevice() {
- // TODO: display list of all output devices
+ // TODO: display list of all output devices if more then earpiece &
+
+ val routeAudioToSpeaker = isSpeakerEnabled.value != true
+
+ coreContext.postOnCoreThread {
+ if (::call.isInitialized) {
+ if (routeAudioToSpeaker) {
+ AudioRouteUtils.routeAudioToSpeaker(call)
+ } else {
+ AudioRouteUtils.routeAudioToEarpiece(call)
+ }
+ }
+ }
}
@UiThread
@@ -258,14 +294,6 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
- @WorkerThread
- fun forceShowZrtpSasDialog() {
- val authToken = call.authenticationToken
- if (authToken.orEmpty().isNotEmpty()) {
- showZrtpSasDialog(authToken!!.uppercase(Locale.getDefault()))
- }
- }
-
@WorkerThread
private fun showZrtpSasDialog(authToken: String) {
val toRead: String
diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt
new file mode 100644
index 000000000..7d4bc2ec9
--- /dev/null
+++ b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.utils
+
+import androidx.annotation.WorkerThread
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.AudioDevice
+import org.linphone.core.Call
+import org.linphone.core.tools.Log
+
+class AudioRouteUtils {
+ companion object {
+ private const val TAG = "[Audio Route Utils]"
+
+ @WorkerThread
+ fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean {
+ val currentCall = if (coreContext.core.callsNb > 0) {
+ call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
+ } else {
+ Log.w("$TAG No call found, checking audio route on Core")
+ null
+ }
+ val conference = coreContext.core.conference
+
+ val audioDevice = if (conference != null && conference.isIn) {
+ conference.outputAudioDevice
+ } else if (currentCall != null) {
+ currentCall.outputAudioDevice
+ } else {
+ coreContext.core.outputAudioDevice
+ }
+
+ if (audioDevice == null) return false
+ Log.i(
+ "$TAG Playback audio device currently in use is [${audioDevice.deviceName} (${audioDevice.driverName}) ${audioDevice.type}]"
+ )
+ return audioDevice.type == AudioDevice.Type.Speaker
+ }
+
+ @WorkerThread
+ fun routeAudioToEarpiece(call: Call? = null) {
+ routeAudioTo(call, arrayListOf(AudioDevice.Type.Earpiece))
+ }
+
+ @WorkerThread
+ fun routeAudioToSpeaker(call: Call? = null) {
+ routeAudioTo(call, arrayListOf(AudioDevice.Type.Speaker))
+ }
+
+ @WorkerThread
+ private fun routeAudioTo(
+ call: Call?,
+ types: List
+ ) {
+ val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull()
+ if (currentCall != null) {
+ applyAudioRouteChange(currentCall, types)
+ changeCaptureDeviceToMatchAudioRoute(currentCall, types)
+ } else {
+ applyAudioRouteChange(call, types)
+ changeCaptureDeviceToMatchAudioRoute(call, types)
+ }
+ }
+
+ @WorkerThread
+ private fun applyAudioRouteChange(
+ call: Call?,
+ types: List,
+ output: Boolean = true
+ ) {
+ val currentCall = if (coreContext.core.callsNb > 0) {
+ call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
+ } else {
+ Log.w("$TAG No call found, setting audio route on Core")
+ null
+ }
+
+ val capability = if (output) {
+ AudioDevice.Capabilities.CapabilityPlay
+ } else {
+ AudioDevice.Capabilities.CapabilityRecord
+ }
+ val preferredDriver = if (output) {
+ coreContext.core.defaultOutputAudioDevice?.driverName
+ } else {
+ coreContext.core.defaultInputAudioDevice?.driverName
+ }
+
+ val extendedAudioDevices = coreContext.core.extendedAudioDevices
+ Log.i(
+ "$TAG Looking for an ${if (output) "output" else "input"} audio device with capability [$capability], driver name [$preferredDriver] and type [$types] in extended audio devices list (size ${extendedAudioDevices.size})"
+ )
+ val foundAudioDevice = extendedAudioDevices.find {
+ it.driverName == preferredDriver && types.contains(it.type) && it.hasCapability(
+ capability
+ )
+ }
+ val audioDevice = if (foundAudioDevice == null) {
+ Log.w(
+ "$TAG Failed to find an audio device with capability [$capability], driver name [$preferredDriver] and type [$types]"
+ )
+ extendedAudioDevices.find {
+ types.contains(it.type) && it.hasCapability(capability)
+ }
+ } else {
+ foundAudioDevice
+ }
+
+ if (audioDevice == null) {
+ Log.e(
+ "$TAG Couldn't find audio device with capability [$capability] and type [$types]"
+ )
+ for (device in extendedAudioDevices) {
+ Log.i(
+ "$TAG Extended audio device: [${device.deviceName} (${device.driverName}) ${device.type} / ${device.capabilities}]"
+ )
+ }
+ return
+ }
+ if (currentCall != null) {
+ Log.i(
+ "$TAG Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing call audio to it"
+ )
+ if (output) {
+ currentCall.outputAudioDevice = audioDevice
+ } else {
+ currentCall.inputAudioDevice = audioDevice
+ }
+ } else {
+ Log.i(
+ "$TAG Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], changing core default audio device"
+ )
+ if (output) {
+ coreContext.core.outputAudioDevice = audioDevice
+ } else {
+ coreContext.core.inputAudioDevice = audioDevice
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List) {
+ when (types.first()) {
+ AudioDevice.Type.Earpiece, AudioDevice.Type.Speaker -> {
+ Log.i(
+ "$TAG Audio route requested to Earpiece or Speaker, setting input to Microphone"
+ )
+ applyAudioRouteChange(call, (arrayListOf(AudioDevice.Type.Microphone)), false)
+ }
+ else -> {
+ Log.w("$TAG Unexpected audio device type: ${types.first()}")
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/voip_call_main_actions.xml b/app/src/main/res/layout/voip_call_main_actions.xml
index 585b9d7a6..e4917bd41 100644
--- a/app/src/main/res/layout/voip_call_main_actions.xml
+++ b/app/src/main/res/layout/voip_call_main_actions.xml
@@ -81,7 +81,7 @@
android:layout_height="@dimen/voip_button_size"
android:layout_marginEnd="30dp"
android:padding="@dimen/voip_button_icon_padding"
- android:src="@{viewModel.isSpeakerEnabled ? @drawable/speaker_slash : @drawable/speaker_high, default=@drawable/speaker_slash}"
+ android:src="@{viewModel.isSpeakerEnabled ? @drawable/speaker_high : @drawable/speaker_slash, default=@drawable/speaker_slash}"
android:background="@drawable/in_call_button_background"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"