Disable BT audio devices for Element Call on Android < 12 (#4876)
Display an error dialog muting the call when a bluetooth audio device is selected on Android 11 or lower, re-enable the audio once another device is used.
This commit is contained in:
parent
0b72bdc8ac
commit
c598b0699e
7 changed files with 101 additions and 7 deletions
|
|
@ -44,6 +44,7 @@ import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
|||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -52,6 +53,7 @@ import kotlinx.serialization.json.jsonObject
|
|||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class CallScreenPresenter @AssistedInject constructor(
|
||||
@Assisted private val callType: CallType,
|
||||
|
|
@ -165,6 +167,13 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
|
||||
sendHangupMessage(widgetId, interceptor)
|
||||
isJoinedCall = false
|
||||
|
||||
coroutineScope.launch {
|
||||
// Wait for a couple of seconds to receive the hangup message
|
||||
// If we don't get it in time, we close the screen anyway
|
||||
delay(2.seconds)
|
||||
close(callWidgetDriver.value, navigator)
|
||||
}
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
close(callWidgetDriver.value, navigator)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import io.element.android.features.call.impl.pip.PictureInPictureEvents
|
|||
import io.element.android.features.call.impl.pip.PictureInPictureState
|
||||
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.InvalidAudioDeviceReason
|
||||
import io.element.android.features.call.impl.utils.WebViewAudioManager
|
||||
import io.element.android.features.call.impl.utils.WebViewPipController
|
||||
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
||||
|
|
@ -105,6 +106,14 @@ internal fun CallScreenView(
|
|||
} else {
|
||||
var webViewAudioManager by remember { mutableStateOf<WebViewAudioManager?>(null) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var invalidAudioDeviceReason by remember { mutableStateOf<InvalidAudioDeviceReason?>(null) }
|
||||
invalidAudioDeviceReason?.let {
|
||||
InvalidAudioDeviceDialog(invalidAudioDeviceReason = it) {
|
||||
invalidAudioDeviceReason = null
|
||||
}
|
||||
}
|
||||
|
||||
CallWebView(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
|
|
@ -130,7 +139,11 @@ internal fun CallScreenView(
|
|||
},
|
||||
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
|
||||
)
|
||||
webViewAudioManager = WebViewAudioManager(webView, coroutineScope)
|
||||
webViewAudioManager = WebViewAudioManager(
|
||||
webView = webView,
|
||||
coroutineScope = coroutineScope,
|
||||
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
|
||||
)
|
||||
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
|
||||
val pipController = WebViewPipController(webView)
|
||||
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
|
||||
|
|
@ -157,6 +170,21 @@ internal fun CallScreenView(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InvalidAudioDeviceDialog(
|
||||
invalidAudioDeviceReason: InvalidAudioDeviceReason,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
ErrorDialog(
|
||||
content = when (invalidAudioDeviceReason) {
|
||||
InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED -> {
|
||||
stringResource(R.string.call_invalid_audio_device_bluetooth_devices_disabled)
|
||||
}
|
||||
},
|
||||
onSubmit = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallWebView(
|
||||
url: AsyncData<String>,
|
||||
|
|
@ -277,3 +305,9 @@ internal fun CallScreenPipViewPreview(
|
|||
requestPermissions = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {
|
||||
InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,22 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||
class WebViewAudioManager(
|
||||
private val webView: WebView,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val onInvalidAudioDeviceAdded: (InvalidAudioDeviceReason) -> Unit,
|
||||
) {
|
||||
// The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication.
|
||||
/**
|
||||
* Whether to disable bluetooth audio devices. This must be done on Android versions lower than Android 12,
|
||||
* since the WebView approach breaks when using the legacy Bluetooth audio APIs.
|
||||
*/
|
||||
private val disableBluetoothAudioDevices = Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
||||
|
||||
/**
|
||||
* This flag indicates whether the WebView audio is enabled or not. By default, it is enabled.
|
||||
*/
|
||||
private val isWebViewAudioEnabled = AtomicBoolean(true)
|
||||
|
||||
/**
|
||||
* The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication.
|
||||
*/
|
||||
private val wantedDeviceTypes = listOf(
|
||||
// Paired bluetooth device with microphone
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
||||
|
|
@ -60,6 +74,10 @@ class WebViewAudioManager(
|
|||
|
||||
private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
/**
|
||||
* This wake lock is used to turn off the screen when the proximity sensor is triggered during a call,
|
||||
* if the selected audio device is the built-in earpiece.
|
||||
*/
|
||||
private val proximitySensorWakeLock by lazy {
|
||||
webView.context.getSystemService<PowerManager>()
|
||||
?.takeIf { it.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK) }
|
||||
|
|
@ -296,12 +314,13 @@ class WebViewAudioManager(
|
|||
* @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<AudioDeviceInfo> = listAudioDevices()) {
|
||||
val selectedDevice = availableDevices.minByOrNull {
|
||||
wantedDeviceTypes.indexOf(it.type).let { index ->
|
||||
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
|
||||
if (index == -1) Int.MAX_VALUE else index
|
||||
val selectedDevice = availableDevices
|
||||
.minByOrNull {
|
||||
wantedDeviceTypes.indexOf(it.type).let { index ->
|
||||
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
|
||||
if (index == -1) Int.MAX_VALUE else index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expectedNewCommunicationDeviceId = selectedDevice?.id
|
||||
audioManager.selectAudioDevice(selectedDevice)
|
||||
|
|
@ -361,6 +380,13 @@ class WebViewAudioManager(
|
|||
// On Android 11 and lower, we don't have the concept of communication devices
|
||||
// We have to call the right methods based on the device type
|
||||
if (device != null) {
|
||||
if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO && disableBluetoothAudioDevices) {
|
||||
Timber.w("Bluetooth audio devices are disabled on this Android version")
|
||||
setAudioEnabled(false)
|
||||
onInvalidAudioDeviceAdded(InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED)
|
||||
return
|
||||
}
|
||||
setAudioEnabled(true)
|
||||
isSpeakerphoneOn = device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
isBluetoothScoOn = device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
||||
} else {
|
||||
|
|
@ -380,6 +406,19 @@ class WebViewAudioManager(
|
|||
proximitySensorWakeLock?.release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the audio is enabled for Element Call in the WebView.
|
||||
* It will only perform the change if the audio state has changed.
|
||||
*/
|
||||
private fun setAudioEnabled(enabled: Boolean) {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
Timber.d("Setting audio enabled in Element Call: $enabled")
|
||||
if (isWebViewAudioEnabled.getAndSet(enabled) != enabled) {
|
||||
webView.evaluateJavascript("controls.setAudioEnabled($enabled);", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -434,6 +473,10 @@ private fun isBuiltIn(type: Int): Boolean = when (type) {
|
|||
else -> false
|
||||
}
|
||||
|
||||
enum class InvalidAudioDeviceReason {
|
||||
BT_AUDIO_DEVICE_DISABLED,
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to serialize the audio device information to JSON.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
<string name="call_foreground_service_channel_title_android">"Ongoing call"</string>
|
||||
<string name="call_foreground_service_message_android">"Tap to return to the call"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Call in progress"</string>
|
||||
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call does not support using Bluetooth audio devices in this Android version. Please select a different audio device."</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Incoming Element Call"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c2a698cd7518bc96296bcd4f9b0b51b7a09094a178bca84b853efc03b58c228
|
||||
size 25365
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3302dd631da6a38d569425e4a827051c00d20ab5755c7787739bf7ff37441dfd
|
||||
size 23622
|
||||
|
|
@ -23,6 +23,7 @@ forbiddenTerms = {
|
|||
"screen_onboarding_welcome_title",
|
||||
# Contains "Element Call"
|
||||
"screen_incoming_call_subtitle_android",
|
||||
"call_invalid_audio_device_bluetooth_devices_disabled",
|
||||
# Contains "Element X"
|
||||
"screen_room_timeline_legacy_call",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue