Add Small Group Ringing support.

This commit is contained in:
Cody Henthorne
2021-08-24 10:18:39 -04:00
committed by Alex Hart
parent 5787a5f68a
commit db7272730e
39 changed files with 1597 additions and 609 deletions

View File

@@ -10,7 +10,6 @@ import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -21,13 +20,11 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.VideoTrack;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -186,40 +183,37 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():");
protected @NonNull WebRtcServiceState handleSetRingGroup(@NonNull WebRtcServiceState currentState, boolean ringGroup) {
Log.i(tag, "handleReceivedOpaqueMessage(): ring: " + ringGroup);
try {
webRtcInteractor.getCallManager().receivedCallMessage(opaqueMessageMetadata.getUuid(),
opaqueMessageMetadata.getRemoteDeviceId(),
1,
opaqueMessageMetadata.getOpaque(),
opaqueMessageMetadata.getMessageAgeSeconds());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to receive opaque message", e);
if (currentState.getCallSetupState().shouldRingGroup() == ringGroup) {
return currentState;
}
return currentState;
return currentState.builder()
.changeCallSetupState()
.setRingGroup(ringGroup)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupMessageSentError(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull WebRtcViewModel.State errorCallState,
@NonNull Optional<IdentityKey> identityKey)
@NonNull Collection<RecipientId> recipientIds,
@NonNull WebRtcViewModel.State errorCallState)
{
Log.w(tag, "handleGroupMessageSentError(): error: " + errorCallState);
if (errorCallState == WebRtcViewModel.State.UNTRUSTED_IDENTITY) {
return currentState.builder()
.changeCallInfoState()
.addIdentityChangedRecipient(remotePeer.getId())
.addIdentityChangedRecipients(recipientIds)
.build();
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupApproveSafetyNumberChange(@NonNull WebRtcServiceState currentState,
@NonNull List<RecipientId> recipientIds)
{
@@ -291,56 +285,4 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
return terminateGroupCall(currentState);
}
public @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
try {
if (groupCall != null) {
groupCall.disconnect();
}
webRtcInteractor.getCallManager().reset();
} catch (CallException e) {
Log.w(tag, "Unable to reset call manager: ", e);
}
return terminateGroupCall(currentState);
}
public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) {
return terminateGroupCall(currentState, true);
}
public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState, boolean terminateVideo) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.stopForegroundService();
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
if (terminateVideo) {
WebRtcVideoUtil.deinitializeVideo(currentState);
}
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(context, currentState.getCallInfoState().getCallRecipient());
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
}

View File

@@ -0,0 +1,14 @@
package org.thoughtcrime.securesms.service.webrtc
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.UUID
data class GroupCallRingCheckInfo(
val recipientId: RecipientId,
val groupId: GroupId.V2,
val ringId: Long,
val ringerUuid: UUID,
val ringUpdate: CallManager.RingUpdate
)

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
@@ -12,11 +14,10 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
/**
* Process actions to go from lobby to a joined call.
*/
@@ -81,6 +82,14 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
throw new RuntimeException(e);
}
if (FeatureFlags.groupCallRinging() && currentState.getCallSetupState().shouldRingGroup()) {
try {
groupCall.ringAll();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to ring group", e);
}
}
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_CONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED)
@@ -89,8 +98,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
.changeLocalDeviceState()
.wantsBluetooth(true)
.commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor))
.build();
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor));
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
builder.changeCallInfoState()
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)

View File

