Add initial support for Group Calling.

This commit is contained in:
Cody Henthorne
2020-11-11 15:11:03 -05:00
parent 696fffb603
commit b1f6786392
53 changed files with 1887 additions and 130 deletions

View File

@@ -15,14 +15,19 @@ import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.CallManager.CallEvent;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.HttpHeader;
import org.signal.ringrtc.IceCandidate;
import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.zkgroup.VerificationFailedException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
@@ -30,8 +35,10 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
@@ -55,12 +62,15 @@ import org.whispersystems.libsignal.util.Pair;
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.calls.CallingResponse;
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 org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@@ -71,8 +81,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WebRtcCallService extends Service implements CallManager.Observer,
BluetoothStateManager.BluetoothStateListener,
CameraEventListener
BluetoothStateManager.BluetoothStateListener,
CameraEventListener,
GroupCall.Observer
{
private static final String TAG = WebRtcCallService.class.getSimpleName();
@@ -107,6 +118,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public static final String EXTRA_CAMERA_STATE = "camera_state";
public static final String EXTRA_IS_ALWAYS_TURN = "is_always_turn";
public static final String EXTRA_TURN_SERVER_INFO = "turn_server_info";
public static final String EXTRA_GROUP_EXTERNAL_TOKEN = "group_external_token";
public static final String EXTRA_HTTP_REQUEST_ID = "http_request_id";
public static final String EXTRA_HTTP_RESPONSE_STATUS = "http_response_status";
public static final String EXTRA_HTTP_RESPONSE_BODY = "http_response_body";
public static final String EXTRA_OPAQUE_MESSAGE = "opaque";
public static final String EXTRA_UUID = "uuid";
public static final String EXTRA_MESSAGE_AGE_SECONDS = "message_age_seconds";
public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN";
public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL";
@@ -158,6 +176,17 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public static final String ACTION_CAMERA_SWITCH_COMPLETED = "CAMERA_FLIP_COMPLETE";
public static final String ACTION_TURN_SERVER_UPDATE = "TURN_SERVER_UPDATE";
public static final String ACTION_SETUP_FAILURE = "SETUP_FAILURE";
public static final String ACTION_HTTP_SUCCESS = "HTTP_SUCCESS";
public static final String ACTION_HTTP_FAILURE = "HTTP_FAILURE";
public static final String ACTION_SEND_OPAQUE_MESSAGE = "SEND_OPAQUE_MESSAGE";
public static final String ACTION_RECEIVE_OPAQUE_MESSAGE = "RECEIVE_OPAQUE_MESSAGE";
public static final String ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED = "GROUP_LOCAL_DEVICE_CHANGE";
public static final String ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED = "GROUP_REMOTE_DEVICE_CHANGE";
public static final String ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED = "GROUP_JOINED_MEMBERS_CHANGE";
public static final String ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF = "GROUP_REQUEST_MEMBERSHIP_PROOF";
public static final String ACTION_GROUP_REQUEST_UPDATE_MEMBERS = "GROUP_REQUEST_UPDATE_MEMBERS";
public static final String ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS = "GROUP_UPDATE_RENDERED_RESOLUTIONS";
public static final int BUSY_TONE_LENGTH = 2000;
@@ -215,7 +244,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
return false;
}
webRtcInteractor = new WebRtcInteractor(this, callManager, lockManager, new SignalAudioManager(this), bluetoothStateManager, this);
webRtcInteractor = new WebRtcInteractor(this,
callManager,
lockManager,
new SignalAudioManager(this),
bluetoothStateManager,
this,
this);
return true;
}
@@ -366,9 +401,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
});
}
public void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
public void setCallInProgressNotification(int type, @NonNull Recipient recipient) {
startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type),
CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient()));
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient));
}
public void sendMessage() {
@@ -377,6 +412,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public void sendMessage(@NonNull WebRtcServiceState state) {
EventBus.getDefault().postSticky(new WebRtcViewModel(state.getCallInfoState().getCallState(),
state.getCallInfoState().getGroupCallState(),
state.getCallInfoState().getCallRecipient(),
state.getLocalDeviceState().getCameraState(),
state.getVideoState().getLocalSink(),
@@ -612,6 +648,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
listenableFutureTask.addListener(new SendCallMessageListener<>(remotePeer));
}
public void sendOpaqueCallMessage(@NonNull UUID uuid, @NonNull SignalServiceCallMessage opaqueMessage) {
sendMessage(new RemotePeer(RecipientId.from(uuid, null)), opaqueMessage);
}
@Override
public void onStartCall(@Nullable Remote remote, @NonNull CallId callId, @NonNull Boolean isOutgoing, @Nullable CallManager.CallMediaType callMediaType) {
Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType);
@@ -871,12 +911,93 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
@Override
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] bytes) {
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] opaque) {
Log.i(TAG, "onSendCallMessage:");
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(ACTION_SEND_OPAQUE_MESSAGE)
.putExtra(EXTRA_UUID, uuid.toString())
.putExtra(EXTRA_OPAQUE_MESSAGE, opaque);
startService(intent);
}
@Override
public void onSendHttpRequest(long l, @NonNull String s, @NonNull CallManager.HttpMethod httpMethod, @Nullable List<HttpHeader> list, @Nullable byte[] bytes) {
Log.i(TAG, "onSendHttpRequest:");
public void onSendHttpRequest(long requestId, @NonNull String url, @NonNull CallManager.HttpMethod httpMethod, @Nullable List<HttpHeader> headers, @Nullable byte[] body) {
Log.i(TAG, "onSendHttpRequest(): request_id: " + requestId);
networkExecutor.execute(() -> {
List<Pair<String, String>> headerPairs;
if (headers != null) {
headerPairs = Stream.of(headers)
.map(header -> new Pair<>(header.getName(), header.getValue()))
.toList();
} else {
headerPairs = Collections.emptyList();
}
CallingResponse response = messageSender.makeCallingRequest(requestId, url, httpMethod.name(), headerPairs, body);
Intent intent = new Intent(this, WebRtcCallService.class);
if (response instanceof CallingResponse.Success) {
CallingResponse.Success success = (CallingResponse.Success) response;
intent.setAction(ACTION_HTTP_SUCCESS)
.putExtra(EXTRA_HTTP_REQUEST_ID, success.getRequestId())
.putExtra(EXTRA_HTTP_RESPONSE_STATUS, success.getResponseStatus())
.putExtra(EXTRA_HTTP_RESPONSE_BODY, success.getResponseBody());
} else {
intent.setAction(ACTION_HTTP_FAILURE)
.putExtra(EXTRA_HTTP_REQUEST_ID, response.getRequestId());
}
startService(intent);
});
}
@Override
public void requestMembershipProof(@NonNull GroupCall groupCall) {
Log.i(TAG, "requestMembershipProof():");
networkExecutor.execute(() -> {
try {
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(this, serviceState.getCallInfoState().getCallRecipient().getGroupId().get().requireV2());
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF)
.putExtra(EXTRA_GROUP_EXTERNAL_TOKEN, credential.getTokenBytes().toByteArray());
startService(intent);
} catch (IOException | VerificationFailedException e) {
Log.w(TAG, "Unable to fetch group membership proof", e);
}
});
}
@Override
public void requestGroupMembers(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_REQUEST_UPDATE_MEMBERS));
}
@Override
public void onLocalDeviceStateChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED));
}
@Override
public void onRemoteDeviceStatesChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED));
}
@Override
public void onJoinedMembersChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED));
}
@Override
public void onEnded(@NonNull GroupCall groupCall, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
Log.i(TAG, "onEnded: " + groupCallEndReason);
}
}

