Update ringrtc to v1.0.1

Add support for RingRTC Call Manager, a new component which provides
the control layer for all calling features.
This commit is contained in:
Curt Brune
2020-02-11 19:56:16 -08:00
committed by Greyson Parrelli
parent 81532cad95
commit 0970fd7040
16 changed files with 1789 additions and 1387 deletions

View File

@@ -1,400 +0,0 @@
package org.thoughtcrime.securesms.ringrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import org.signal.ringrtc.CallConnection;
import org.signal.ringrtc.CallConnectionFactory;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.SignalMessageRecipient;
import org.thoughtcrime.securesms.logging.Log;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Capturer;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoSink;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.PENDING;
public class CallConnectionWrapper {
private static final String TAG = Log.tag(CallConnectionWrapper.class);
@NonNull private final CallConnection callConnection;
@NonNull private final AudioTrack audioTrack;
@NonNull private final AudioSource audioSource;
@NonNull private final Camera camera;
@Nullable private final VideoSource videoSource;
@Nullable private final VideoTrack videoTrack;
public CallConnectionWrapper(@NonNull Context context,
@NonNull CallConnectionFactory factory,
@NonNull CallConnection.Observer observer,
@NonNull VideoSink localRenderer,
@NonNull CameraEventListener cameraEventListener,
@NonNull EglBase eglBase,
boolean hideIp,
long callId,
boolean outBound,
@NonNull SignalMessageRecipient recipient,
@NonNull SignalServiceAccountManager accountManager)
throws UnregisteredUserException, IOException, CallException
{
CallConnection.Configuration configuration = new CallConnection.Configuration(callId,
outBound,
recipient,
accountManager,
hideIp);
this.callConnection = factory.createCallConnection(configuration, observer);
this.callConnection.setAudioPlayout(false);
this.callConnection.setAudioRecording(false);
MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS");
MediaConstraints audioConstraints = new MediaConstraints();
audioConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
this.audioSource = factory.createAudioSource(audioConstraints);
this.audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource);
this.audioTrack.setEnabled(false);
mediaStream.addTrack(audioTrack);
this.camera = new Camera(context, cameraEventListener);
if (camera.capturer != null) {
this.videoSource = factory.createVideoSource(false);
this.videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);
camera.capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()), context, videoSource.getCapturerObserver());
this.videoTrack.addSink(localRenderer);
this.videoTrack.setEnabled(false);
mediaStream.addTrack(videoTrack);
} else {
this.videoSource = null;
this.videoTrack = null;
}
this.callConnection.addStream(mediaStream);
}
public boolean addIceCandidate(IceCandidate candidate) {
return callConnection.addIceCandidate(candidate);
}
public void sendOffer() throws CallException {
callConnection.sendOffer();
}
public boolean validateResponse(SignalMessageRecipient recipient, Long inCallId)
throws CallException
{
return callConnection.validateResponse(recipient, inCallId);
}
public void handleOfferAnswer(String sessionDescription) throws CallException {
callConnection.handleOfferAnswer(sessionDescription);
}
public void acceptOffer(String offer) throws CallException {
callConnection.acceptOffer(offer);
}
public void hangUp() throws CallException {
callConnection.hangUp();
}
public void answerCall() throws CallException {
callConnection.answerCall();
}
public void setVideoEnabled(boolean enabled) throws CallException {
if (videoTrack != null) {
videoTrack.setEnabled(enabled);
}
camera.setEnabled(enabled);
callConnection.sendVideoStatus(enabled);
}
public void flipCamera() {
camera.flip();
}
public CameraState getCameraState() {
return new CameraState(camera.getActiveDirection(), camera.getCount());
}
public void setCommunicationMode() {
callConnection.setAudioPlayout(true);
callConnection.setAudioRecording(true);
}
public void setAudioEnabled(boolean enabled) {
audioTrack.setEnabled(enabled);
}
public void dispose() {
camera.dispose();
if (videoSource != null) {
videoSource.dispose();
}
audioSource.dispose();
callConnection.dispose();
}
private static class Camera implements CameraVideoCapturer.CameraSwitchHandler {
@Nullable
private final CameraVideoCapturer capturer;
private final CameraEventListener cameraEventListener;
private final int cameraCount;
private CameraState.Direction activeDirection;
private boolean enabled;
Camera(@NonNull Context context, @NonNull CameraEventListener cameraEventListener)
{
this.cameraEventListener = cameraEventListener;
CameraEnumerator enumerator = getCameraEnumerator(context);
cameraCount = enumerator.getDeviceNames().length;
CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT);
if (capturerCandidate != null) {
activeDirection = FRONT;
} else {
capturerCandidate = createVideoCapturer(enumerator, BACK);
if (capturerCandidate != null) {
activeDirection = BACK;
} else {
activeDirection = NONE;
}
}
capturer = capturerCandidate;
}
void flip() {
if (capturer == null || cameraCount < 2) {
throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount +
" of them.");
}
activeDirection = PENDING;
capturer.switchCamera(this);
}
void setEnabled(boolean enabled) {
this.enabled = enabled;
if (capturer == null) {
return;
}
try {
if (enabled) {
capturer.startCapture(1280, 720, 30);
} else {
capturer.stopCapture();
}
} catch (InterruptedException e) {
Log.w(TAG, "Got interrupted while trying to stop video capture", e);
}
}
void dispose() {
if (capturer != null) {
capturer.dispose();
}
}
int getCount() {
return cameraCount;
}
@NonNull CameraState.Direction getActiveDirection() {
return enabled ? activeDirection : NONE;
}
@Nullable CameraVideoCapturer getCapturer() {
return capturer;
}
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator,
@NonNull CameraState.Direction direction)
{
String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if ((direction == FRONT && enumerator.isFrontFacing(deviceName)) ||
(direction == BACK && enumerator.isBackFacing(deviceName)))
{
return enumerator.createCapturer(deviceName, null);
}
}
return null;
}
private @NonNull CameraEnumerator getCameraEnumerator(@NonNull Context context) {
boolean camera2EnumeratorIsSupported = false;
try {
camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context);
} catch (final Throwable throwable) {
Log.w(TAG, "Camera2Enumator.isSupport() threw.", throwable);
}
Log.i(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported);
return camera2EnumeratorIsSupported ? new FilteredCamera2Enumerator(context)
: new Camera1Enumerator(true);
}
@Override
public void onCameraSwitchDone(boolean isFrontFacing) {
activeDirection = isFrontFacing ? FRONT : BACK;
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
@Override
public void onCameraSwitchError(String errorMessage) {
Log.e(TAG, "onCameraSwitchError: " + errorMessage);
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
}
public interface CameraEventListener {
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
}
@TargetApi(21)
private static class FilteredCamera2Enumerator extends Camera2Enumerator {
@NonNull private final Context context;
@Nullable private final CameraManager cameraManager;
@Nullable private String[] deviceNames;
FilteredCamera2Enumerator(@NonNull Context context) {
super(context);
this.context = context;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
this.deviceNames = null;
}
private boolean isMonochrome(String deviceName, CameraManager cameraManager) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (capabilities != null) {
for (int cap : capabilities) {
if (cap == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME) {
return true;
}
}
}
} catch (CameraAccessException e) {
return false;
}
return false;
}
private boolean isLensFacing(String deviceName, CameraManager cameraManager, Integer facing) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
return facing.equals(lensFacing);
} catch (CameraAccessException e) {
return false;
}
}
@Override
public @NonNull String[] getDeviceNames() {
if (deviceNames != null) {
return deviceNames;
}
try {
List<String> cameraList = new LinkedList<>();
if (cameraManager != null) {
// While skipping cameras that are monochrome, gather cameras
// until we have at most 1 front facing camera and 1 back
// facing camera.
List<String> devices = Stream.of(cameraManager.getCameraIdList())
.filterNot(id -> isMonochrome(id, cameraManager))
.toList();
String frontCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_FRONT))
.findFirst()
.orElse(null);
if (frontCamera != null) {
cameraList.add(frontCamera);
}
String backCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_BACK))
.findFirst()
.orElse(null);
if (backCamera != null) {
cameraList.add(backCamera);
}
}
this.deviceNames = cameraList.toArray(new String[0]);
} catch (CameraAccessException e) {
Log.e(TAG, "Camera access exception: " + e);
this.deviceNames = new String[] {};
}
return deviceNames;
}
@Override
public @NonNull CameraVideoCapturer createCapturer(@Nullable String deviceName,
@Nullable CameraVideoCapturer.CameraEventsHandler eventsHandler) {
return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context));
}
}
}

