From 468eb9bfe8d95ce570901632f05597d9c8728efd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 17 Apr 2026 11:52:07 +0200 Subject: [PATCH 1/2] audio: Let EC decide alone what communication device to use --- .../call/impl/utils/WebViewAudioManager.kt | 78 ++++--------------- 1 file changed, 13 insertions(+), 65 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt index 0c1ecf83cb..80cc9b535a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -113,23 +113,7 @@ class WebViewAudioManager( @get:RequiresApi(Build.VERSION_CODES.S) private val commsDeviceChangedListener by lazy { AudioManager.OnCommunicationDeviceChangedListener { device -> - if (device != null && device.id == expectedNewCommunicationDeviceId) { - expectedNewCommunicationDeviceId = null - Timber.d("Audio device changed, type: ${device.type}") - updateSelectedAudioDeviceInWebView(device.id.toString()) - } else if (device != null && device.id != expectedNewCommunicationDeviceId) { - // We were expecting a device change but it didn't happen, so we should retry - val expectedDeviceId = expectedNewCommunicationDeviceId - if (expectedDeviceId != null) { - // Remove the expected id so we only retry once - expectedNewCommunicationDeviceId = null - audioManager.selectAudioDevice(expectedDeviceId.toString()) - } - } else { - Timber.d("Audio device cleared") - expectedNewCommunicationDeviceId = null - audioManager.selectAudioDevice(null) - } + Timber.d("Audio device changed, type: ${device?.id}") } } @@ -144,39 +128,20 @@ class WebViewAudioManager( // We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }.sortedWith(audioDeviceComparator) setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo)) - // This should automatically switch to a new device if it has a higher priority than the current one - selectDefaultAudioDevice(audioDevices) } override fun onAudioDevicesRemoved(removedDevices: Array?) { // Update the available devices + // Element Call will then decide to switch devices if needed setAvailableAudioDevices() - - // Unless the removed device is the current one, we don't need to do anything else - val removedCurrentDevice = removedDevices.orEmpty().any { it.id == currentDeviceId } - if (!removedCurrentDevice) return - - val previousDevice = previousSelectedDevice - if (previousDevice != null) { - previousSelectedDevice = null - // If we have a previous device, we should select it again - audioManager.selectAudioDevice(previousDevice.id.toString()) - } else { - // If we don't have a previous device, we should select the default one - selectDefaultAudioDevice() - } } } - - /** - * The currently used audio device id. - */ - private var currentDeviceId: Int? = null - - /** - * When a new audio device is selected but not yet set as the communication device by the OS, this id is used to check if the device is the expected one. - */ - private var expectedNewCommunicationDeviceId: Int? = null +// +// +// /** +// * When a new audio device is selected but not yet set as the communication device by the OS, this id is used to check if the device is the expected one. +// */ +// private var expectedNewCommunicationDeviceId: Int? = null /** * Previously selected device, used to restore the selection when the selected device is removed. @@ -330,23 +295,6 @@ class WebViewAudioManager( }) } - /** - * Selects the default audio device based on the sorted available devices. - * - * @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices. - */ - private fun selectDefaultAudioDevice(availableDevices: List = listAudioDevices()) { - val selectedDevice = availableDevices.firstOrNull() - expectedNewCommunicationDeviceId = selectedDevice?.id - audioManager.selectAudioDevice(selectedDevice) - - selectedDevice?.let { - updateSelectedAudioDeviceInWebView(it.id.toString()) - } ?: run { - Timber.w("Audio: unable to select default audio device") - } - } - /** * Updates the WebView's UI to reflect the selected audio device. * @@ -381,14 +329,14 @@ class WebViewAudioManager( * * @param device The info of the audio device to select, or none to clear the selected device. */ - @Suppress("DEPRECATION") private fun AudioManager.selectAudioDevice(device: AudioDeviceInfo?) { - currentDeviceId = device?.id if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (device != null) { runCatchingExceptions { Timber.d("Setting communication device: ${device.id} - ${deviceName(device.type, device.productName.toString())}") - setCommunicationDevice(device) + if (!setCommunicationDevice(device)) { + Timber.w("Failed to setCommunication device") + } }.onFailure { Timber.e(it, "Could not set communication device.") } @@ -410,16 +358,16 @@ class WebViewAudioManager( return } setAudioEnabled(true) + @Suppress("DEPRECATION") isSpeakerphoneOn = device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER isBluetoothScoOn = device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } else { + @Suppress("DEPRECATION") isSpeakerphoneOn = false isBluetoothScoOn = false } } - expectedNewCommunicationDeviceId = null - coroutineScope.launch { proximitySensorMutex.withLock { @Suppress("WakeLock", "WakeLockTimeout") From b0b02102aea7e10efbdb4d1df6595c365be697fa Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 17 Apr 2026 18:00:25 +0200 Subject: [PATCH 2/2] enforce selecting the EC preferred device --- .../call/impl/utils/WebViewAudioManager.kt | 29 ++++++++----------- .../libraries/audio/impl/DefaultAudioFocus.kt | 3 ++ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt index 80cc9b535a..c6e8557ff9 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -64,6 +64,11 @@ class WebViewAudioManager( */ private val isWebViewAudioEnabled = AtomicBoolean(true) + /** + * Store the device id requested by EC, and re-set it if something try to switch (only android S+). + */ + private var ecRequestedDeviceId: String? = null + /** * The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication. */ @@ -114,6 +119,12 @@ class WebViewAudioManager( private val commsDeviceChangedListener by lazy { AudioManager.OnCommunicationDeviceChangedListener { device -> Timber.d("Audio device changed, type: ${device?.id}") + val wantedDevice = this.ecRequestedDeviceId + if (wantedDevice != null && this.ecRequestedDeviceId != device?.id?.toString()) { + // We want to ensure that we stick to what EC selected even if it was changed outside + Timber.d("Audio device changed to unwanted device ${device?.id}, enforce using the expected device $wantedDevice") + audioManager.selectAudioDevice(wantedDevice) + } } } @@ -136,12 +147,6 @@ class WebViewAudioManager( setAvailableAudioDevices() } } -// -// -// /** -// * When a new audio device is selected but not yet set as the communication device by the OS, this id is used to check if the device is the expected one. -// */ -// private var expectedNewCommunicationDeviceId: Int? = null /** * Previously selected device, used to restore the selection when the selected device is removed. @@ -228,6 +233,7 @@ class WebViewAudioManager( val webViewAudioDeviceSelectedCallback = AndroidWebViewAudioBridge( onAudioDeviceSelected = { selectedDeviceId -> previousSelectedDevice = listAudioDevices().find { it.id.toString() == selectedDeviceId } + this.ecRequestedDeviceId = selectedDeviceId audioManager.selectAudioDevice(selectedDeviceId) }, onAudioPlaybackStarted = { @@ -295,17 +301,6 @@ class WebViewAudioManager( }) } - /** - * Updates the WebView's UI to reflect the selected audio device. - * - * @param deviceId The id of the selected audio device. - */ - private fun updateSelectedAudioDeviceInWebView(deviceId: String) { - coroutineScope.launch(Dispatchers.Main) { - webView.evaluateJavascript("controls.setOutputDevice('$deviceId');", null) - } - } - /** * Selects the audio device on the OS based on the provided device id. * diff --git a/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt index faab73593e..bc6a42d2f9 100644 --- a/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt +++ b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt @@ -19,6 +19,7 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.di.annotations.ApplicationContext +import timber.log.Timber @ContributesBinding(AppScope::class) class DefaultAudioFocus( @@ -38,9 +39,11 @@ class DefaultAudioFocus( when (it) { AudioManager.AUDIOFOCUS_GAIN -> { // Do nothing + Timber.d("AudioFocus: AUDIOFOCUS_GAIN") } AudioManager.AUDIOFOCUS_LOSS -> { // Permanent focus loss (e.g., phone call) — always stop/pause. + Timber.d("AudioFocus: AUDIOFOCUS_LOSS") onFocusLost() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,