View File

@@ -116,7 +116,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
Log.i(tag, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId());
CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
CallParticipant newParticipant = oldParticipant.withVideoEnabled(enable);
return currentState.builder()

View File

@@ -0,0 +1,214 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.util.LongSparseArray;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
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.signalservice.api.messages.calls.OpaqueMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Base group call action processor that handles general callbacks around call members
* and call specific setup information that is the same for any group call state.
*/
public class GroupActionProcessor extends DeviceAwareActionProcessor {
public GroupActionProcessor(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
super(webRtcInteractor, tag);
}
@Override
protected @NonNull WebRtcServiceState handleGroupRemoteDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRemoteDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
WebRtcServiceStateBuilder.CallInfoStateBuilder builder = currentState.builder()
.changeCallInfoState()
.clearParticipantMap();
LongSparseArray<GroupCall.RemoteDeviceState> remoteDevices = groupCall.getRemoteDeviceStates();
for(int i = 0; i < remoteDevices.size(); i++) {
GroupCall.RemoteDeviceState device = remoteDevices.get(remoteDevices.keyAt(i));
Recipient recipient = Recipient.externalPush(context, device.getUserId(), null, false);
CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId());
CallParticipant callParticipant = participants.get(callParticipantId);
BroadcastVideoSink videoSink;
VideoTrack videoTrack = device.getVideoTrack();
if (videoTrack != null) {
videoSink = (callParticipant != null && callParticipant.getVideoSink().getEglBase() != null) ? callParticipant.getVideoSink()
: new BroadcastVideoSink(currentState.getVideoState().requireEglBase());
videoTrack.addSink(videoSink);
} else {
videoSink = new BroadcastVideoSink(null);
}
builder.putParticipant(callParticipantId,
CallParticipant.createRemote(recipient,
null,
videoSink,
true));
}
return builder.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
Log.i(tag, "handleGroupRequestMembershipProof():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.setMembershipProof(groupMembershipToken);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set group membership proof", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupRequestUpdateMembers(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRequestUpdateMembers():");
Recipient group = currentState.getCallInfoState().getCallRecipient();
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(context, group.requireGroupId().requireV2()))
.map(e -> new GroupCall.GroupMemberInfo(e.getKey(), e.getValue().serialize()))
.toList();
try {
groupCall.setGroupMembers(new ArrayList<>(members));
} catch (CallException e) {
return groupCallFailure(currentState, "Unable set group members", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
ArrayList<GroupCall.RenderedResolution> renderedResolutions = new ArrayList<>(participants.size());
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
BroadcastVideoSink.RequestedSize maxSize = entry.getValue().getVideoSink().getMaxRequestingSize();
renderedResolutions.add(new GroupCall.RenderedResolution(entry.getKey().getDemuxId(), maxSize.getWidth(), maxSize.getHeight(), null));
}
try {
currentState.getCallInfoState().requireGroupCall().setRenderedResolutions(renderedResolutions);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set rendered resolutions", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
try {
webRtcInteractor.getCallManager().receivedHttpResponse(httpData.getRequestId(), httpData.getStatus(), httpData.getBody() != null ? httpData.getBody() : new byte[0]);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
try {
webRtcInteractor.getCallManager().httpRequestFailed(httpData.getRequestId());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSendOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleSendOpaqueMessage():");
OpaqueMessage opaqueMessage = new OpaqueMessage(opaqueMessageMetadata.getOpaque());
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOpaque(opaqueMessage, true, null);
webRtcInteractor.sendOpaqueCallMessage(opaqueMessageMetadata.getUuid(), callMessage);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():");
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;
}
public @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
try {
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
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) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.stopForegroundService();
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
WebRtcVideoUtil.deinitializeVideo(currentState);
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
}

View File

@@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
/**
* Process actions for when the call has at least once been connected and joined.
*/
public class GroupConnectedActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupConnectedActionProcessor.class);
public GroupConnectedActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(TAG, "handleSetEnableVideo():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
Camera camera = currentState.getVideoState().requireCamera();
try {
groupCall.setOutgoingVideoMuted(!enable);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable set video muted", e);
}
camera.setEnabled(enable);
currentState = currentState.builder()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor.getWebRtcCallService(), currentState.getCallSetupState().isEnableVideoOnCreate());
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
try {
currentState.getCallInfoState().requireGroupCall().setOutgoingAudioMuted(muted);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set audio muted", e);
}
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "handleLocalHangup():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
return terminateGroupCall(currentState);
}
}

