Skip to content

Use in-call volume and mode for EC #4481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions features/call/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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>()
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION

audioManager?.enableExternalAudioDevice()

WebView(context).apply {
onWebViewCreate(this)
setup(userAgent, onPermissionsRequest)
Expand All @@ -161,6 +171,11 @@ private fun CallWebView(
}
},
onRelease = { webView ->
// Reset audio mode
val audioManager = webView.context.getSystemService<AudioManager>()
audioManager?.disableExternalAudioDevice()
audioManager?.mode = AudioManager.MODE_NORMAL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe store the previous mode and restore it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I saw in some issues (this is poorly documented, AFAICT) the app can be 'blacklisted' if it doesn't play any audio in the 6s after enabling this mode or doesn't return to the MODE_NORMAL when it's done playing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks.


webView.destroy()
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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(

Check warning on line 17 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L17

Added line #L17 was not covered by tests
// Paired bluetooth device with microphone
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,

Check warning on line 19 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L19

Added line #L19 was not covered by tests
// USB devices which can play or record audio
AudioDeviceInfo.TYPE_USB_HEADSET,
AudioDeviceInfo.TYPE_USB_DEVICE,
AudioDeviceInfo.TYPE_USB_ACCESSORY,

Check warning on line 23 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L21-L23

Added lines #L21 - L23 were not covered by tests
// Wired audio devices
AudioDeviceInfo.TYPE_WIRED_HEADSET,
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,

Check warning on line 26 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L25-L26

Added lines #L25 - L26 were not covered by tests
// The built-in speaker of the device
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,

Check warning on line 28 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L28

Added line #L28 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Video Call, I guess TYPE_BUILTIN_SPEAKER should have a higher priority than TYPE_BUILTIN_EARPIECE, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nice catch.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we have the "EC stop audio output"/"EC start/continue audio output" widget actions it should be easy to switch from TYPE_BUILTIN_SPEAKER to TYPE_BUILTIN_EARPIECE when the phone is put in standby right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't say without having tested it first. In theory it should be.

// The built-in earpiece of the device
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,

Check warning on line 30 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L30

Added line #L30 was not covered by tests
)
val devices = availableCommunicationDevices

Check warning on line 32 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L32

Added line #L32 was not covered by tests
val selectedDevice = devices.find {
wantedDeviceTypes.contains(it.type)

Check warning on line 34 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L34

Added line #L34 was not covered by tests
}
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()

Check warning on line 46 in libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt

View check run for this annotation

Codecov / codecov/patch

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/AudioManager.kt#L46

Added line #L46 was not covered by tests
} else {
// If we don't have access to the new APIs, use the deprecated ones
@Suppress("DEPRECATION")
isSpeakerphoneOn = false
}
}
Loading