View File

@@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.ringrtc;
/**
*
* Enumeration of call state
*
*/
public enum CallState {
/** Idle, setting up objects */
IDLE,
/** Dialing. Outgoing call is signaling the remote peer */
DIALING,
/** Answering. Incoming call is responding to remote peer */
ANSWERING,
/** Remote ringing. Outgoing call, ICE negotiation is complete */
REMOTE_RINGING,
/** Local ringing. Incoming call, ICE negotiation is complete */
LOCAL_RINGING,
/** Connected. Incoming/Outgoing call, the call is connected */
CONNECTED,
/** Terminated. Incoming/Outgoing call, the call is terminated */
TERMINATED,
/** Busy. Outgoing call received a busy notification */
RECEIVED_BUSY;
}

View File

@@ -0,0 +1,280 @@
package org.thoughtcrime.securesms.ringrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import org.signal.ringrtc.CameraControl;
import org.thoughtcrime.securesms.logging.Log;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Capturer;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.CapturerObserver;
import org.webrtc.EglBase;
import org.webrtc.SurfaceTextureHelper;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.PENDING;
/**
* Encapsulate the camera functionality needed for video calling.
*/
public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHandler {
private static final String TAG = Log.tag(Camera.class);
@NonNull private final Context context;
@Nullable private final CameraVideoCapturer capturer;
@NonNull private final CameraEventListener cameraEventListener;
@NonNull private final EglBase eglBase;
private final int cameraCount;
@NonNull private CameraState.Direction activeDirection;
private boolean enabled;
public Camera(@NonNull Context context,
@NonNull CameraEventListener cameraEventListener,
@NonNull EglBase eglBase)
{
this.context = context;
this.cameraEventListener = cameraEventListener;
this.eglBase = eglBase;
CameraEnumerator enumerator = getCameraEnumerator(context);
cameraCount = enumerator.getDeviceNames().length;
CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT);
if (capturerCandidate != null) {
activeDirection = FRONT;
} else {
capturerCandidate = createVideoCapturer(enumerator, BACK);
if (capturerCandidate != null) {
activeDirection = BACK;
} else {
activeDirection = NONE;
}
}
capturer = capturerCandidate;
}
@Override
public void initCapturer(@NonNull CapturerObserver observer) {
if (capturer != null) {
capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()),
context,
observer);
}
}
@Override
public boolean hasCapturer() {
return capturer != null;
}
@Override
public void flip() {
if (capturer == null || cameraCount < 2) {
throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount + " of them.");
}
activeDirection = PENDING;
capturer.switchCamera(this);
}
@Override
public void setEnabled(boolean enabled) {
Log.i(TAG, "setEnabled(): " + enabled);
this.enabled = enabled;
if (capturer == null) {
return;
}
try {
if (enabled) {
Log.i(TAG, "setEnabled(): starting capture");
capturer.startCapture(1280, 720, 30);
} else {
Log.i(TAG, "setEnabled(): stopping capture");
capturer.stopCapture();
}
} catch (InterruptedException e) {
Log.w(TAG, "Got interrupted while trying to stop video capture", e);
}
}
public void dispose() {
if (capturer != null) {
capturer.dispose();
}
}
int getCount() {
return cameraCount;
}
@NonNull CameraState.Direction getActiveDirection() {
return enabled ? activeDirection : NONE;
}
@NonNull public CameraState getCameraState() {
return new CameraState(getActiveDirection(), getCount());
}
@Nullable CameraVideoCapturer getCapturer() {
return capturer;
}
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator,
@NonNull CameraState.Direction direction)
{
String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if ((direction == FRONT && enumerator.isFrontFacing(deviceName)) ||
(direction == BACK && enumerator.isBackFacing(deviceName)))
{
return enumerator.createCapturer(deviceName, null);
}
}
return null;
}
private @NonNull CameraEnumerator getCameraEnumerator(@NonNull Context context) {
boolean camera2EnumeratorIsSupported = false;
try {
camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context);
} catch (final Throwable throwable) {
Log.w(TAG, "Camera2Enumator.isSupport() threw.", throwable);
}
Log.i(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported);
return camera2EnumeratorIsSupported ? new FilteredCamera2Enumerator(context)
: new Camera1Enumerator(true);
}
@Override
public void onCameraSwitchDone(boolean isFrontFacing) {
activeDirection = isFrontFacing ? FRONT : BACK;
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
@Override
public void onCameraSwitchError(String errorMessage) {
Log.e(TAG, "onCameraSwitchError: " + errorMessage);
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
@TargetApi(21)
private static class FilteredCamera2Enumerator extends Camera2Enumerator {
private static final String TAG = Log.tag(Camera2Enumerator.class);
@NonNull private final Context context;
@Nullable private final CameraManager cameraManager;
@Nullable private String[] deviceNames;
FilteredCamera2Enumerator(@NonNull Context context) {
super(context);
this.context = context;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
this.deviceNames = null;
}
private static boolean isMonochrome(@NonNull String deviceName, @NonNull CameraManager cameraManager) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (capabilities != null) {
for (int capability : capabilities) {
if (capability == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME) {
return true;
}
}
}
} catch (CameraAccessException e) {
return false;
}
return false;
}
private static boolean isLensFacing(@NonNull String deviceName, @NonNull CameraManager cameraManager, @NonNull Integer facing) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
return facing.equals(lensFacing);
} catch (CameraAccessException e) {
return false;
}
}
@Override
public @NonNull String[] getDeviceNames() {
if (deviceNames != null) {
return deviceNames;
}
try {
List<String> cameraList = new LinkedList<>();
if (cameraManager != null) {
List<String> devices = Stream.of(cameraManager.getCameraIdList())
.filterNot(id -> isMonochrome(id, cameraManager))
.toList();
String frontCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_FRONT))
.findFirst()
.orElse(null);
if (frontCamera != null) {
cameraList.add(frontCamera);
}
String backCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_BACK))
.findFirst()
.orElse(null);
if (backCamera != null) {
cameraList.add(backCamera);
}
}
this.deviceNames = cameraList.toArray(new String[0]);
} catch (CameraAccessException e) {
Log.e(TAG, "Camera access exception: " + e);
this.deviceNames = new String[] {};
}
return deviceNames;
}
@Override
public @NonNull CameraVideoCapturer createCapturer(@Nullable String deviceName,
@Nullable CameraVideoCapturer.CameraEventsHandler eventsHandler)
{
return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context));
}
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.ringrtc;
import androidx.annotation.NonNull;
public interface CameraEventListener {
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
}