View File

@@ -0,0 +1,149 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
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.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
/**
* Process actions to go from lobby to a joined call.
*/
public class GroupJoiningActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupJoiningActionProcessor.class);
private final CallSetupActionProcessorDelegate callSetupDelegate;
public GroupJoiningActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
callSetupDelegate = new CallSetupActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
GroupCall.LocalDeviceState device = groupCall.getLocalDeviceState();
Log.i(tag, "local device changed: " + device.getConnectionState() + " " + device.getJoinState());
WebRtcServiceStateBuilder builder = currentState.builder();
switch (device.getConnectionState()) {
case NOT_CONNECTED:
case RECONNECTING:
builder.changeCallInfoState()
.groupCallState(WebRtcUtil.groupCallStateForConnection(device.getConnectionState()))
.commit();
break;
case CONNECTING:
case CONNECTED:
if (device.getJoinState() == GroupCall.JoinState.JOINED) {
webRtcInteractor.startAudioCommunication(true);
webRtcInteractor.setWantsBluetoothConnection(true);
if (currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
} else {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, currentState.getCallInfoState().getCallRecipient());
try {
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
} catch (CallException e) {
Log.e(tag, e);
throw new RuntimeException(e);
}
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_CONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED)
.callConnectedTime(System.currentTimeMillis())
.commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor))
.build();
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
builder.changeCallInfoState()
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.commit();
} else {
builder.changeCallInfoState()
.groupCallState(WebRtcUtil.groupCallStateForConnection(device.getConnectionState()))
.commit();
}
break;
}
return builder.build();
}
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleLocalHangup():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
return terminateGroupCall(currentState);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
Camera camera = currentState.getVideoState().requireCamera();
try {
groupCall.setOutgoingVideoMuted(!enable);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set video muted", e);
}
camera.setEnabled(enable);
currentState = currentState.builder()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor.getWebRtcCallService(), currentState.getCallSetupState().isEnableVideoOnCreate());
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
try {
currentState.getCallInfoState().requireGroupCall().setOutgoingAudioMuted(muted);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set audio muted", e);
}
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
}

