From a6d7741a609289b8579cfe251942eb0535727f9c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 11 Apr 2025 16:46:51 +0200 Subject: [PATCH] Use in-call volume and mode for EC, allow external audio devices (#4481) * Use in-call volume and mode for EC: * Try routing audio to external device * Place speaker before earpiece in list of audio devices --- features/call/impl/build.gradle.kts | 1 + .../features/call/impl/ui/CallScreenView.kt | 15 ++++++ .../androidutils/compat/AudioManager.kt | 52 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index f0a9eedca0..343da45b47 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation(projects.appconfig) implementation(projects.features.enterprise.api) implementation(projects.libraries.architecture) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index 7ace349f6f..09bd90a9e3 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -8,6 +8,7 @@ package io.element.android.features.call.impl.ui import android.annotation.SuppressLint +import android.media.AudioManager import android.util.Log import android.view.ViewGroup import android.webkit.ConsoleMessage @@ -27,6 +28,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.getSystemService import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.call.impl.R import io.element.android.features.call.impl.pip.PictureInPictureEvents @@ -35,6 +37,8 @@ import io.element.android.features.call.impl.pip.PictureInPictureStateProvider import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.utils.WebViewPipController import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor +import io.element.android.libraries.androidutils.compat.disableExternalAudioDevice +import io.element.android.libraries.androidutils.compat.enableExternalAudioDevice import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton @@ -150,6 +154,12 @@ private fun CallWebView( AndroidView( modifier = modifier, factory = { context -> + // Set 'voice call' mode so volume keys actually control the call volume + val audioManager = context.getSystemService() + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + + audioManager?.enableExternalAudioDevice() + WebView(context).apply { onWebViewCreate(this) setup(userAgent, onPermissionsRequest) @@ -161,6 +171,11 @@ private fun CallWebView( } }, onRelease = { webView -> + // Reset audio mode + val audioManager = webView.context.getSystemService() + audioManager?.disableExternalAudioDevice() + audioManager?.mode = AudioManager.MODE_NORMAL + webView.destroy() } ) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt new file mode 100644 index 0000000000..ce092f15f9 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.compat + +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.os.Build + +fun AudioManager.enableExternalAudioDevice() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication. + val wantedDeviceTypes = listOf( + // Paired bluetooth device with microphone + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + // USB devices which can play or record audio + AudioDeviceInfo.TYPE_USB_HEADSET, + AudioDeviceInfo.TYPE_USB_DEVICE, + AudioDeviceInfo.TYPE_USB_ACCESSORY, + // Wired audio devices + AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + // The built-in speaker of the device + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + // The built-in earpiece of the device + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, + ) + val devices = availableCommunicationDevices + val selectedDevice = devices.find { + wantedDeviceTypes.contains(it.type) + } + selectedDevice?.let { setCommunicationDevice(it) } + } else { + // If we don't have access to the new APIs, use the deprecated ones + @Suppress("DEPRECATION") + isSpeakerphoneOn = true + } +} + +fun AudioManager.disableExternalAudioDevice() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + clearCommunicationDevice() + } else { + // If we don't have access to the new APIs, use the deprecated ones + @Suppress("DEPRECATION") + isSpeakerphoneOn = false + } +}