@@ -3,15 +3,17 @@ package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.webrtc.CapturerObserver;
import org.webrtc.VideoFrame;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.UUID;
/**
* Action handler for when the system is at rest. Mainly responsible
* for starting pre-call state, starting an outgoing call, or receiving an
@@ -66,4 +68,59 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
return isGroupCall ? currentState.getActionProcessor().handlePreJoinCall(currentState, remotePeer)
: currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupCallRingUpdate(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeerGroup,
@NonNull GroupId.V2 groupId,
long ringId,
@NonNull UUID uuid,
@NonNull CallManager.RingUpdate ringUpdate)
{
Log.i(TAG, "handleGroupCallRingUpdate(): recipient: " + remotePeerGroup.getId() + " ring: " + ringId + " update: " + ringUpdate);
if (ringUpdate != CallManager.RingUpdate.REQUESTED) {
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
return currentState;
} else if (DatabaseFactory.getGroupCallRingDatabase(context).isCancelled(ringId)) {
try {
Log.i(TAG, "Incoming ring request for already cancelled ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, null);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
}
return currentState;
}
webRtcInteractor.peekGroupCallForRingingCheck(new GroupCallRingCheckInfo(remotePeerGroup.getId(), groupId, ringId, uuid, ringUpdate));
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleReceivedGroupCallPeekForRingingCheck(@NonNull WebRtcServiceState currentState, @NonNull GroupCallRingCheckInfo info, long deviceCount) {
Log.i(tag, "handleReceivedGroupCallPeekForRingingCheck(): recipient: " + info.getRecipientId() + " ring: " + info.getRingId() + " deviceCount: " + deviceCount);
if (DatabaseFactory.getGroupCallRingDatabase(context).isCancelled(info.getRingId())) {
try {
Log.i(TAG, "Ring was cancelled while getting peek info ring: " + info.getRingId());
webRtcInteractor.getCallManager().cancelGroupRing(info.getGroupId().getDecodedId(), info.getRingId(), null);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + info.getRingId(), e);
}
return currentState;
}
if (deviceCount == 0) {
Log.i(TAG, "No one in the group call, mark as expired and do not ring");
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(info.getRingId(), System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
return currentState;
}
currentState = currentState.builder()
.actionProcessor(new IncomingGroupCallActionProcessor(webRtcInteractor))
.build();
return currentState.getActionProcessor().handleGroupCallRingUpdate(currentState, new RemotePeer(info.getRecipientId()), info.getGroupId(), info.getRingId(), info.getRingerUuid(), info.getRingUpdate());
}
}

View File

@@ -0,0 +1,258 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.UUID;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_CONNECTING;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_RINGING;
/**
* Process actions to go from incoming "ringing" group call to joining. By the time this processor
* is running, the group call to ring has been verified to have at least one active device.
*/
public final class IncomingGroupCallActionProcessor extends DeviceAwareActionProcessor {
private static final String TAG = Log.tag(IncomingGroupCallActionProcessor.class);
public IncomingGroupCallActionProcessor(WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleGroupCallRingUpdate(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeerGroup,
@NonNull GroupId.V2 groupId,
long ringId,
@NonNull UUID uuid,
@NonNull CallManager.RingUpdate ringUpdate)
{
Log.i(TAG, "handleGroupCallRingUpdate(): recipient: " + remotePeerGroup.getId() + " ring: " + ringId + " update: " + ringUpdate);
Recipient recipient = remotePeerGroup.getRecipient();
boolean updateForCurrentRingId = ringId == currentState.getCallSetupState().getRingId();
boolean isCurrentlyRinging = currentState.getCallInfoState().getGroupCallState().isRinging();
if (DatabaseFactory.getGroupCallRingDatabase(context).isCancelled(ringId)) {
try {
Log.i(TAG, "Ignoring incoming ring request for already cancelled ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, null);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
}
return currentState;
}
if (ringUpdate != CallManager.RingUpdate.REQUESTED) {
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
if (updateForCurrentRingId && isCurrentlyRinging) {
Log.i(TAG, "Cancelling current ring: " + ringId);
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
return terminateGroupCall(currentState);
} else {
return currentState;
}
}
if (!updateForCurrentRingId && isCurrentlyRinging) {
try {
Log.i(TAG, "Already ringing so reply busy for new ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.Busy);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
}
return currentState;
}
if (updateForCurrentRingId) {
Log.i(TAG, "Already ringing for ring: " + ringId);
return currentState;
}
Log.i(TAG, "Requesting new ring: " + ringId);
DatabaseFactory.getGroupCallRingDatabase(context).insertGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext());
if (shouldDisturbUserWithCall) {
boolean started = webRtcInteractor.startWebRtcCallActivityIfPossible();
if (!started) {
Log.i(TAG, "Unable to start call activity due to OS version or not being in the foreground");
ApplicationDependencies.getAppForegroundObserver().addListener(webRtcInteractor.getForegroundListener());
}
}
webRtcInteractor.initializeAudioForCall();
if (shouldDisturbUserWithCall && SignalStore.settings().isCallNotificationsEnabled()) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientDatabase.VibrateState vibrateState = recipient.resolve().getCallVibrate();
if (ringtone == null) {
ringtone = SignalStore.settings().getCallRingtone();
}
webRtcInteractor.startIncomingRinger(ringtone, vibrateState == RecipientDatabase.VibrateState.ENABLED || (vibrateState == RecipientDatabase.VibrateState.DEFAULT && SignalStore.settings().isCallVibrateEnabled()));
}
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup);
webRtcInteractor.registerPowerButtonReceiver();
return currentState.builder()
.changeCallSetupState()
.isRemoteVideoOffer(true)
.ringId(ringId)
.ringerRecipient(Recipient.externalPush(context, uuid, null, false))
.commit()
.changeCallInfoState()
.callRecipient(remotePeerGroup.getRecipient())
.callState(WebRtcViewModel.State.CALL_INCOMING)
.groupCallState(WebRtcViewModel.GroupCallState.RINGING)
.putParticipant(remotePeerGroup.getRecipient(),
CallParticipant.createRemote(new CallParticipantId(remotePeerGroup.getRecipient()),
remotePeerGroup.getRecipient(),
null,
new BroadcastVideoSink(currentState.getVideoState().getLockableEglBase(),
false,
true,
currentState.getLocalDeviceState().getOrientation().getDegrees()),
true,
false,
0,
true,
0,
false,
CallParticipant.DeviceOrdinal.PRIMARY
))
.build();
}
@Override
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId();
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
SignalStore.internalValues().groupCallingServer(),
currentState.getVideoState().getLockableEglBase().require(),
webRtcInteractor.getGroupCallObserver());
try {
groupCall.setOutgoingAudioMuted(true);
groupCall.setOutgoingVideoMuted(true);
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
Log.i(TAG, "Connecting to group call: " + currentState.getCallInfoState().getCallRecipient().getId());
groupCall.connect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to connect to group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.groupCall(groupCall)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.commit()
.changeCallSetupState()
.enableVideoOnCreate(answerWithVideo)
.build();
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.setWantsBluetoothConnection(true);
try {
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
groupCall.setOutgoingVideoMuted(answerWithVideo);
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
groupCall.join();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to join group call", e);
}
return currentState.builder()
.actionProcessor(new GroupJoiningActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_OUTGOING)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleDenyCall(@NonNull WebRtcServiceState currentState) {
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
Optional<GroupId> groupId = recipient.getGroupId();
long ringId = currentState.getCallSetupState().getRingId();
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
try {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.get().getDecodedId(),
ringId,
CallManager.RingCancelReason.DeclinedByUser);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring " + ringId, e);
}
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.stopAudio(false);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();
return WebRtcVideoUtil.deinitializeVideo(currentState)
.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor))
.terminate()
.build();
}
}