View File

@@ -0,0 +1,175 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.List;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_RINGING;
/**
* Process actions while the user is in the pre-join lobby for the call.
*/
public class GroupPreJoinActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupPreJoinActionProcessor.class);
public GroupPreJoinActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handlePreJoinCall():");
byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId();
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
currentState.getVideoState().requireEglBase(),
webRtcInteractor.getGroupCallObserver());
try {
groupCall.setOutgoingAudioMuted(true);
groupCall.setOutgoingVideoMuted(true);
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);
}
return currentState.builder()
.changeCallInfoState()
.groupCall(groupCall)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleCancelPreJoinCall(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "handleCancelPreJoinCall():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
WebRtcVideoUtil.deinitializeVideo(currentState);
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
@Override
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
GroupCall.LocalDeviceState device = groupCall.getLocalDeviceState();
Log.i(tag, "local device changed: " + device.getConnectionState() + " " + device.getJoinState());
return currentState.builder()
.changeCallInfoState()
.groupCallState(WebRtcUtil.groupCallStateForConnection(device.getConnectionState()))
.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
List<Recipient> callParticipants = Stream.of(groupCall.getJoinedGroupMembers())
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
.toList();
WebRtcServiceStateBuilder.CallInfoStateBuilder builder = currentState.builder()
.changeCallInfoState();
for (Recipient recipient : callParticipants) {
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), false));
}
return builder.build();
}
@Override
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull OfferMessage.Type offerType)
{
Log.i(TAG, "handleOutgoingCall():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
currentState = WebRtcVideoUtil.reinitializeCamera(context, webRtcInteractor.getCameraEventListener(), currentState);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.setWantsBluetoothConnection(true);
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, currentState.getCallInfoState().getCallRecipient());
try {
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
groupCall.setBandwidthMode(GroupCall.BandwidthMode.NORMAL);
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)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(TAG, "handleSetEnableVideo(): Changing for pre-join group call. enable: " + enable);
currentState.getVideoState().requireCamera().setEnabled(enable);
return currentState.builder()
.changeCallSetupState()
.enableVideoOnCreate(enable)
.commit()
.changeLocalDeviceState()
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
Log.i(TAG, "handleSetMuteAudio(): Changing for pre-join group call. muted: " + muted);
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
}

View File

