Compare commits

...

2 Commits

Author SHA1 Message Date
Avently e2d00fb68a android <= 11 2024-10-11 16:53:54 +07:00
Avently 98ccf5a17c android: prevent WebView from changing audio device when toggling mic 2024-10-11 16:27:49 +07:00
2 changed files with 51 additions and 1 deletions
@@ -7,9 +7,12 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withBGApi
import dev.icerock.moko.resources.ImageResource
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import java.util.concurrent.Executors
interface CallAudioDeviceManagerInterface {
@@ -19,6 +22,7 @@ interface CallAudioDeviceManagerInterface {
fun stop()
// AudioDeviceInfo.AudioDeviceType
fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean)
fun selectSameDeviceOnWebViewChange()
// AudioDeviceInfo.AudioDeviceType
fun selectDevice(id: Int)
@@ -35,12 +39,15 @@ interface CallAudioDeviceManagerInterface {
@RequiresApi(Build.VERSION_CODES.S)
class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var resetCurrentToDevice: AudioDeviceInfo? = null
private var resetCurrentToDeviceJob: Job = Job()
override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList())
override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null)
private val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
resetCurrentToDevice = null
super.onAudioDevicesAdded(addedDevices)
val oldDevices = devices.value
devices.value = am.availableCommunicationDevices
@@ -53,13 +60,21 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
resetCurrentToDevice = null
super.onAudioDevicesRemoved(removedDevices)
devices.value = am.availableCommunicationDevices
}
}
private val listener: OnCommunicationDeviceChangedListener = OnCommunicationDeviceChangedListener { device ->
val resetTo = resetCurrentToDevice
if (resetTo != null && device != null && resetTo.id != device.id) {
Log.w(TAG, "Resetting device that was set by WebView ${device.name?.localized()} to previously set device ${resetTo.name?.localized()}")
selectDevice(resetTo.id)
return@OnCommunicationDeviceChangedListener
}
devices.value = am.availableCommunicationDevices
//Log.d(TAG, "Devices changed ${device?.details()} | ${devices.value.map { it.details() }}")
currentDevice.value = device
}
@@ -98,7 +113,18 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
}
}
// WebView modifies speaker when muting/unmuting microphone. It's not needed at all, returning back current device if it will be changed
override fun selectSameDeviceOnWebViewChange() {
resetCurrentToDevice = currentDevice.value
resetCurrentToDeviceJob.cancel()
resetCurrentToDeviceJob = withBGApi {
delay(5000)
resetCurrentToDevice = null
}
}
override fun selectDevice(id: Int) {
resetCurrentToDevice = null
val device = devices.value.lastOrNull { it.id == id }
if (device != null && am.communicationDevice?.id != id ) {
am.setCommunicationDevice(device)
@@ -108,12 +134,14 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var resetCurrentToDeviceJob: Job = Job()
override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList())
override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null)
private val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
resetCurrentToDeviceJob.cancel()
super.onAudioDevicesAdded(addedDevices)
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
@@ -121,6 +149,7 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
resetCurrentToDeviceJob.cancel()
super.onAudioDevicesRemoved(removedDevices)
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true)
@@ -153,7 +182,22 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
}
}
override fun selectSameDeviceOnWebViewChange() {
val current = currentDevice.value ?: return
resetCurrentToDeviceJob.cancel()
resetCurrentToDeviceJob = withBGApi {
// select old device manually because WebView will change it for sure.
// smaller delay first if it's possible to make the switch less noticeable
delay(300)
selectDevice(current.id)
// and in order to be sure that it will be selected even if WebView will be slow in changing its device
delay(700)
selectDevice(current.id)
}
}
override fun selectDevice(id: Int) {
resetCurrentToDeviceJob.cancel()
val device = devices.value.lastOrNull { it.id == id }
val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
if (isExternalDevice) {
@@ -216,6 +260,8 @@ val AudioDeviceInfo.icon: ImageResource
else -> MR.images.ic_brand_awareness_filled
}
private fun AudioDeviceInfo.details(): String = "$productName id:$id name:${name?.localized()} type:$type sink:$isSink source:$isSource"
val AudioDeviceInfo.name: StringResource?
get() = when (this.type) {
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.strings.audio_device_earpiece
@@ -202,6 +202,7 @@ actual fun ActiveCallView() {
is WCallCommand.Camera -> {
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
if (!call.localMediaSources.mic) {
callAudioDeviceManager.selectSameDeviceOnWebViewChange()
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
}
}
@@ -261,7 +262,10 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceM
devices = remember { callAudioDeviceManager.devices }.value,
currentDevice = remember { callAudioDeviceManager.currentDevice },
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
toggleAudio = {
callAudioDeviceManager.selectSameDeviceOnWebViewChange()
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic))
},
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
toggleVideo = {
if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {