Add call audio toggle to calling v2.

This commit is contained in:
Alex Hart
2024-08-23 15:52:16 -03:00
committed by Nicholas Tinsley
parent 69d62d385e
commit 282ec6918b
10 changed files with 488 additions and 12 deletions

View File

@@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.components.webrtc
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlin.math.min
/**
@@ -13,15 +16,30 @@ class ToggleButtonOutputState {
throw IndexOutOfBoundsException("Index: $value, size: ${availableOutputs.size}")
}
field = value
currentDevice = getCurrentOutput()
}
/**
* Observable state of currently selected device.
*/
var currentDevice by mutableStateOf(getCurrentOutput())
private set
/**
* Observable state of available devices.
*/
var availableDevices by mutableStateOf(getOutputs())
private set
var isEarpieceAvailable: Boolean
get() = availableOutputs.contains(WebRtcAudioOutput.HANDSET)
set(value) {
if (value) {
availableOutputs.add(WebRtcAudioOutput.HANDSET)
availableDevices = getOutputs()
} else {
availableOutputs.remove(WebRtcAudioOutput.HANDSET)
availableDevices = getOutputs()
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
}
}
@@ -31,8 +49,10 @@ class ToggleButtonOutputState {
set(value) {
if (value) {
availableOutputs.add(WebRtcAudioOutput.BLUETOOTH_HEADSET)
availableDevices = getOutputs()
} else {
availableOutputs.remove(WebRtcAudioOutput.BLUETOOTH_HEADSET)
availableDevices = getOutputs()
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
}
}
@@ -41,8 +61,10 @@ class ToggleButtonOutputState {
set(value) {
if (value) {
availableOutputs.add(WebRtcAudioOutput.WIRED_HEADSET)
availableDevices = getOutputs()
} else {
availableOutputs.remove(WebRtcAudioOutput.WIRED_HEADSET)
availableDevices = getOutputs()
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
}
}

View File

@@ -5,7 +5,13 @@ import android.content.DialogInterface
import android.media.AudioDeviceInfo
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.fragment.app.FragmentActivity
import kotlinx.collections.immutable.toImmutableList
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -33,7 +39,7 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(fragmentActivity).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinctBy { it.deviceType.name }.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val currentDeviceId = am.communicationDevice?.id ?: -1
if (devices.size < threshold) {
Log.d(TAG, "Only found $devices devices,\nnot showing picker.")
Log.d(TAG, "Only found $devices devices, not showing picker.")
if (devices.isEmpty()) return null
val index = devices.indexOfFirst { it.deviceId == currentDeviceId }
@@ -42,11 +48,45 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
onAudioDeviceSelected(devices[(index + 1) % devices.size])
return null
} else {
Log.d(TAG, "Found $devices devices,\nshowing picker.")
Log.d(TAG, "Found $devices devices, showing picker.")
return WebRtcAudioOutputBottomSheet.show(fragmentActivity.supportFragmentManager, devices, currentDeviceId, onAudioDeviceSelected, onDismiss)
}
}
@Composable
fun Picker(threshold: Int) {
val context = LocalContext.current
val am = AppDependencies.androidCallAudioManager
if (am.availableCommunicationDevices.isEmpty()) {
Toast.makeText(context, R.string.WebRtcAudioOutputToggleButton_no_eligible_audio_i_o_detected, Toast.LENGTH_LONG).show()
return
}
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(context).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinctBy { it.deviceType.name }.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val currentDeviceId = am.communicationDevice?.id ?: -1
if (devices.size < threshold) {
Log.d(TAG, "Only found $devices devices, not showing picker.")
if (devices.isEmpty()) return
val index = devices.indexOfFirst { it.deviceId == currentDeviceId }
if (index == -1) return
onAudioDeviceSelected(devices[(index + 1) % devices.size])
return
} else {
Log.d(TAG, "Found $devices devices, showing picker.")
DeviceList(
audioOutputOptions = devices.toImmutableList(),
initialDeviceId = currentDeviceId,
onDeviceSelected = onAudioDeviceSelected,
modifier = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.core_ui__gutter)
)
)
}
}
@RequiresApi(31)
val onAudioDeviceSelected: (AudioOutputOption) -> Unit = {
Log.d(TAG, "User selected audio device of type ${it.deviceType}")

View File

@@ -35,6 +35,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
@@ -263,6 +264,14 @@ class CallActivity : BaseActivity(), CallControlsCallback {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) {
viewModel.onAudioDeviceSheetDisplayChanged(displayed)
}
override fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice) {
viewModel.onSelectedAudioDeviceChanged(audioDevice)
}
override fun onVideoToggleClick(enabled: Boolean) {
if (webRtcCallViewModel.recipient.get() != Recipient.UNKNOWN) {
callPermissionsDialogController.requestCameraPermission(

View File

@@ -0,0 +1,323 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.v2
import android.os.Build
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.DarkPreview
import org.signal.core.ui.IconButtons
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.AudioStateUpdater
import org.thoughtcrime.securesms.components.webrtc.ToggleButtonOutputState
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioPicker31
private const val SHOW_PICKER_THRESHOLD = 3
/**
* Button which allows user to select from different audio devices to play back call audio through.
*/
@Composable
fun CallAudioToggleButton(
outputState: ToggleButtonOutputState,
contentDescription: String,
onSelectedDeviceChanged: (WebRtcAudioDevice) -> Unit,
onSheetDisplayChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val buttonSize = dimensionResource(id = R.dimen.webrtc_button_size)
val currentOutput = outputState.currentDevice
val allOutputs = outputState.availableDevices
val containerColor = if (currentOutput == WebRtcAudioOutput.HANDSET || allOutputs.size >= SHOW_PICKER_THRESHOLD) {
MaterialTheme.colorScheme.secondaryContainer
} else {
colorResource(id = R.color.signal_light_colorSecondaryContainer)
}
val contentColor = if (currentOutput == WebRtcAudioOutput.HANDSET || allOutputs.size >= SHOW_PICKER_THRESHOLD) {
colorResource(id = R.color.signal_light_colorOnPrimary)
} else {
colorResource(id = R.color.signal_light_colorOnSecondaryContainer)
}
val pickerController = rememberPickerController(
onSelectedDeviceChanged = onSelectedDeviceChanged,
outputState = outputState
)
IconButtons.IconButton(
size = buttonSize,
onClick = {
pickerController.show()
},
colors = IconButtons.iconButtonColors(
containerColor = containerColor,
contentColor = contentColor
),
modifier = modifier.size(buttonSize)
) {
val iconRes = remember(currentOutput, pickerController.willDisplayPicker) {
if (!pickerController.willDisplayPicker && currentOutput == WebRtcAudioOutput.HANDSET) {
WebRtcAudioOutput.SPEAKER.iconRes
} else {
currentOutput.iconRes
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = contentDescription,
modifier = Modifier.size(28.dp)
)
if (pickerController.willDisplayPicker) {
Icon(
painter = painterResource(id = R.drawable.symbol_dropdown_triangle_compat_bold_16),
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
pickerController.Sheet()
LaunchedEffect(pickerController.displaySheet) {
onSheetDisplayChanged(pickerController.displaySheet)
}
}
@Composable
private fun rememberPickerController(
onSelectedDeviceChanged: (WebRtcAudioDevice) -> Unit,
outputState: ToggleButtonOutputState
): PickerController {
return remember(onSelectedDeviceChanged, outputState) {
PickerController(
onSelectedDeviceChanged,
outputState
)
}
}
/**
* Controller for the Audio picker which contains different state variables for choosing whether
* or not to display the sheet.
*/
private class PickerController(
private val onSelectedDeviceChanged: (WebRtcAudioDevice) -> Unit,
private val outputState: ToggleButtonOutputState
) {
var displaySheet: Boolean by mutableStateOf(false)
private set
val willDisplayPicker: Boolean by derivedStateOf {
outputState.availableDevices.size >= SHOW_PICKER_THRESHOLD || !outputState.availableDevices.contains(WebRtcAudioOutput.HANDSET)
}
private val newApiController = if (Build.VERSION.SDK_INT >= 31) WebRtcAudioPicker31(
audioOutputChangedListener = onSelectedDeviceChanged,
outputState = outputState,
stateUpdater = object : AudioStateUpdater {
override fun updateAudioOutputState(audioOutput: WebRtcAudioOutput) {
outputState.setCurrentOutput(audioOutput)
displaySheet = false
}
override fun hidePicker() {
displaySheet = false
}
}
) else null
fun show() {
displaySheet = true
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Sheet() {
if (!displaySheet) {
return
}
val isLegacy = Build.VERSION.SDK_INT < 31
if (!willDisplayPicker) {
if (isLegacy) {
onSelectedDeviceChanged(WebRtcAudioDevice(outputState.peekNext(), null))
} else {
newApiController!!.Picker(threshold = SHOW_PICKER_THRESHOLD)
}
return
}
ModalBottomSheet(
onDismissRequest = { displaySheet = false }
) {
if (isLegacy) {
LegacyAudioPickerContent(
toggleButtonOutputState = outputState,
onSelectedDeviceChanged = {
displaySheet = false
onSelectedDeviceChanged(WebRtcAudioDevice(it, null))
}
)
} else {
newApiController!!.Picker(threshold = SHOW_PICKER_THRESHOLD)
}
}
}
}
/**
* Picker for pre-api-31 devices.
*/
@Composable
private fun LegacyAudioPickerContent(
toggleButtonOutputState: ToggleButtonOutputState,
onSelectedDeviceChanged: (WebRtcAudioOutput) -> Unit
) {
Text(
text = stringResource(R.string.WebRtcAudioOutputToggle__audio_output),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.padding(8.dp)
.padding(
horizontal = dimensionResource(id = R.dimen.core_ui__gutter)
)
)
LazyColumn(
modifier = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.core_ui__gutter)
)
) {
items(
items = toggleButtonOutputState.availableDevices
) { item ->
val icon = when (item) {
WebRtcAudioOutput.HANDSET -> R.drawable.symbol_phone_speaker_outline_24
WebRtcAudioOutput.SPEAKER -> R.drawable.symbol_speaker_outline_24
WebRtcAudioOutput.BLUETOOTH_HEADSET -> R.drawable.symbol_speaker_bluetooth_outline_24
WebRtcAudioOutput.WIRED_HEADSET -> R.drawable.symbol_headphones_outline_24
}
Rows.RadioRow(
selected = item == toggleButtonOutputState.currentDevice,
content = {
Icon(
painter = painterResource(id = icon),
contentDescription = null,
modifier = Modifier.padding(end = 16.dp)
)
Text(
text = stringResource(id = item.labelRes),
style = MaterialTheme.typography.bodyLarge
)
},
modifier = Modifier.clickable {
onSelectedDeviceChanged(item)
}
)
}
}
}
@DarkPreview
@Composable
private fun CallAudioPickerSheetContentPreview() {
Previews.BottomSheetPreview {
Column {
LegacyAudioPickerContent(
toggleButtonOutputState = ToggleButtonOutputState().apply {
isEarpieceAvailable = true
isBluetoothHeadsetAvailable = true
isWiredHeadsetAvailable = true
},
onSelectedDeviceChanged = {}
)
}
}
}
@DarkPreview
@Composable
private fun TwoDeviceCallAudioToggleButtonPreview() {
val outputState = remember {
ToggleButtonOutputState().apply {
isEarpieceAvailable = true
}
}
Previews.Preview {
CallAudioToggleButton(
outputState = outputState,
contentDescription = "",
onSelectedDeviceChanged = {
outputState.setCurrentOutput(it.webRtcAudioOutput)
},
onSheetDisplayChanged = {}
)
}
}
@DarkPreview
@Composable
private fun ThreeDeviceCallAudioToggleButtonPreview() {
val outputState = remember {
ToggleButtonOutputState().apply {
isEarpieceAvailable = true
isBluetoothHeadsetAvailable = true
}
}
Previews.Preview {
CallAudioToggleButton(
outputState = outputState,
contentDescription = "",
onSelectedDeviceChanged = {
outputState.setCurrentOutput(it.webRtcAudioOutput)
},
onSheetDisplayChanged = {}
)
}
}

View File

@@ -43,12 +43,14 @@ private fun ToggleCallButton(
onCheckedChange = onCheckedChange,
size = buttonSize,
modifier = modifier.size(buttonSize),
colors = IconButtons.iconToggleButtonColors(
checkedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
checkedContentColor = colorResource(id = R.color.signal_light_colorOnPrimary),
containerColor = colorResource(id = R.color.signal_light_colorSecondaryContainer),
contentColor = colorResource(id = R.color.signal_light_colorOnSecondaryContainer)
)
colors = IconButtons.run {
iconToggleButtonColors(
checkedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
checkedContentColor = colorResource(id = R.color.signal_light_colorOnPrimary),
containerColor = colorResource(id = R.color.signal_light_colorSecondaryContainer),
contentColor = colorResource(id = R.color.signal_light_colorOnSecondaryContainer)
)
}
) {
Icon(
painter = if (checked) checkedPainter else painter,

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -31,6 +32,8 @@ import org.signal.core.ui.DarkPreview
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
import org.thoughtcrime.securesms.components.webrtc.ToggleButtonOutputState
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
import org.thoughtcrime.securesms.events.WebRtcViewModel
@@ -66,7 +69,30 @@ fun CallControls(
Row(
horizontalArrangement = spacedBy(20.dp)
) {
// TODO [alex] -- Audio output toggle
if (callControlsState.displayAudioOutputToggle) {
val outputState = remember {
ToggleButtonOutputState().apply {
isEarpieceAvailable = callControlsState.isEarpieceAvailable
isWiredHeadsetAvailable = callControlsState.isWiredHeadsetAvailable
isBluetoothHeadsetAvailable = callControlsState.isBluetoothHeadsetAvailable
}
}
LaunchedEffect(callControlsState.isEarpieceAvailable, callControlsState.isWiredHeadsetAvailable, callControlsState.isBluetoothHeadsetAvailable) {
outputState.apply {
isEarpieceAvailable = callControlsState.isEarpieceAvailable
isWiredHeadsetAvailable = callControlsState.isWiredHeadsetAvailable
isBluetoothHeadsetAvailable = callControlsState.isBluetoothHeadsetAvailable
}
}
CallAudioToggleButton(
outputState = outputState,
contentDescription = stringResource(id = R.string.WebRtcAudioOutputToggle__audio_output),
onSelectedDeviceChanged = callControlsCallback::onSelectedAudioDeviceChanged,
onSheetDisplayChanged = callControlsCallback::onAudioDeviceSheetDisplayChanged
)
}
val hasCameraPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
if (callControlsState.displayVideoToggle) {
@@ -145,6 +171,8 @@ fun CallControlsPreview() {
* Callbacks for call controls actions.
*/
interface CallControlsCallback {
fun onAudioDeviceSheetDisplayChanged(displayed: Boolean)
fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice)
fun onVideoToggleClick(enabled: Boolean)
fun onMicToggleClick(enabled: Boolean)
fun onGroupRingingToggleClick(enabled: Boolean, allowed: Boolean)
@@ -153,6 +181,8 @@ interface CallControlsCallback {
fun onEndCallClick()
object Empty : CallControlsCallback {
override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) = Unit
override fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice) = Unit
override fun onVideoToggleClick(enabled: Boolean) = Unit
override fun onMicToggleClick(enabled: Boolean) = Unit
override fun onGroupRingingToggleClick(enabled: Boolean, allowed: Boolean) = Unit
@@ -168,6 +198,9 @@ interface CallControlsCallback {
* sources so we don't need to listen to multiple here.
*/
data class CallControlsState(
val isEarpieceAvailable: Boolean = false,
val isBluetoothHeadsetAvailable: Boolean = false,
val isWiredHeadsetAvailable: Boolean = false,
val skipHiddenState: Boolean = true,
val displayAudioOutputToggle: Boolean = false,
val audioOutput: WebRtcAudioOutput = WebRtcAudioOutput.HANDSET,
@@ -200,6 +233,9 @@ data class CallControlsState(
}
return CallControlsState(
isEarpieceAvailable = webRtcControls.isEarpieceAvailableForAudioToggle,
isBluetoothHeadsetAvailable = webRtcControls.isBluetoothHeadsetAvailableForAudioToggle,
isWiredHeadsetAvailable = webRtcControls.isWiredHeadsetAvailableForAudioToggle,
skipHiddenState = !(webRtcControls.isFadeOutEnabled || webRtcControls == WebRtcControls.PIP || webRtcControls.displayErrorControls()),
displayAudioOutputToggle = webRtcControls.displayAudioToggle(),
audioOutput = webRtcControls.audioOutput,

View File

@@ -177,6 +177,7 @@ fun CallScreen(
overflowParticipants = overflowParticipants,
scaffoldState = scaffoldState,
callControlsState = callControlsState,
callScreenState = callScreenState,
onPipClick = onLocalPictureInPictureClicked,
onControlsToggled = onControlsToggled
)
@@ -196,6 +197,7 @@ fun CallScreen(
overflowParticipants = overflowParticipants,
scaffoldState = scaffoldState,
callControlsState = callControlsState,
callScreenState = callScreenState,
onPipClick = onLocalPictureInPictureClicked,
onControlsToggled = onControlsToggled
)
@@ -264,6 +266,7 @@ private fun BoxScope.Viewport(
overflowParticipants: List<CallParticipant>,
scaffoldState: BottomSheetScaffoldState,
callControlsState: CallControlsState,
callScreenState: CallScreenState,
onPipClick: () -> Unit,
onControlsToggled: (Boolean) -> Unit
) {
@@ -279,7 +282,7 @@ private fun BoxScope.Viewport(
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
val scope = rememberCoroutineScope()
val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState)
val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState && !callScreenState.isDisplayingAudioToggleSheet)
LaunchedEffect(hideSheet) {
if (hideSheet) {
delay(5.seconds)

View File

@@ -14,15 +14,18 @@ import kotlin.time.Duration.Companion.seconds
* This contains higher level information that would have traditionally been directly
* set on views. (Statuses, popups, etc.), allowing us to manage this from CallViewModel
*
* @param status Status text resource to display as call status.
* @param callRecipientId The recipient ID of the call target (1:1 recipient, call link, or group)
* @param hangup Set on call termination.
* @param callControlsChange Update to display in a CallStateUpdate component.
* @param callStatus Status text resource to display as call status.
* @param isDisplayingAudioToggleSheet Whether the audio toggle sheet is currently displayed. Displaying this sheet should suppress hiding the controls.
*/
data class CallScreenState(
val callRecipientId: RecipientId = RecipientId.UNKNOWN,
val hangup: Hangup? = null,
val callControlsChange: CallControlsChange? = null,
val callStatus: CallString? = null
val callStatus: CallString? = null,
val isDisplayingAudioToggleSheet: Boolean = false
) {
data class Hangup(
val hangupMessageType: HangupMessage.Type,

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.components.webrtc.v2
import android.os.Build
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -18,6 +19,8 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -26,6 +29,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
import kotlin.time.Duration.Companion.milliseconds
@@ -311,4 +315,26 @@ class CallViewModel(
else -> null
}
}
fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) {
internalCallScreenState.update {
it.copy(isDisplayingAudioToggleSheet = displayed)
}
}
fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice) {
// TODO [alex] maybeDisplaySpeakerphonePopup(audioOutput);
if (Build.VERSION.SDK_INT >= 31) {
AppDependencies.signalCallManager.selectAudioDevice(SignalAudioManager.ChosenAudioDeviceIdentifier(audioDevice.deviceId!!))
} else {
val managerDevice = when (audioDevice.webRtcAudioOutput) {
WebRtcAudioOutput.HANDSET -> SignalAudioManager.AudioDevice.EARPIECE
WebRtcAudioOutput.SPEAKER -> SignalAudioManager.AudioDevice.SPEAKER_PHONE
WebRtcAudioOutput.BLUETOOTH_HEADSET -> SignalAudioManager.AudioDevice.BLUETOOTH
WebRtcAudioOutput.WIRED_HEADSET -> SignalAudioManager.AudioDevice.WIRED_HEADSET
}
AppDependencies.signalCallManager.selectAudioDevice(SignalAudioManager.ChosenAudioDeviceIdentifier(managerDevice))
}
}
}