@@ -12,8 +12,6 @@ import org.webrtc.CapturerObserver;
import org.webrtc.VideoFrame;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.Objects;
/**
* Action handler for when the system is at rest. Mainly responsible
* for starting pre-call state, starting an outgoing call, or receiving an
@@ -52,14 +50,21 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handlePreJoinCall():");
WebRtcServiceState newState = initializeVanityCamera(WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState));
boolean isGroupCall = remotePeer.getRecipient().isPushV2Group();
WebRtcActionProcessor processor = isGroupCall ? new GroupPreJoinActionProcessor(webRtcInteractor)
: new PreJoinActionProcessor(webRtcInteractor);
return newState.builder()
.actionProcessor(new PreJoinActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_PRE_JOIN)
.callRecipient(remotePeer.getRecipient())
.build();
currentState = initializeVanityCamera(WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState));
currentState = currentState.builder()
.actionProcessor(processor)
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_PRE_JOIN)
.callRecipient(remotePeer.getRecipient())
.build();
return isGroupCall ? currentState.getActionProcessor().handlePreJoinCall(currentState, remotePeer)
: currentState;
}
private @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) {

View File

@@ -80,7 +80,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn;
VideoState videoState = currentState.getVideoState();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
try {
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),

View File

@@ -107,7 +107,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
try {
VideoState videoState = currentState.getVideoState();
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context,

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.HttpData;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
@@ -57,6 +58,14 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_SIGNALING_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_TIMEOUT;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_FLIP_CAMERA;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REQUEST_UPDATE_MEMBERS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_HTTP_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_HTTP_SUCCESS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_IS_IN_CALL_QUERY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_LOCAL_HANGUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_LOCAL_RINGING;
@@ -71,6 +80,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIV
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVE_HANGUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVE_ICE_CANDIDATES;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVE_OFFER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVE_OPAQUE_MESSAGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_REMOTE_RINGING;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_REMOTE_VIDEO_ENABLE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SCREEN_OFF;
@@ -79,6 +89,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_B
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_HANGUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_ICE_CANDIDATES;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_OFFER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_OPAQUE_MESSAGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SETUP_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_AUDIO_BLUETOOTH;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_AUDIO_SPEAKER;
@@ -90,20 +101,22 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_TURN_S
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_WIRED_HEADSET_CHANGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ANSWER_WITH_VIDEO;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_BLUETOOTH;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_CAMERA_STATE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_IS_ALWAYS_TURN;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MUTE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_RESULT_RECEIVER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SPEAKER;
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 static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getAvailable;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getBroadcastFlag;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getCallId;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getCameraState;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getEnable;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorCallState;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorIdentityKey;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupMembershipToken;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getIceCandidates;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getIceServers;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getOfferMessageType;
@@ -189,7 +202,7 @@ public abstract class WebRtcActionProcessor {
case ACTION_SET_AUDIO_SPEAKER: return handleSetSpeakerAudio(currentState, intent.getBooleanExtra(EXTRA_SPEAKER, false));
case ACTION_SET_AUDIO_BLUETOOTH: return handleSetBluetoothAudio(currentState, intent.getBooleanExtra(EXTRA_BLUETOOTH, false));
case ACTION_BLUETOOTH_CHANGE: return handleBluetoothChange(currentState, getAvailable(intent));
case ACTION_CAMERA_SWITCH_COMPLETED: return handleCameraSwitchCompleted(currentState, intent.getParcelableExtra(EXTRA_CAMERA_STATE));
case ACTION_CAMERA_SWITCH_COMPLETED: return handleCameraSwitchCompleted(currentState, getCameraState(intent));
// End Call Actions
case ACTION_ENDED_REMOTE_HANGUP:
@@ -208,6 +221,20 @@ public abstract class WebRtcActionProcessor {
// Local Call Failure Actions
case ACTION_SETUP_FAILURE: return handleSetupFailure(currentState, getCallId(intent));
// Group Calling
case ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED: return handleGroupLocalDeviceStateChanged(currentState);
case ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED: return handleGroupRemoteDeviceStateChanged(currentState);
case ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED: return handleGroupJoinedMembershipChanged(currentState);
case ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF: return handleGroupRequestMembershipProof(currentState, getGroupMembershipToken(intent));
case ACTION_GROUP_REQUEST_UPDATE_MEMBERS: return handleGroupRequestUpdateMembers(currentState);
case ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS: return handleUpdateRenderedResolutions(currentState);
case ACTION_HTTP_SUCCESS: return handleHttpSuccess(currentState, HttpData.fromIntent(intent));
case ACTION_HTTP_FAILURE: return handleHttpFailure(currentState, HttpData.fromIntent(intent));
case ACTION_SEND_OPAQUE_MESSAGE: return handleSendOpaqueMessage(currentState, OpaqueMessageMetadata.fromIntent(intent));
case ACTION_RECEIVE_OPAQUE_MESSAGE: return handleReceivedOpaqueMessage(currentState, OpaqueMessageMetadata.fromIntent(intent));
}
return currentState;
@@ -275,8 +302,8 @@ public abstract class WebRtcActionProcessor {
//region Incoming call
protected @NonNull WebRtcServiceState handleReceivedOffer(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull WebRtcData.OfferMetadata offerMetadata,
@NonNull CallMetadata callMetadata,
@NonNull OfferMetadata offerMetadata,
@NonNull ReceivedOfferMetadata receivedOfferMetadata)
{
Log.i(tag, "handleReceivedOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
@@ -386,7 +413,7 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleSendBusy(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.CallMetadata callMetadata, boolean broadcast) {
protected @NonNull WebRtcServiceState handleSendBusy(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, boolean broadcast) {
Log.i(tag, "handleSendBusy(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
BusyMessage busyMessage = new BusyMessage(callMetadata.getCallId().longValue());
@@ -473,7 +500,7 @@ public abstract class WebRtcActionProcessor {
WebRtcServiceStateBuilder builder = currentState.builder();
if (errorCallState == WebRtcViewModel.State.UNTRUSTED_IDENTITY) {
CallParticipant participant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant participant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
CallParticipant untrusted = participant.withIdentityKey(identityKey.get());
builder.changeCallInfoState()
@@ -648,4 +675,68 @@ public abstract class WebRtcActionProcessor {
}
//endregion
//region Group Calling
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRemoteDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRemoteDeviceStateChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
Log.i(tag, "handleGroupRequestMembershipProof not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRequestUpdateMembers(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRequestUpdateMembers not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleUpdateRenderedResolutions not processed");
return currentState;
}
//endregion
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) {
try {
webRtcInteractor.getCallManager().receivedHttpResponse(httpData.getRequestId(), httpData.getStatus(), httpData.getBody() != null ? httpData.getBody() : new byte[0]);
} catch (CallException e) {
return callFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) {
try {
webRtcInteractor.getCallManager().httpRequestFailed(httpData.getRequestId());
} catch (CallException e) {
return callFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleSendOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleSendOpaqueMessage not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage not processed");
return currentState;
}
}

View File

@@ -11,11 +11,18 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.UUID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_DEVICE_ID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_IS_LEGACY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_TYPE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_REQUEST_ID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_RESPONSE_BODY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_RESPONSE_STATUS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MESSAGE_AGE_SECONDS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemoteDevice;
/**
* Collection of classes to ease parsing data from intents and passing said data
@@ -224,4 +231,77 @@ public class WebRtcData {
return deviceId;
}
}
/**
* Http response data.
*/
static class HttpData {
private final long requestId;
private final int status;
private final byte[] body;
static @NonNull HttpData fromIntent(@NonNull Intent intent) {
return new HttpData(intent.getLongExtra(EXTRA_HTTP_REQUEST_ID, -1),
intent.getIntExtra(EXTRA_HTTP_RESPONSE_STATUS, -1),
intent.getByteArrayExtra(EXTRA_HTTP_RESPONSE_BODY));
}
HttpData(long requestId, int status, @Nullable byte[] body) {
this.requestId = requestId;
this.status = status;
this.body = body;
}
long getRequestId() {
return requestId;
}
int getStatus() {
return status;
}
@Nullable byte[] getBody() {
return body;
}
}
/**
* An opaque calling message.
*/
static class OpaqueMessageMetadata {
private final UUID uuid;
private final byte[] opaque;
private final int remoteDeviceId;
private final long messageAgeSeconds;
static @NonNull OpaqueMessageMetadata fromIntent(@NonNull Intent intent) {
return new OpaqueMessageMetadata(WebRtcIntentParser.getUuid(intent),
WebRtcIntentParser.getOpaque(intent),
getRemoteDevice(intent),
intent.getLongExtra(EXTRA_MESSAGE_AGE_SECONDS, 0));
}
OpaqueMessageMetadata(@NonNull UUID uuid, @NonNull byte[] opaque, int remoteDeviceId, long messageAgeSeconds) {
this.uuid = uuid;
this.opaque = opaque;
this.remoteDeviceId = remoteDeviceId;
this.messageAgeSeconds = messageAgeSeconds;
}
@NonNull UUID getUuid() {
return uuid;
}
@NonNull byte[] getOpaque() {
return opaque;
}
int getRemoteDeviceId() {
return remoteDeviceId;
}
long getMessageAgeSeconds() {
return messageAgeSeconds;
}
}
}

