From 3d7162cdd38a85cdb0b953ead58fc75565d426fd Mon Sep 17 00:00:00 2001 From: Miriam Zimmerman Date: Fri, 25 Apr 2025 10:06:47 -0400 Subject: [PATCH] Implement remote mute receive; Update to RingRTC v2.52.0 Co-authored-by: Alex Hart Co-authored-by: Cody Henthorne --- .../webrtc/CallToastPopupWindow.java | 11 ++++ .../components/webrtc/v2/CallEvent.kt | 16 +++++ .../webrtc/v2/WebRtcCallActivity.kt | 5 ++ .../webrtc/v2/WebRtcCallViewModel.kt | 38 ++++++++++++ .../securesms/events/CallParticipant.kt | 9 ++- .../securesms/events/WebRtcViewModel.kt | 6 +- .../webrtc/GroupConnectedActionProcessor.java | 60 +++++++++++++++++++ .../service/webrtc/SignalCallManager.java | 11 ++++ .../service/webrtc/WebRtcActionProcessor.java | 10 ++++ .../service/webrtc/state/LocalDeviceState.kt | 3 +- .../state/WebRtcServiceStateBuilder.java | 11 ++++ app/src/main/res/values/strings.xml | 9 +++ gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 10 ++-- 14 files changed, 191 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java index d982670f06..289893af1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt index 1dd1d43f96..7396775ddb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt @@ -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) : 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)) + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt index 074f8d3b5e..e7c11b1938 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt index 60898f7c8b..66c1623085 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt @@ -56,6 +56,7 @@ class WebRtcCallViewModel : ViewModel() { private val callPeerRepository = CallPeerRepository(viewModelScope) private val internalMicrophoneEnabled = MutableStateFlow(true) + private val remoteMutedBy = MutableStateFlow(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(null) private val canEnterPipMode = MutableStateFlow(false) private val ephemeralState = MutableStateFlow(null) + private val remoteMutesReported = MutableStateFlow(HashSet()) private val controlsWithFoldableState: Flow = combine(foldableState, webRtcControls, this::updateControlsFoldableState) private val realWebRtcControls: StateFlow = 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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt index a1f91d08b3..7e479771c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt index b4c6c94b7a..0de14faecc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt @@ -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()) { "" diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java index 338fc35f26..f3a21f6020 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -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 participants = currentState.getCallInfoState().getRemoteCallParticipantsMap(); + + if (!currentState.getLocalDeviceState().isMicrophoneEnabled()) { + // Nothing to do. + return currentState; + } + + for (Map.Entry 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 participants = currentState.getCallInfoState().getRemoteCallParticipantsMap(); + + Long selfDemuxId = groupCall.getLocalDeviceState().getDemuxId(); + Recipient source = null; + if (selfDemuxId != null && sourceDemuxId == selfDemuxId) { + source = Recipient.self(); + } else { + for (Map.Entry 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 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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index f95143c6e9..f0a90c3389 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -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)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 0b63951e2d..8087459588 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt index 82422bd3e2..6fabb266e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt @@ -19,7 +19,8 @@ data class LocalDeviceState( var availableDevices: Set = 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index a3b06813d6..3b75f1a523 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -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 { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 315037fa67..d1644d240b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2482,6 +2482,15 @@ Toggle group ringing + + %1$s muted you. + + You muted %1$s. + + %1$s muted %2$s. + + You muted yourself from another device. + A UI error occurred. Please report this error to the developers. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a464a941e..47aeffa86a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -143,7 +143,7 @@ libsignal-client = { module = "org.signal:libsignal-client", version.ref = "libs libsignal-android = { module = "org.signal:libsignal-android", version.ref = "libsignal-client" } protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobuf-gradle-plugin" } signal-aesgcmprovider = "org.signal:aesgcmprovider:0.0.4" -signal-ringrtc = "org.signal:ringrtc-android:2.50.6" +signal-ringrtc = "org.signal:ringrtc-android:2.52.0" signal-android-database-sqlcipher = "org.signal:sqlcipher-android:4.6.0-S1" # Third Party diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 463f80051d..296eff4f99 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -7188,12 +7188,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + +