Disable call audio toggle while the switch is processing.

This commit is contained in:
Alex Hart
2026-01-22 16:11:59 -04:00
committed by GitHub
parent c0d9efc930
commit 96273bb724
12 changed files with 53 additions and 8 deletions

View File

@@ -55,7 +55,8 @@ fun CallAudioToggleButton(
contentDescription: String,
onSheetDisplayChanged: (Boolean) -> Unit,
pickerController: AudioOutputPickerController,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
val buttonSize = dimensionResource(id = R.dimen.webrtc_button_size)
@@ -79,9 +80,12 @@ fun CallAudioToggleButton(
onClick = {
pickerController.show()
},
enabled = enabled,
colors = IconButtons.iconButtonColors(
containerColor = containerColor,
contentColor = contentColor
contentColor = contentColor,
disabledContainerColor = containerColor.copy(alpha = 0.38f),
disabledContentColor = contentColor.copy(alpha = 0.38f)
),
modifier = modifier.size(buttonSize)
) {

View File

@@ -76,7 +76,8 @@ fun CallControls(
CallAudioToggleButton(
contentDescription = stringResource(id = R.string.WebRtcAudioOutputToggle__audio_output),
onSheetDisplayChanged = callScreenSheetDisplayListener::onAudioDeviceSheetDisplayChanged,
pickerController = audioOutputPickerController
pickerController = audioOutputPickerController,
enabled = !callControlsState.isAudioOutputChangePending
)
}
@@ -202,6 +203,7 @@ data class CallControlsState(
val skipHiddenState: Boolean = true,
val displayAudioOutputToggle: Boolean = false,
val audioOutput: WebRtcAudioOutput = WebRtcAudioOutput.HANDSET,
val isAudioOutputChangePending: Boolean = false,
val displayVideoToggle: Boolean = false,
val isVideoEnabled: Boolean = false,
val displayMicToggle: Boolean = false,
@@ -222,7 +224,8 @@ data class CallControlsState(
fun fromViewModelData(
callParticipantsState: CallParticipantsState,
webRtcControls: WebRtcControls,
groupMemberCount: Int
groupMemberCount: Int,
isAudioDeviceChangePending: Boolean = false
): CallControlsState {
return CallControlsState(
isEarpieceAvailable = webRtcControls.isEarpieceAvailableForAudioToggle,
@@ -231,6 +234,7 @@ data class CallControlsState(
skipHiddenState = !(webRtcControls.isFadeOutEnabled || webRtcControls == WebRtcControls.PIP || webRtcControls.displayErrorControls()),
displayAudioOutputToggle = webRtcControls.displayAudioToggle(),
audioOutput = webRtcControls.audioOutput,
isAudioOutputChangePending = isAudioDeviceChangePending,
displayVideoToggle = webRtcControls.displayVideoToggle(),
isVideoEnabled = callParticipantsState.localParticipant.isVideoEnabled,
displayMicToggle = webRtcControls.displayMuteAudio(),

View File

@@ -56,6 +56,7 @@ class WebRtcCallViewModel : ViewModel() {
private val callPeerRepository = CallPeerRepository(viewModelScope)
private val internalMicrophoneEnabled = MutableStateFlow(true)
private val isAudioDeviceChangePending = MutableStateFlow(false)
private val remoteMutedBy = MutableStateFlow<CallParticipant?>(null)
private val isInPipMode = MutableStateFlow(false)
private val _savedLocalParticipantLandscape = MutableStateFlow(false)
@@ -177,8 +178,10 @@ class WebRtcCallViewModel : ViewModel() {
callParticipantsState,
getWebRtcControls(),
groupSize,
CallControlsState::fromViewModelData
)
isAudioDeviceChangePending
) { participantsState, controls, groupMemberCount, audioChangePending ->
CallControlsState.fromViewModelData(participantsState, controls, groupMemberCount, audioChangePending)
}
}
val callParticipantsState: Flow<CallParticipantsState> get() = participantsState
@@ -311,6 +314,7 @@ class WebRtcCallViewModel : ViewModel() {
}
internalMicrophoneEnabled.value = localParticipant.isMicrophoneEnabled
isAudioDeviceChangePending.value = webRtcViewModel.isAudioDeviceChangePending
if (internalMicrophoneEnabled.value) {
remoteMutedBy.update { null }

View File

@@ -121,6 +121,7 @@ class WebRtcViewModel(state: WebRtcServiceState) {
val activeDevice: SignalAudioManager.AudioDevice = state.localDeviceState.activeDevice
val availableDevices: Set<SignalAudioManager.AudioDevice> = state.localDeviceState.availableDevices
val bluetoothPermissionDenied: Boolean = state.localDeviceState.bluetoothPermissionDenied
val isAudioDeviceChangePending: Boolean = state.localDeviceState.isAudioDeviceChangePending
val localParticipant: CallParticipant = createLocal(
state.localDeviceState.cameraState,

View File

@@ -265,6 +265,10 @@ class ActiveCallManager(
callManager.onAudioDeviceChanged(activeDevice, devices)
}
override fun onAudioDeviceChangeFailed() {
callManager.onAudioDeviceChangeFailed()
}
override fun onBluetoothPermissionDenied() {
callManager.onBluetoothPermissionDenied()
}

View File

@@ -40,6 +40,7 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
.changeLocalDeviceState()
.setActiveDevice(activeDevice)
.setAvailableDevices(availableDevices)
.setAudioDeviceChangePending(false)
.build();
}
@@ -50,7 +51,10 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
webRtcInteractor.setUserAudioDevice(activePeer != null ? activePeer.getId() : null, userDevice);
return currentState;
return currentState.builder()
.changeLocalDeviceState()
.setAudioDeviceChangePending(true)
.build();
}
@Override

View File

@@ -360,6 +360,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleAudioDeviceChanged(s, activeDevice, availableDevices));
}
public void onAudioDeviceChangeFailed() {
process((s, p) -> p.handleAudioDeviceChangeFailed(s));
}
public void onBluetoothPermissionDenied() {
process((s, p) -> p.handleBluetoothPermissionDenied(s));
}

View File

@@ -497,6 +497,14 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
public @NonNull WebRtcServiceState handleAudioDeviceChangeFailed(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleAudioDeviceChangeFailed(): clearing pending state");
return currentState.builder()
.changeLocalDeviceState()
.setAudioDeviceChangePending(false)
.build();
}
public @NonNull WebRtcServiceState handleBluetoothPermissionDenied(@NonNull WebRtcServiceState currentState) {
return currentState.builder()
.changeLocalDeviceState()

View File

@@ -18,6 +18,7 @@ data class LocalDeviceState(
var activeDevice: SignalAudioManager.AudioDevice = SignalAudioManager.AudioDevice.NONE,
var availableDevices: Set<SignalAudioManager.AudioDevice> = emptySet(),
var bluetoothPermissionDenied: Boolean = false,
var isAudioDeviceChangePending: Boolean = false,
var networkConnectionType: PeerConnection.AdapterType = PeerConnection.AdapterType.UNKNOWN,
var handRaisedTimestamp: Long = CallParticipant.HAND_LOWERED,
var remoteMutedBy: CallParticipant? = null

View File

@@ -140,6 +140,11 @@ public class WebRtcServiceStateBuilder {
return this;
}
public @NonNull LocalDeviceStateBuilder setAudioDeviceChangePending(boolean isAudioDeviceChangePending) {
toBuild.setAudioDeviceChangePending(isAudioDeviceChangePending);
return this;
}
public @NonNull LocalDeviceStateBuilder setNetworkConnectionType(@NonNull PeerConnection.AdapterType type) {
toBuild.setNetworkConnectionType(type);
return this;

View File

@@ -241,6 +241,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
eventListener?.onAudioDeviceChanged(AudioDeviceMapping.fromPlatformType(candidate.type), availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet())
} else {
Log.w(TAG, "Failed to set ${candidate.id} of type ${getDeviceTypeName(candidate.type)} as communication device.")
eventListener?.onAudioDeviceChangeFailed()
}
} else {
val searchOrder: List<AudioDevice> = listOf(AudioDevice.BLUETOOTH, AudioDevice.WIRED_HEADSET, defaultAudioDevice, AudioDevice.EARPIECE, AudioDevice.SPEAKER_PHONE, AudioDevice.NONE).distinct()
@@ -264,6 +265,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
eventListener?.onAudioDeviceChanged(AudioDeviceMapping.fromPlatformType(candidate.type), availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet())
} else {
Log.w(TAG, "Failed to set ${candidate.id} as communication device.")
eventListener?.onAudioDeviceChangeFailed()
}
}
}

View File

@@ -141,6 +141,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
interface EventListener {
@JvmSuppressWildcards
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
fun onAudioDeviceChangeFailed()
fun onBluetoothPermissionDenied()
}
}
@@ -354,8 +355,11 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
setAudioDevice(newAudioDevice)
Log.i(TAG, "New device status: available: $audioDevices, selected: $newAudioDevice")
eventListener?.onAudioDeviceChanged(selectedAudioDevice, audioDevices)
}
// Always notify listener to clear any pending audio device change state,
// even if the device didn't actually change
eventListener?.onAudioDeviceChanged(selectedAudioDevice, audioDevices)
}
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {