mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 20:23:19 +00:00
Implement remote mute receive; Update to RingRTC v2.52.0
Co-authored-by: Alex Hart <alex@signal.org> Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
ed9a945f05
commit
3d7162cdd3
@@ -5,7 +5,9 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -30,6 +32,15 @@ public class CallToastPopupWindow extends PopupWindow {
|
||||
toast.show();
|
||||
}
|
||||
|
||||
public static void show(@NonNull ViewGroup viewGroup, @DrawableRes int iconId, @NonNull String description) {
|
||||
CallToastPopupWindow toast = new CallToastPopupWindow(viewGroup);
|
||||
|
||||
TextView text = toast.getContentView().findViewById(R.id.description);
|
||||
text.setText(description);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(iconId, 0, 0, 0);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
private CallToastPopupWindow(@NonNull ViewGroup parent) {
|
||||
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.call_toast_popup_window, parent, false),
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Replacement sealed class for WebRtcCallViewModel.Event
|
||||
@@ -20,4 +23,17 @@ sealed interface CallEvent {
|
||||
data class ShowGroupCallSafetyNumberChange(val identityRecords: List<IdentityRecord>) : CallEvent
|
||||
data object SwitchToSpeaker : CallEvent
|
||||
data object ShowSwipeToSpeakerHint : CallEvent
|
||||
data class ShowRemoteMuteToast(private val muted: Recipient, private val mutedBy: Recipient) : CallEvent {
|
||||
fun getDescription(context: Context): String {
|
||||
return if (muted.isSelf && mutedBy.isSelf) {
|
||||
context.getString(R.string.WebRtcCallView__you_muted_yourself)
|
||||
} else if (muted.isSelf) {
|
||||
context.getString(R.string.WebRtcCallView__s_remotely_muted_you, mutedBy.getDisplayName(context))
|
||||
} else if (mutedBy.isSelf) {
|
||||
context.getString(R.string.WebRtcCallView__you_remotely_muted_s, muted.getDisplayName(context))
|
||||
} else {
|
||||
context.getString(R.string.WebRtcCallView__s_remotely_muted_s, mutedBy.getDisplayName(context), muted.getDisplayName(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +399,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
WebRtcViewModel.State.CALL_OUTGOING -> handleOutgoingCall(event)
|
||||
WebRtcViewModel.State.CALL_CONNECTED -> handleCallConnected(event)
|
||||
WebRtcViewModel.State.CALL_RINGING -> handleCallRinging()
|
||||
@@ -410,6 +411,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
handleTerminate(event.recipient, HangupMessage.Type.NORMAL)
|
||||
}
|
||||
}
|
||||
|
||||
WebRtcViewModel.State.CALL_DISCONNECTED_GLARE -> handleGlare(event.recipient)
|
||||
WebRtcViewModel.State.CALL_NEEDS_PERMISSION -> handleTerminate(event.recipient, HangupMessage.Type.NEED_PERMISSION)
|
||||
WebRtcViewModel.State.CALL_RECONNECTING -> handleCallReconnecting()
|
||||
@@ -871,6 +873,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
is CallEvent.ShowGroupCallSafetyNumberChange -> SafetyNumberBottomSheet.forGroupCall(event.identityRecords).show(supportFragmentManager)
|
||||
is CallEvent.SwitchToSpeaker -> callScreen.switchToSpeakerView()
|
||||
is CallEvent.ShowSwipeToSpeakerHint -> CallToastPopupWindow.show(rootView())
|
||||
is CallEvent.ShowRemoteMuteToast -> CallToastPopupWindow.show(rootView(), R.drawable.ic_mic_off_solid_18, event.getDescription(this))
|
||||
is CallEvent.ShowVideoTooltip -> {
|
||||
if (isInPipMode()) return
|
||||
|
||||
@@ -914,6 +917,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
val formatter: EllapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(inCallStatus.elapsedTime) ?: return
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, formatter.toString()))
|
||||
}
|
||||
|
||||
is InCallStatus.PendingCallLinkUsers -> {
|
||||
val waiting = inCallStatus.pendingUserCount
|
||||
|
||||
@@ -925,6 +929,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is InCallStatus.JoinedCallLinkUsers -> {
|
||||
val joined = inCallStatus.joinedUserCount
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
private val callPeerRepository = CallPeerRepository(viewModelScope)
|
||||
|
||||
private val internalMicrophoneEnabled = MutableStateFlow(true)
|
||||
private val remoteMutedBy = MutableStateFlow<CallParticipant?>(null)
|
||||
private val isInPipMode = MutableStateFlow(false)
|
||||
private val webRtcControls = MutableStateFlow(WebRtcControls.NONE)
|
||||
private val foldableState = MutableStateFlow(WebRtcControls.FoldableState.flat())
|
||||
@@ -63,6 +64,7 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
private val isLandscapeEnabled = MutableStateFlow<Boolean?>(null)
|
||||
private val canEnterPipMode = MutableStateFlow(false)
|
||||
private val ephemeralState = MutableStateFlow<WebRtcEphemeralState?>(null)
|
||||
private val remoteMutesReported = MutableStateFlow(HashSet<CallParticipantId>())
|
||||
|
||||
private val controlsWithFoldableState: Flow<WebRtcControls> = combine(foldableState, webRtcControls, this::updateControlsFoldableState)
|
||||
private val realWebRtcControls: StateFlow<WebRtcControls> = combine(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls)
|
||||
@@ -284,8 +286,24 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
|
||||
val localParticipant = webRtcViewModel.localParticipant
|
||||
|
||||
if (remoteMutedBy.value == null && webRtcViewModel.remoteMutedBy != null) {
|
||||
remoteMutedBy.update { webRtcViewModel.remoteMutedBy }
|
||||
viewModelScope.launch {
|
||||
events.emit(
|
||||
CallEvent.ShowRemoteMuteToast(
|
||||
muted = Recipient.self(),
|
||||
mutedBy = remoteMutedBy.value!!.recipient
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internalMicrophoneEnabled.value = localParticipant.isMicrophoneEnabled
|
||||
|
||||
if (internalMicrophoneEnabled.value) {
|
||||
remoteMutedBy.update { null }
|
||||
}
|
||||
|
||||
val state: CallParticipantsState = participantsState.value!!
|
||||
val wasScreenSharing: Boolean = state.focusedParticipant.isScreenSharing
|
||||
val newState: CallParticipantsState = CallParticipantsState.update(state, webRtcViewModel, enableVideo)
|
||||
@@ -306,6 +324,26 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
for (remote in webRtcViewModel.remoteParticipants) {
|
||||
if (remote.remotelyMutedBy == null) {
|
||||
remoteMutesReported.value.remove(remote.callParticipantId)
|
||||
} else if (!remoteMutesReported.value.contains(remote.callParticipantId)) {
|
||||
remoteMutesReported.value.add(remote.callParticipantId)
|
||||
if (remote.callParticipantId.recipientId == remote.remotelyMutedBy.id) {
|
||||
// Ignore self-mutes if we're not the recipient (handled above)
|
||||
continue
|
||||
}
|
||||
viewModelScope.launch {
|
||||
events.emit(
|
||||
CallEvent.ShowRemoteMuteToast(
|
||||
muted = remote.recipient,
|
||||
mutedBy = remote.remotelyMutedBy
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousParticipantList = webRtcViewModel.remoteParticipants
|
||||
identityChangedRecipients.value = webRtcViewModel.identityChangedParticipants
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ data class CallParticipant(
|
||||
val isMediaKeysReceived: Boolean = true,
|
||||
val addedToCallTime: Long = 0,
|
||||
val isScreenSharing: Boolean = false,
|
||||
val remotelyMutedBy: Recipient? = null,
|
||||
private val deviceOrdinal: DeviceOrdinal = DeviceOrdinal.PRIMARY
|
||||
) {
|
||||
val cameraDirection: CameraState.Direction
|
||||
@@ -68,7 +69,7 @@ data class CallParticipant(
|
||||
}
|
||||
|
||||
fun withAudioEnabled(audioEnabled: Boolean): CallParticipant {
|
||||
return copy(isMicrophoneEnabled = audioEnabled)
|
||||
return copy(isMicrophoneEnabled = audioEnabled, remotelyMutedBy = if (audioEnabled) null else remotelyMutedBy)
|
||||
}
|
||||
|
||||
fun withVideoEnabled(videoEnabled: Boolean): CallParticipant {
|
||||
@@ -83,8 +84,12 @@ data class CallParticipant(
|
||||
return copy(handRaisedTimestamp = timestamp)
|
||||
}
|
||||
|
||||
fun withRemotelyMutedBy(source: Recipient): CallParticipant {
|
||||
return copy(remotelyMutedBy = source)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CallParticipant(callParticipantId=$callParticipantId, isForwardingVideo=$isForwardingVideo, isVideoEnabled=$isVideoEnabled, isMicrophoneEnabled=$isMicrophoneEnabled, handRaisedTimestamp=$handRaisedTimestamp, isMediaKeysReceived=$isMediaKeysReceived, isScreenSharing=$isScreenSharing)"
|
||||
return "CallParticipant(callParticipantId=$callParticipantId, isForwardingVideo=$isForwardingVideo, isVideoEnabled=$isVideoEnabled, isMicrophoneEnabled=$isMicrophoneEnabled, handRaisedTimestamp=$handRaisedTimestamp, isMediaKeysReceived=$isMediaKeysReceived, isScreenSharing=$isScreenSharing, remotelyMutedBy=$remotelyMutedBy)"
|
||||
}
|
||||
|
||||
enum class DeviceOrdinal {
|
||||
|
||||
@@ -129,6 +129,8 @@ class WebRtcViewModel(state: WebRtcServiceState) {
|
||||
state.localDeviceState.handRaisedTimestamp
|
||||
)
|
||||
|
||||
val remoteMutedBy: CallParticipant? = state.localDeviceState.remoteMutedBy
|
||||
|
||||
val isCellularConnection: Boolean = when (state.localDeviceState.networkConnectionType) {
|
||||
PeerConnection.AdapterType.UNKNOWN,
|
||||
PeerConnection.AdapterType.ETHERNET,
|
||||
@@ -166,7 +168,8 @@ class WebRtcViewModel(state: WebRtcServiceState) {
|
||||
activeDevice=$activeDevice,
|
||||
availableDevices=$availableDevices,
|
||||
bluetoothPermissionDenied=$bluetoothPermissionDenied,
|
||||
ringGroup=$ringGroup
|
||||
ringGroup=$ringGroup,
|
||||
remoteMutedBy=$remoteMutedBy
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
@@ -197,6 +200,7 @@ class WebRtcViewModel(state: WebRtcServiceState) {
|
||||
if (availableDevices != previousEvent.availableDevices) builder.append(" availableDevices=$availableDevices\n")
|
||||
if (bluetoothPermissionDenied != previousEvent.bluetoothPermissionDenied) builder.append(" bluetoothPermissionDenied=$bluetoothPermissionDenied\n")
|
||||
if (ringGroup != previousEvent.ringGroup) builder.append(" ringGroup=$ringGroup\n")
|
||||
if (remoteMutedBy != previousEvent.remoteMutedBy) builder.append(" remoteMutedBy=$remoteMutedBy\n")
|
||||
|
||||
if (builder.isEmpty()) {
|
||||
"<no change>"
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -121,6 +122,65 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleRemoteMuteRequest(@NonNull WebRtcServiceState currentState, long sourceDemuxId) {
|
||||
Log.i(tag, "handleRemoteMuteRequest():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
|
||||
|
||||
if (!currentState.getLocalDeviceState().isMicrophoneEnabled()) {
|
||||
// Nothing to do.
|
||||
return currentState;
|
||||
}
|
||||
|
||||
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
|
||||
if (entry.getKey().getDemuxId() == sourceDemuxId) {
|
||||
try {
|
||||
groupCall.setOutgoingAudioMutedRemotely(sourceDemuxId);
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to set attribution of remote mute", e);
|
||||
}
|
||||
return currentState.builder().changeLocalDeviceState().setRemoteMutedBy(entry.getValue()).build();
|
||||
}
|
||||
}
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleObservedRemoteMute(@NonNull WebRtcServiceState currentState, long sourceDemuxId, long targetDemuxId) {
|
||||
Log.i(tag, "handleObservedRemoteMute not processed");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
|
||||
|
||||
Long selfDemuxId = groupCall.getLocalDeviceState().getDemuxId();
|
||||
Recipient source = null;
|
||||
if (selfDemuxId != null && sourceDemuxId == selfDemuxId) {
|
||||
source = Recipient.self();
|
||||
} else {
|
||||
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
|
||||
if (entry.getKey().getDemuxId() == sourceDemuxId) {
|
||||
source = entry.getValue().getRecipient();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source == null) {
|
||||
Log.w(tag, "handleObservedRemoteMute: source not found");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
|
||||
if (entry.getKey().getDemuxId() == targetDemuxId) {
|
||||
WebRtcServiceStateBuilder.CallInfoStateBuilder builder = currentState.builder().changeCallInfoState().putParticipant(entry.getKey(), entry.getValue().withRemotelyMutedBy(source));
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcEphemeralState handleGroupAudioLevelsChanged(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState) {
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
|
||||
@@ -971,6 +971,17 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
||||
process((s, p) -> p.handleGroupCallEnded(s, groupCall.hashCode(), groupCallEndReason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteMuteRequest(@NonNull GroupCall groupCall, long sourceDemuxId) {
|
||||
process((s, p) -> p.handleRemoteMuteRequest(s, sourceDemuxId));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onObservedRemoteMute(@NonNull GroupCall groupCall, long sourceDemuxId, long targetDemuxId) {
|
||||
process((s, p) -> p.handleObservedRemoteMute(s, sourceDemuxId, targetDemuxId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpeakingNotification(@NonNull GroupCall groupCall, @NonNull GroupCall.SpeechEvent speechEvent) {
|
||||
process((s, p) -> p.handleGroupCallSpeechEvent(s, speechEvent));
|
||||
|
||||
@@ -786,6 +786,16 @@ public abstract class WebRtcActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleRemoteMuteRequest(@NonNull WebRtcServiceState currentState, long sourceDemuxId) {
|
||||
Log.i(tag, "handleRemoteMuteRequest not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleObservedRemoteMute(@NonNull WebRtcServiceState currentState, long sourceDemuxId, long targetDemuxId) {
|
||||
Log.i(tag, "handleObservedRemoteMute not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleGroupCallSpeechEvent(@NonNull WebRtcServiceState currentState, @NonNull GroupCall.SpeechEvent speechEvent) {
|
||||
Log.i(tag, "handleGroupCallSpeechEvent not processed");
|
||||
return currentState;
|
||||
|
||||
@@ -19,7 +19,8 @@ data class LocalDeviceState(
|
||||
var availableDevices: Set<SignalAudioManager.AudioDevice> = emptySet(),
|
||||
var bluetoothPermissionDenied: Boolean = false,
|
||||
var networkConnectionType: PeerConnection.AdapterType = PeerConnection.AdapterType.UNKNOWN,
|
||||
var handRaisedTimestamp: Long = CallParticipant.HAND_LOWERED
|
||||
var handRaisedTimestamp: Long = CallParticipant.HAND_LOWERED,
|
||||
var remoteMutedBy: CallParticipant? = null
|
||||
) {
|
||||
|
||||
fun duplicate(): LocalDeviceState {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.OptionalLong;
|
||||
|
||||
import org.checkerframework.checker.units.qual.N;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.components.sensors.Orientation;
|
||||
@@ -101,6 +102,10 @@ public class WebRtcServiceStateBuilder {
|
||||
|
||||
public @NonNull LocalDeviceStateBuilder isMicrophoneEnabled(boolean enabled) {
|
||||
toBuild.setMicrophoneEnabled(enabled);
|
||||
if (enabled) {
|
||||
// Clear any remote mute attribution.
|
||||
toBuild.setRemoteMutedBy(null);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -143,6 +148,12 @@ public class WebRtcServiceStateBuilder {
|
||||
toBuild.setHandRaisedTimestamp(handRaisedTimestamp);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull LocalDeviceStateBuilder setRemoteMutedBy(@NonNull CallParticipant participant) {
|
||||
toBuild.setRemoteMutedBy(participant);
|
||||
toBuild.setMicrophoneEnabled(false);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class CallSetupStateBuilder {
|
||||
|
||||
Reference in New Issue
Block a user