mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
Add call audio toggle to calling v2.
This commit is contained in:
committed by
Nicholas Tinsley
parent
69d62d385e
commit
282ec6918b
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user