View File

@@ -26,6 +26,11 @@ public class CameraState {
return this.activeDirection != Direction.NONE;
}
@Override
public String toString() {
return "count: " + cameraCount + ", activeDirection: " + activeDirection;
}
public enum Direction {
FRONT, BACK, NONE, PENDING
}

View File

@@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallId;
import org.webrtc.IceCandidate;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
/**
* Utility class for passing ICE candidate objects via Intents.
*
* Also provides utility methods for converting to/from Signal ICE
* candidate messages.
*/
public class IceCandidateParcel implements Parcelable {
@NonNull private final IceCandidate iceCandidate;
public IceCandidateParcel(@NonNull IceCandidate iceCandidate) {
this.iceCandidate = iceCandidate;
}
public IceCandidateParcel(@NonNull IceUpdateMessage iceUpdateMessage) {
this.iceCandidate = new IceCandidate(iceUpdateMessage.getSdpMid(),
iceUpdateMessage.getSdpMLineIndex(),
iceUpdateMessage.getSdp());
}
private IceCandidateParcel(@NonNull Parcel in) {
this.iceCandidate = new IceCandidate(in.readString(),
in.readInt(),
in.readString());
}
public @NonNull IceCandidate getIceCandidate() {
return iceCandidate;
}
public @NonNull IceUpdateMessage getIceUpdateMessage(@NonNull CallId callId) {
return new IceUpdateMessage(callId.longValue(),
iceCandidate.sdpMid,
iceCandidate.sdpMLineIndex,
iceCandidate.sdp);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(iceCandidate.sdpMid);
dest.writeInt(iceCandidate.sdpMLineIndex);
dest.writeString(iceCandidate.sdp);
}
public static final Creator<IceCandidateParcel> CREATOR = new Creator<IceCandidateParcel>() {
@Override
public IceCandidateParcel createFromParcel(@NonNull Parcel in) {
return new IceCandidateParcel(in);
}
@Override
public IceCandidateParcel[] newArray(int size) {
return new IceCandidateParcel[size];
}
};
}