View File

@@ -9,6 +9,7 @@ import org.signal.ringrtc.CallId;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.ringrtc.TurnServerInfoParcel;
@@ -18,29 +19,35 @@ import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ANSWER_OPAQUE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ANSWER_SDP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_AVAILABLE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_BROADCAST;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_CALL_ID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_CAMERA_STATE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ENABLE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_CALL_STATE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_IDENTITY_KEY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_EXTERNAL_TOKEN;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ICE_CANDIDATES;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MULTI_RING;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_OFFER_OPAQUE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_OFFER_SDP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_OFFER_TYPE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_OPAQUE_MESSAGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_REMOTE_DEVICE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_REMOTE_IDENTITY_KEY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_REMOTE_PEER_KEY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_TURN_SERVER_INFO;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_UUID;
/**
* Helper to parse the various attributes out of intents passed to the service.
@@ -111,6 +118,14 @@ public final class WebRtcIntentParser {
return intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE);
}
public static @NonNull byte[] getOpaque(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getByteArrayExtra(EXTRA_OPAQUE_MESSAGE));
}
public static @NonNull UUID getUuid(@NonNull Intent intent) {
return UuidUtil.parseOrThrow(intent.getStringExtra(EXTRA_UUID));
}
public static boolean getBroadcastFlag(@NonNull Intent intent) {
return intent.getBooleanExtra(EXTRA_BROADCAST, false);
}
@@ -149,10 +164,17 @@ public final class WebRtcIntentParser {
return intent.getBooleanExtra(EXTRA_ENABLE, false);
}
public static @NonNull byte[] getGroupMembershipToken(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getByteArrayExtra(EXTRA_GROUP_EXTERNAL_TOKEN));
}
public static @NonNull CameraState getCameraState(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getParcelableExtra(EXTRA_CAMERA_STATE));
}
public static @NonNull WebRtcViewModel.State getErrorCallState(@NonNull Intent intent) {
return (WebRtcViewModel.State) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_ERROR_CALL_STATE));
}
public static @NonNull Optional<IdentityKey> getErrorIdentityKey(@NonNull Intent intent) {
IdentityKeyParcelable identityKeyParcelable = (IdentityKeyParcelable) intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY);
if (identityKeyParcelable != null) {

View File

@@ -6,6 +6,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
@@ -16,6 +19,8 @@ import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.UUID;
/**
* Serves as the bridge between the action processing framework as the WebRTC service. Attempts
* to minimize direct access to various managers by providing a simple proxy to them. Due to the
@@ -23,19 +28,21 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
*/
public class WebRtcInteractor {
@NonNull private final WebRtcCallService webRtcCallService;
@NonNull private final CallManager callManager;
@NonNull private final LockManager lockManager;
@NonNull private final SignalAudioManager audioManager;
@NonNull private final BluetoothStateManager bluetoothStateManager;
@NonNull private final CameraEventListener cameraEventListener;
@NonNull private final WebRtcCallService webRtcCallService;
@NonNull private final CallManager callManager;
@NonNull private final LockManager lockManager;
@NonNull private final SignalAudioManager audioManager;
@NonNull private final BluetoothStateManager bluetoothStateManager;
@NonNull private final CameraEventListener cameraEventListener;
@NonNull private final GroupCall.Observer groupCallObserver;
public WebRtcInteractor(@NonNull WebRtcCallService webRtcCallService,
@NonNull CallManager callManager,
@NonNull LockManager lockManager,
@NonNull SignalAudioManager audioManager,
@NonNull BluetoothStateManager bluetoothStateManager,
@NonNull CameraEventListener cameraEventListener)
@NonNull CameraEventListener cameraEventListener,
@NonNull GroupCall.Observer groupCallObserver)
{
this.webRtcCallService = webRtcCallService;
this.callManager = callManager;
@@ -43,6 +50,7 @@ public class WebRtcInteractor {
this.audioManager = audioManager;
this.bluetoothStateManager = bluetoothStateManager;
this.cameraEventListener = cameraEventListener;
this.groupCallObserver = groupCallObserver;
}
@NonNull CameraEventListener getCameraEventListener() {
@@ -57,6 +65,10 @@ public class WebRtcInteractor {
return webRtcCallService;
}
@NonNull GroupCall.Observer getGroupCallObserver() {
return groupCallObserver;
}
void setWantsBluetoothConnection(boolean enabled) {
bluetoothStateManager.setWantsConnection(enabled);
}
@@ -73,8 +85,16 @@ public class WebRtcInteractor {
webRtcCallService.sendCallMessage(remotePeer, callMessage);
}
void sendOpaqueCallMessage(@NonNull UUID uuid, @NonNull SignalServiceCallMessage callMessage) {
webRtcCallService.sendOpaqueCallMessage(uuid, callMessage);
}
void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
webRtcCallService.setCallInProgressNotification(type, remotePeer);
webRtcCallService.setCallInProgressNotification(type, remotePeer.getRecipient());
}
void setCallInProgressNotification(int type, @NonNull Recipient recipient) {
webRtcCallService.setCallInProgressNotification(type, recipient);
}
void retrieveTurnServers(@NonNull RemotePeer remotePeer) {

View File

@@ -6,6 +6,8 @@ import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -56,4 +58,17 @@ public final class WebRtcUtil {
androidAudioManager.setSpeakerphoneOn(true);
}
}
public static @NonNull WebRtcViewModel.GroupCallState groupCallStateForConnection(@NonNull GroupCall.ConnectionState connectionState) {
switch (connectionState) {
case CONNECTING:
return WebRtcViewModel.GroupCallState.CONNECTING;
case CONNECTED:
return WebRtcViewModel.GroupCallState.CONNECTED;
case RECONNECTING:
return WebRtcViewModel.GroupCallState.RECONNECTING;
default:
return WebRtcViewModel.GroupCallState.DISCONNECTED;
}
}
}

