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:
Miriam Zimmerman
2025-04-25 10:06:47 -04:00
committed by Cody Henthorne
parent ed9a945f05
commit 3d7162cdd3
14 changed files with 191 additions and 10 deletions

View File

@@ -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,

View File

@@ -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))
}
}
}
}

View File

@@ -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

View File

@@ -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
}