View File

@@ -1,107 +0,0 @@
package org.thoughtcrime.securesms.ringrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.List;
import org.signal.ringrtc.SignalMessageRecipient;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
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.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
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.SignalServiceAddress;
public final class MessageRecipient implements SignalMessageRecipient {
private static final String TAG = Log.tag(MessageRecipient.class);
@NonNull private final Recipient recipient;
@NonNull private final SignalServiceMessageSender messageSender;
public MessageRecipient(@NonNull SignalServiceMessageSender messageSender,
@NonNull Recipient recipient)
{
this.recipient = recipient;
this.messageSender = messageSender;
}
public @NonNull RecipientId getId() {
return recipient.getId();
}
@Override
public boolean isEqual(@NonNull SignalMessageRecipient inRecipient) {
if (!(inRecipient instanceof MessageRecipient)) {
return false;
}
if (getClass() != inRecipient.getClass()) {
Log.e(TAG, "CLASSES NOT EQUAL: " + getClass().toString() + ", " + recipient.getClass().toString());
return false;
}
MessageRecipient that = (MessageRecipient) inRecipient;
return recipient.equals(that.recipient);
}
private void sendMessage(Context context, SignalServiceCallMessage callMessage)
throws UntrustedIdentityException, IOException
{
messageSender.sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
UnidentifiedAccessUtil.getAccessFor(context, recipient),
callMessage);
}
@Override
public void sendOfferMessage(Context context, long callId, String description)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendOfferMessage(): callId: 0x" + Long.toHexString(callId));
OfferMessage offerMessage = new OfferMessage(callId, description);
sendMessage(context, SignalServiceCallMessage.forOffer(offerMessage));
}
@Override
public void sendAnswerMessage(Context context, long callId, String description)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendAnswerMessage(): callId: 0x" + Long.toHexString(callId));
AnswerMessage answerMessage = new AnswerMessage(callId, description);
sendMessage(context, SignalServiceCallMessage.forAnswer(answerMessage));
}
@Override
public void sendIceUpdates(Context context, List<IceUpdateMessage> iceUpdateMessages)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendIceUpdates(): iceUpdates: " + iceUpdateMessages.size());
sendMessage(context, SignalServiceCallMessage.forIceUpdates(iceUpdateMessages));
}
@Override
public void sendHangupMessage(Context context, long callId)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendHangupMessage(): callId: 0x" + Long.toHexString(callId));
HangupMessage hangupMessage = new HangupMessage(callId);
sendMessage(context, SignalServiceCallMessage.forHangup(hangupMessage));
}
}