View File

@@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
@@ -12,6 +14,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -20,27 +23,31 @@ import java.util.Objects;
*/
public class CallInfoState {
WebRtcViewModel.State callState;
Recipient callRecipient;
long callConnectedTime;
Map<Recipient, CallParticipant> remoteParticipants;
Map<Integer, RemotePeer> peerMap;
RemotePeer activePeer;
WebRtcViewModel.State callState;
Recipient callRecipient;
long callConnectedTime;
Map<CallParticipantId, CallParticipant> remoteParticipants;
Map<Integer, RemotePeer> peerMap;
RemotePeer activePeer;
GroupCall groupCall;
WebRtcViewModel.GroupCallState groupState;
public CallInfoState() {
this(WebRtcViewModel.State.IDLE, Recipient.UNKNOWN, -1, Collections.emptyMap(), Collections.emptyMap(), null);
this(WebRtcViewModel.State.IDLE, Recipient.UNKNOWN, -1, Collections.emptyMap(), Collections.emptyMap(), null, null, WebRtcViewModel.GroupCallState.IDLE);
}
public CallInfoState(@NonNull CallInfoState toCopy) {
this(toCopy.callState, toCopy.callRecipient, toCopy.callConnectedTime, toCopy.remoteParticipants, toCopy.peerMap, toCopy.activePeer);
this(toCopy.callState, toCopy.callRecipient, toCopy.callConnectedTime, toCopy.remoteParticipants, toCopy.peerMap, toCopy.activePeer, toCopy.groupCall, toCopy.groupState);
}
public CallInfoState(@NonNull WebRtcViewModel.State callState,
@NonNull Recipient callRecipient,
long callConnectedTime,
@NonNull Map<Recipient, CallParticipant> remoteParticipants,
@NonNull Map<CallParticipantId, CallParticipant> remoteParticipants,
@NonNull Map<Integer, RemotePeer> peerMap,
@Nullable RemotePeer activePeer)
@Nullable RemotePeer activePeer,
@Nullable GroupCall groupCall,
@NonNull WebRtcViewModel.GroupCallState groupState)
{
this.callState = callState;
this.callRecipient = callRecipient;
@@ -48,6 +55,8 @@ public class CallInfoState {
this.remoteParticipants = new LinkedHashMap<>(remoteParticipants);
this.peerMap = new HashMap<>(peerMap);
this.activePeer = activePeer;
this.groupCall = groupCall;
this.groupState = groupState;
}
public @NonNull Recipient getCallRecipient() {
@@ -58,11 +67,19 @@ public class CallInfoState {
return callConnectedTime;
}
public @Nullable CallParticipant getRemoteParticipant(@NonNull Recipient recipient) {
return remoteParticipants.get(recipient);
public @NonNull Map<CallParticipantId, CallParticipant> getRemoteCallParticipantsMap() {
return new LinkedHashMap<>(remoteParticipants);
}
public @NonNull ArrayList<CallParticipant> getRemoteCallParticipants() {
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull Recipient recipient) {
return getRemoteCallParticipant(new CallParticipantId(recipient));
}
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull CallParticipantId callParticipantId) {
return remoteParticipants.get(callParticipantId);
}
public @NonNull List<CallParticipant> getRemoteCallParticipants() {
return new ArrayList<>(remoteParticipants.values());
}
@@ -81,4 +98,16 @@ public class CallInfoState {
public @NonNull RemotePeer requireActivePeer() {
return Objects.requireNonNull(activePeer);
}
public @Nullable GroupCall getGroupCall() {
return groupCall;
}
public @NonNull GroupCall requireGroupCall() {
return Objects.requireNonNull(groupCall);
}
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
return groupState;
}
}

View File

@@ -3,8 +3,10 @@ package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.Camera;
@@ -192,8 +194,18 @@ public class WebRtcServiceStateBuilder {
return this;
}
public @NonNull CallInfoStateBuilder putParticipant(@NonNull CallParticipantId callParticipantId, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(callParticipantId, callParticipant);
return this;
}
public @NonNull CallInfoStateBuilder putParticipant(@NonNull Recipient recipient, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(recipient, callParticipant);
toBuild.remoteParticipants.put(new CallParticipantId(recipient), callParticipant);
return this;
}
public @NonNull CallInfoStateBuilder clearParticipantMap() {
toBuild.remoteParticipants.clear();
return this;
}
@@ -216,5 +228,15 @@ public class WebRtcServiceStateBuilder {
toBuild.activePeer = activePeer;
return this;
}
public @NonNull CallInfoStateBuilder groupCall(@Nullable GroupCall groupCall) {
toBuild.groupCall = groupCall;
return this;
}
public @NonNull CallInfoStateBuilder groupCallState(@Nullable WebRtcViewModel.GroupCallState groupState) {
toBuild.groupState = groupState;
return this;
}
}
}