View File

@@ -1,5 +1,11 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
@@ -21,10 +27,13 @@ import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.HttpHeader;
import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupIdentifier;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -33,6 +42,7 @@ import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messages.GroupSendUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
@@ -42,7 +52,9 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection;
@@ -51,6 +63,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
@@ -63,16 +76,12 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
import java.util.stream.Collectors;
/**
* Entry point for all things calling. Lives for the life of the app instance and will spin up a foreground service when needed to
@@ -94,6 +103,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
private final LockManager lockManager;
private WebRtcServiceState serviceState;
private boolean needsToSetSelfUuid = true;
public SignalCallManager(@NonNull Application application) {
this.context = application.getApplicationContext();
@@ -136,6 +146,15 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
}
serviceExecutor.execute(() -> {
if (needsToSetSelfUuid) {
try {
callManager.setSelfUuid(Recipient.self().requireUuid());
needsToSetSelfUuid = false;
} catch (CallException e) {
Log.w(TAG, "Unable to set self UUID on CallManager", e);
}
}
Log.v(TAG, "Processing action, handler: " + serviceState.getActionProcessor().getTag());
WebRtcServiceState previous = serviceState;
serviceState = action.process(previous, previous.getActionProcessor());
@@ -270,6 +289,14 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleReceivedOpaqueMessage(s, opaqueMessageMetadata));
}
public void setRingGroup(boolean ringGroup) {
process((s, p) -> p.handleSetRingGroup(s, ringGroup));
}
private void receivedGroupCallPeekForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo, long deviceCount) {
process((s, p) -> p.handleReceivedGroupCallPeekForRingingCheck(s, groupCallRingCheckInfo, deviceCount));
}
public void peekGroupCall(@NonNull RecipientId id) {
if (callManager == null) {
Log.i(TAG, "Unable to peekGroupCall, call manager is null");
@@ -307,6 +334,33 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
});
}
public void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo info) {
if (callManager == null) {
Log.i(TAG, "Unable to peekGroupCall, call manager is null");
return;
}
networkExecutor.execute(() -> {
try {
Recipient group = Recipient.resolved(info.getRecipientId());
GroupId.V2 groupId = group.requireGroupId().requireV2();
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(context, groupId);
List<GroupCall.GroupMemberInfo> members = GroupManager.getUuidCipherTexts(context, groupId)
.entrySet()
.stream()
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
.collect(Collectors.toList());
callManager.peekGroupCall(SignalStore.internalValues().groupCallingServer(), credential.getTokenBytes().toByteArray(), members, peekInfo -> {
receivedGroupCallPeekForRingingCheck(info, peekInfo.getDeviceCount());
});
} catch (IOException | VerificationFailedException | CallException e) {
Log.e(TAG, "error peeking for ringing check", e);
}
});
}
public boolean startCallCardActivityIfPossible() {
if (Build.VERSION.SDK_INT >= 29 && !ApplicationDependencies.getAppForegroundObserver().isForegrounded()) {
return false;
@@ -535,7 +589,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
}
@Override
public void onSendCallMessage(@NonNull final UUID uuid, @NonNull final byte[] bytes) {
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] bytes, @NonNull CallManager.CallMessageUrgency unused) {
Log.i(TAG, "onSendCallMessage():");
OpaqueMessage opaqueMessage = new OpaqueMessage(bytes);
@@ -553,10 +607,49 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
} catch (UntrustedIdentityException e) {
Log.i(TAG, "sendOpaqueCallMessage onFailure: ", e);
RetrieveProfileJob.enqueue(recipient.getId());
process((s, p) -> p.handleGroupMessageSentError(s, new RemotePeer(recipient.getId()), UNTRUSTED_IDENTITY, Optional.fromNullable(e.getIdentityKey())));
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), UNTRUSTED_IDENTITY));
} catch (IOException e) {
Log.i(TAG, "sendOpaqueCallMessage onFailure: ", e);
process((s, p) -> p.handleGroupMessageSentError(s, new RemotePeer(recipient.getId()), NETWORK_FAILURE, Optional.absent()));
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), NETWORK_FAILURE));
}
});
}
@Override
public void onSendCallMessageToGroup(@NonNull byte[] groupIdBytes, @NonNull byte[] message, @NonNull CallManager.CallMessageUrgency unused) {
Log.i(TAG, "onSendCallMessageToGroup():");
networkExecutor.execute(() -> {
try {
GroupId groupId = GroupId.v2(new GroupIdentifier(groupIdBytes));
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
recipients = RecipientUtil.getEligibleForSending((recipients.stream()
.map(Recipient::resolve)
.filter(r -> !r.isBlocked())
.collect(Collectors.toList())));
OpaqueMessage opaqueMessage = new OpaqueMessage(message);
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOutgoingGroupOpaque(groupId.getDecodedId(), System.currentTimeMillis(), opaqueMessage, true, null);
RecipientAccessList accessList = new RecipientAccessList(recipients);
List<SendMessageResult> results = GroupSendUtil.sendCallMessage(context,
groupId.requireV2(),
recipients,
callMessage);
Set<RecipientId> identifyFailureRecipientIds = results.stream()
.filter(result -> result.getIdentityFailure() != null)
.map(result -> accessList.requireIdByAddress(result.getAddress()))
.collect(Collectors.toSet());
if (Util.hasItems(identifyFailureRecipientIds)) {
process((s, p) -> p.handleGroupMessageSentError(s, identifyFailureRecipientIds, UNTRUSTED_IDENTITY));
RetrieveProfileJob.enqueue(identifyFailureRecipientIds);
}
} catch (UntrustedIdentityException | IOException | InvalidInputException e) {
Log.w(TAG, "onSendCallMessageToGroup failed", e);
}
});
}
@@ -594,6 +687,22 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
});
}
@Override
public void onGroupCallRingUpdate(@NonNull byte[] groupIdBytes, long ringId, @NonNull UUID uuid, @NonNull CallManager.RingUpdate ringUpdate) {
try {
GroupId.V2 groupId = GroupId.v2(new GroupIdentifier(groupIdBytes));
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
if (group.isPresent()) {
process((s, p) -> p.handleGroupCallRingUpdate(s, new RemotePeer(group.get().getRecipientId()), groupId, ringId, uuid, ringUpdate));
} else {
Log.w(TAG, "Unable to ring unknown group.");
}
} catch (InvalidInputException e) {
Log.w(TAG, "Unable to ring group due to invalid group id", e);
}
}
@Override
public void requestMembershipProof(@NonNull final GroupCall groupCall) {
Log.i(TAG, "requestMembershipProof():");
@@ -662,8 +771,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
@Override
public void onForeground() {
process((s, p) -> {
WebRtcViewModel.State callState = s.getCallInfoState().getCallState();
if (callState == CALL_INCOMING && s.getCallInfoState().getGroupCallState() == IDLE) {
WebRtcViewModel.State callState = s.getCallInfoState().getCallState();
WebRtcViewModel.GroupCallState groupCallState = s.getCallInfoState().getGroupCallState();
if (callState == CALL_INCOMING && (groupCallState == IDLE || groupCallState.isRinging())) {
startCallCardActivityIfPossible();
}
ApplicationDependencies.getAppForegroundObserver().removeListener(this);

View File

@@ -1,5 +1,9 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata;
import android.content.Context;
import android.os.ResultReceiver;
@@ -10,14 +14,18 @@ import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.CallManager.RingUpdate;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CallState;
@@ -41,13 +49,10 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.OpaqueMessageMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata;
import java.util.UUID;
/**
* Base WebRTC action processor and core of the calling state machine. As actions (as intents)
@@ -616,9 +621,8 @@ public abstract class WebRtcActionProcessor {
}
protected @NonNull WebRtcServiceState handleGroupMessageSentError(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull WebRtcViewModel.State errorCallState,
@NonNull Optional<IdentityKey> identityKey)
@NonNull Collection<RecipientId> recipientIds,
@NonNull WebRtcViewModel.State errorCallState)
{
Log.i(tag, "handleGroupMessageSentError not processed");
return currentState;
@@ -631,11 +635,106 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
//endregion
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():");
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage not processed");
try {
webRtcInteractor.getCallManager().receivedCallMessage(opaqueMessageMetadata.getUuid(),
opaqueMessageMetadata.getRemoteDeviceId(),
1,
opaqueMessageMetadata.getOpaque(),
opaqueMessageMetadata.getMessageAgeSeconds());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to receive opaque message", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupCallRingUpdate(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeerGroup,
@NonNull GroupId.V2 groupId,
long ringId,
@NonNull UUID uuid,
@NonNull RingUpdate ringUpdate)
{
Log.i(tag, "handleGroupCallRingUpdate(): recipient: " + remotePeerGroup.getId() + " ring: " + ringId + " update: " + ringUpdate);
try {
if (ringUpdate != RingUpdate.BUSY_LOCALLY && ringUpdate != RingUpdate.BUSY_ON_ANOTHER_DEVICE) {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.Busy);
}
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(),
ringUpdate == RingUpdate.REQUESTED ? RingUpdate.BUSY_LOCALLY : ringUpdate);
} catch (CallException e) {
Log.w(tag, "Unable to cancel ring", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleSetRingGroup(@NonNull WebRtcServiceState currentState, boolean ringGroup) {
Log.i(tag, "handleSetRingGroup not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedGroupCallPeekForRingingCheck(@NonNull WebRtcServiceState currentState, @NonNull GroupCallRingCheckInfo info, long deviceCount) {
Log.i(tag, "handleReceivedGroupCallPeekForRingingCheck not processed");
return currentState;
}
protected @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
try {
if (groupCall != null) {
groupCall.disconnect();
}
webRtcInteractor.getCallManager().reset();
} catch (CallException e) {
Log.w(tag, "Unable to reset call manager: ", e);
}
return terminateGroupCall(currentState);
}
protected synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) {
return terminateGroupCall(currentState, true);
}
protected synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState, boolean terminateVideo) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();
if (terminateVideo) {
WebRtcVideoUtil.deinitializeVideo(currentState);
}
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(context, currentState.getCallInfoState().getCallRecipient());
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
//endregion
}

View File

@@ -45,9 +45,9 @@ public class WebRtcInteractor {
@NonNull GroupCall.Observer groupCallObserver,
@NonNull AppForegroundObserver.Listener foregroundListener)
{
this.context = context;
this.signalCallManager = signalCallManager;
this.lockManager = lockManager;
this.context = context;
this.signalCallManager = signalCallManager;
this.lockManager = lockManager;
this.audioManager = audioManager;
this.cameraEventListener = cameraEventListener;
this.groupCallObserver = groupCallObserver;
@@ -154,7 +154,7 @@ public class WebRtcInteractor {
audioManager.startCommunication(preserveSpeakerphone);
}
void peekGroupCall(@NonNull RecipientId recipientId) {
signalCallManager.peekGroupCall(recipientId);
void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo) {
signalCallManager.peekGroupCallForRingingCheck(groupCallRingCheckInfo);
}
}

View File

@@ -1,44 +0,0 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
/**
* Information specific to setting up a call.
*/
public final class CallSetupState {
boolean enableVideoOnCreate;
boolean isRemoteVideoOffer;
boolean acceptWithVideo;
boolean sentJoinedMessage;
public CallSetupState() {
this(false, false, false, false);
}
public CallSetupState(@NonNull CallSetupState toCopy) {
this(toCopy.enableVideoOnCreate, toCopy.isRemoteVideoOffer, toCopy.acceptWithVideo, toCopy.sentJoinedMessage);
}
public CallSetupState(boolean enableVideoOnCreate, boolean isRemoteVideoOffer, boolean acceptWithVideo, boolean sentJoinedMessage) {
this.enableVideoOnCreate = enableVideoOnCreate;
this.isRemoteVideoOffer = isRemoteVideoOffer;
this.acceptWithVideo = acceptWithVideo;
this.sentJoinedMessage = sentJoinedMessage;
}
public boolean isEnableVideoOnCreate() {
return enableVideoOnCreate;
}
public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer;
}
public boolean isAcceptWithVideo() {
return acceptWithVideo;
}
public boolean hasSentJoinedMessage() {
return sentJoinedMessage;
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.service.webrtc.state
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Information specific to setting up a call.
*/
data class CallSetupState(
var isEnableVideoOnCreate: Boolean = false,
var isRemoteVideoOffer: Boolean = false,
var isAcceptWithVideo: Boolean = false,
@get:JvmName("hasSentJoinedMessage") var sentJoinedMessage: Boolean = false,
@get:JvmName("shouldRingGroup") var ringGroup: Boolean = true,
var ringId: Long = NO_RING,
var ringerRecipient: Recipient = Recipient.UNKNOWN
) {
fun duplicate(): CallSetupState {
return copy()
}
companion object {
const val NO_RING = 0L
}
}

View File

@@ -25,7 +25,7 @@ public final class WebRtcServiceState {
public WebRtcServiceState(@NonNull WebRtcServiceState toCopy) {
this.actionProcessor = toCopy.actionProcessor;
this.callSetupState = new CallSetupState(toCopy.callSetupState);
this.callSetupState = toCopy.callSetupState.duplicate();
this.callInfoState = new CallInfoState(toCopy.callInfoState);
this.localDeviceState = new LocalDeviceState(toCopy.localDeviceState);
this.videoState = new VideoState(toCopy.videoState);

View File

@@ -126,7 +126,7 @@ public class WebRtcServiceStateBuilder {
private CallSetupState toBuild;
public CallSetupStateBuilder() {
toBuild = new CallSetupState(WebRtcServiceStateBuilder.this.toBuild.callSetupState);
toBuild = WebRtcServiceStateBuilder.this.toBuild.callSetupState.duplicate();
}
public @NonNull WebRtcServiceStateBuilder commit() {
@@ -140,22 +140,37 @@ public class WebRtcServiceStateBuilder {
}
public @NonNull CallSetupStateBuilder enableVideoOnCreate(boolean enableVideoOnCreate) {
toBuild.enableVideoOnCreate = enableVideoOnCreate;
toBuild.setEnableVideoOnCreate(enableVideoOnCreate);
return this;
}
public @NonNull CallSetupStateBuilder isRemoteVideoOffer(boolean isRemoteVideoOffer) {
toBuild.isRemoteVideoOffer = isRemoteVideoOffer;
toBuild.setRemoteVideoOffer(isRemoteVideoOffer);
return this;
}
public @NonNull CallSetupStateBuilder acceptWithVideo(boolean acceptWithVideo) {
toBuild.acceptWithVideo = acceptWithVideo;
toBuild.setAcceptWithVideo(acceptWithVideo);
return this;
}
public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
toBuild.sentJoinedMessage = sentJoinedMessage;
toBuild.setSentJoinedMessage(sentJoinedMessage);
return this;
}
public @NonNull CallSetupStateBuilder setRingGroup(boolean ringGroup) {
toBuild.setRingGroup(ringGroup);
return this;
}
public @NonNull CallSetupStateBuilder ringId(long ringId) {
toBuild.setRingId(ringId);
return this;
}
public @NonNull CallSetupStateBuilder ringerRecipient(@NonNull Recipient ringerRecipient) {
toBuild.setRingerRecipient(ringerRecipient);
return this;
}
}
@@ -270,8 +285,8 @@ public class WebRtcServiceStateBuilder {
return this;
}
public @NonNull CallInfoStateBuilder addIdentityChangedRecipient(@NonNull RecipientId id) {
toBuild.identityChangedRecipients.add(id);
public @NonNull CallInfoStateBuilder addIdentityChangedRecipients(@NonNull Collection<RecipientId> id) {
toBuild.identityChangedRecipients.addAll(id);
return this;
}