View File

@@ -0,0 +1,146 @@
package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.Remote;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
/**
* Container class that represents the remote peer and current state
* of a video/voice call.
*
* The class is also Parcelable for passing around via an Intent.
*/
public final class RemotePeer implements Remote, Parcelable
{
@NonNull private final RecipientId recipientId;
@NonNull private CallState callState;
@NonNull private CallId callId;
public RemotePeer(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
this.callState = CallState.IDLE;
this.callId = new CallId(-1L);
}
private RemotePeer(@NonNull Parcel in) {
this.recipientId = RecipientId.CREATOR.createFromParcel(in);
this.callState = CallState.values()[in.readInt()];
this.callId = new CallId(in.readLong());
}
public @NonNull CallId getCallId() {
return callId;
}
public @NonNull CallState getState() {
return callState;
}
public @NonNull RecipientId getId() {
return recipientId;
}
public @NonNull Recipient getRecipient() {
return Recipient.resolved(recipientId);
}
@Override
public String toString() {
return "recipientId: " + this.recipientId +
", callId: " + this.callId +
", state: " + this.callState;
}
@Override
public boolean recipientEquals(Remote obj) {
if (obj != null && this.getClass() == obj.getClass()) {
RemotePeer that = (RemotePeer)obj;
return this.recipientId.equals(that.recipientId);
}
return false;
}
public boolean callIdEquals(RemotePeer remotePeer) {
return remotePeer != null && this.callId.equals(remotePeer.callId);
}
public void dialing(@NonNull CallId callId) {
if (callState != CallState.IDLE) {
throw new IllegalStateException("Cannot transition to DIALING from state: " + callState);
}
this.callId = callId;
this.callState = CallState.DIALING;
}
public void answering(@NonNull CallId callId) {
if (callState != CallState.IDLE) {
throw new IllegalStateException("Cannot transition to ANSWERING from state: " + callState);
}
this.callId = callId;
this.callState = CallState.ANSWERING;
}
public void remoteRinging() {
if (callState != CallState.DIALING) {
throw new IllegalStateException("Cannot transition to REMOTE_RINGING from state: " + callState);
}
this.callState = CallState.REMOTE_RINGING;
}
public void receivedBusy() {
if (callState != CallState.DIALING) {
throw new IllegalStateException("Cannot transition to RECEIVED_BUSY from state: " + callState);
}
this.callState = CallState.RECEIVED_BUSY;
}
public void localRinging() {
if (callState != CallState.ANSWERING) {
throw new IllegalStateException("Cannot transition to LOCAL_RINGING from state: " + callState);
}
this.callState = CallState.LOCAL_RINGING;
}
public void connected() {
if (callState != CallState.REMOTE_RINGING && callState != CallState.LOCAL_RINGING) {
throw new IllegalStateException("Cannot transition outgoing call to CONNECTED from state: " + callState);
}
this.callState = CallState.CONNECTED;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
recipientId.writeToParcel(dest, flags);
dest.writeInt(callState.ordinal());
dest.writeLong(callId.longValue());
}
public static final Creator<RemotePeer> CREATOR = new Creator<RemotePeer>() {
@Override
public RemotePeer createFromParcel(@NonNull Parcel in) {
return new RemotePeer(in);
}
@Override
public RemotePeer[] newArray(int size) {
return new RemotePeer[size];
}
};
}