Call handling state machine refactor.

This commit is contained in:
Cody Henthorne 2020-10-16 15:42:04 -04:00 committed by Alan Evans
parent b3f0a44f10
commit 5eaac6cb17
31 changed files with 3446 additions and 1391 deletions

View File

@ -198,7 +198,7 @@ public final class CallParticipantsState {
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}
} else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
localRenderState = WebRtcLocalRenderState.LARGE;
}
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {

View File

@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition;
import androidx.transition.ChangeBounds;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
@ -553,6 +552,7 @@ public class WebRtcCallView extends FrameLayout {
Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.endTransitions(parent);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@ -570,6 +570,7 @@ public class WebRtcCallView extends FrameLayout {
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.endTransitions(parent);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();

View File

@ -116,4 +116,16 @@ public class CallParticipant {
public int hashCode() {
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
}
@Override
public @NonNull String toString() {
return "CallParticipant{" +
"cameraState=" + cameraState +
", recipient=" + recipient.getId() +
", identityKey=" + (identityKey == null ? "absent" : "present") +
", videoSink=" + (videoSink.getEglBase() == null ? "not initialized" : "initialized") +
", videoEnabled=" + videoEnabled +
", microphoneEnabled=" + microphoneEnabled +
'}';
}
}

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
@ -13,6 +14,8 @@ import java.util.List;
public class WebRtcViewModel {
public enum State {
IDLE,
// Normal states
CALL_PRE_JOIN,
CALL_INCOMING,
@ -48,7 +51,7 @@ public class WebRtcViewModel {
public WebRtcViewModel(@NonNull State state,
@NonNull Recipient recipient,
@NonNull CameraState localCameraState,
@NonNull BroadcastVideoSink localSink,
@Nullable BroadcastVideoSink localSink,
boolean isBluetoothAvailable,
boolean isMicrophoneEnabled,
boolean isRemoteVideoOffer,
@ -62,7 +65,7 @@ public class WebRtcViewModel {
this.callConnectedTime = callConnectedTime;
this.remoteParticipants = remoteParticipants;
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
localParticipant = CallParticipant.createLocal(localCameraState, localSink != null ? localSink : new BroadcastVideoSink(null), isMicrophoneEnabled);
}
public @NonNull State getState() {
@ -97,4 +100,15 @@ public class WebRtcViewModel {
return remoteParticipants;
}
@Override public @NonNull String toString() {
return "WebRtcViewModel{" +
"state=" + state +
", recipient=" + recipient.getId() +
", isBluetoothAvailable=" + isBluetoothAvailable +
", isRemoteVideoOffer=" + isRemoteVideoOffer +
", callConnectedTime=" + callConnectedTime +
", localParticipant=" + localParticipant +
", remoteParticipants=" + remoteParticipants +
'}';
}
}

View File

@ -45,6 +45,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
private final int cameraCount;
@NonNull private CameraState.Direction activeDirection;
private boolean enabled;
private boolean isInitialized;
public Camera(@NonNull Context context,
@NonNull CameraEventListener cameraEventListener,
@ -80,6 +81,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()),
context,
observer);
isInitialized = true;
}
}
@ -123,6 +125,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
public void dispose() {
if (capturer != null) {
capturer.dispose();
isInitialized = false;
}
}
@ -142,6 +145,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
return capturer;
}
public boolean isInitialized() {
return isInitialized;
}
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator,
@NonNull CameraState.Direction direction)
{

View File

@ -1,8 +1,11 @@
package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class CameraState {
public class CameraState implements Parcelable {
public static final CameraState UNKNOWN = new CameraState(Direction.NONE, 0);
@ -14,6 +17,10 @@ public class CameraState {
this.cameraCount = cameraCount;
}
private CameraState(Parcel in) {
this(Direction.valueOf(in.readString()), in.readInt());
}
public int getCameraCount() {
return cameraCount;
}
@ -31,6 +38,17 @@ public class CameraState {
return "count: " + cameraCount + ", activeDirection: " + activeDirection;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(activeDirection.name());
dest.writeInt(cameraCount);
}
@Override
public int describeContents() {
return 0;
}
public enum Direction {
FRONT, BACK, NONE, PENDING;
@ -49,4 +67,16 @@ public class CameraState {
}
}
}
public static final Creator<CameraState> CREATOR = new Creator<CameraState>() {
@Override
public CameraState createFromParcel(Parcel in) {
return new CameraState(in);
}
@Override
public CameraState[] newArray(int size) {
return new CameraState[size];
}
};
}

View File

@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.Remote;
@ -84,7 +86,7 @@ public final class RemotePeer implements Remote, Parcelable
return false;
}
public boolean callIdEquals(RemotePeer remotePeer) {
public boolean callIdEquals(@Nullable RemotePeer remotePeer) {
return remotePeer != null && this.callId.equals(remotePeer.callId);
}

View File

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import java.util.ArrayList;
import java.util.List;
/**
* Wrap turn server info so it can be sent via an intent.
*/
public class TurnServerInfoParcel implements Parcelable {
private final String username;
private final String password;
private final List<String> urls;
public TurnServerInfoParcel(@NonNull TurnServerInfo turnServerInfo) {
urls = new ArrayList<>(turnServerInfo.getUrls());
username = turnServerInfo.getUsername();
password = turnServerInfo.getPassword();
}
private TurnServerInfoParcel(@NonNull Parcel in) {
username = in.readString();
password = in.readString();
urls = in.createStringArrayList();
}
public @Nullable String getUsername() {
return username;
}
public @Nullable String getPassword() {
return password;
}
public @NonNull List<String> getUrls() {
return urls;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(username);
dest.writeString(password);
dest.writeStringList(urls);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<TurnServerInfoParcel> CREATOR = new Creator<TurnServerInfoParcel>() {
@Override
public TurnServerInfoParcel createFromParcel(Parcel in) {
return new TurnServerInfoParcel(in);
}
@Override
public TurnServerInfoParcel[] newArray(int size) {
return new TurnServerInfoParcel[size];
}
};
}

View File

@ -0,0 +1,249 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.IceCandidate;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_BUSY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_GLARE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_ACCEPTED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_BUSY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_DECLINED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_NEED_PERMISSION;
import static org.thoughtcrime.securesms.service.WebRtcCallService.BUSY_TONE_LENGTH;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_CONNECTING;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_RINGING;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_RINGING;
/**
* Encapsulates the shared logic to manage an active 1:1 call. An active call is any call that is being setup
* or ongoing. Other action processors delegate the appropriate action to it but it is not intended
* to be the main processor for the system.
*/
public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
private static final Map<String, WebRtcViewModel.State> ENDED_ACTION_TO_STATE = new HashMap<String, WebRtcViewModel.State>() {{
put(ACTION_ENDED_REMOTE_HANGUP_ACCEPTED, WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE);
put(ACTION_ENDED_REMOTE_HANGUP_BUSY, WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE);
put(ACTION_ENDED_REMOTE_HANGUP_DECLINED, WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE);
put(ACTION_ENDED_REMOTE_BUSY, WebRtcViewModel.State.CALL_BUSY);
put(ACTION_ENDED_REMOTE_HANGUP_NEED_PERMISSION, WebRtcViewModel.State.CALL_NEEDS_PERMISSION);
put(ACTION_ENDED_REMOTE_GLARE, WebRtcViewModel.State.CALL_DISCONNECTED);
}};
public ActiveCallActionProcessorDelegate(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
super(webRtcInteractor, tag);
}
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
if (resultReceiver != null) {
resultReceiver.send(1, null);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
boolean broadcast,
@NonNull ArrayList<IceCandidateParcel> iceCandidates)
{
Log.i(tag, "handleSendIceCandidates(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
LinkedList<IceUpdateMessage> iceUpdateMessages = new LinkedList<>();
for (IceCandidateParcel parcel : iceCandidates) {
iceUpdateMessages.add(parcel.getIceUpdateMessage(callMetadata.getCallId()));
}
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdateMessages, true, destinationDeviceId);
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull ArrayList<IceCandidateParcel> iceCandidateParcels)
{
Log.i(tag, "handleReceivedIceCandidates(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()) + ", count: " + iceCandidateParcels.size());
LinkedList<IceCandidate> iceCandidates = new LinkedList<>();
for (IceCandidateParcel parcel : iceCandidateParcels) {
iceCandidates.add(parcel.getIceCandidate());
}
try {
webRtcInteractor.getCallManager().receivedIceCandidates(callMetadata.getCallId(), callMetadata.getRemoteDevice(), iceCandidates);
} catch (CallException e) {
return callFailure(currentState, "receivedIceCandidates() failed: ", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
Log.i(tag, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId());
CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant newParticipant = oldParticipant.withVideoEnabled(enable);
return currentState.builder()
.changeCallInfoState()
.putParticipant(activePeer.getRecipient(), newParticipant)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleLocalHangup(): call_id: " + currentState.getCallInfoState().requireActivePeer().getCallId());
ApplicationDependencies.getSignalServiceAccountManager().cancelInFlightRequests();
ApplicationDependencies.getSignalServiceMessageSender().cancelInFlightRequests();
try {
webRtcInteractor.getCallManager().hangup();
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
return terminate(currentState, currentState.getCallInfoState().getActivePeer());
} catch (CallException e) {
return callFailure(currentState, "hangup() failed: ", e);
}
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleCallConcluded():");
Log.i(tag, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
return currentState.builder()
.changeCallInfoState()
.removeRemotePeer(remotePeer)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
Log.i(tag, "handleReceivedOfferWhileActive(): call_id: " + remotePeer.getCallId());
switch (activePeer.getState()) {
case DIALING:
case REMOTE_RINGING: webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, activePeer); break;
case IDLE:
case ANSWERING: webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer); break;
case LOCAL_RINGING: webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, activePeer); break;
case CONNECTED: webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, activePeer); break;
default: throw new IllegalStateException();
}
if (activePeer.getState() == CallState.IDLE) {
webRtcInteractor.stopForegroundService();
}
webRtcInteractor.insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp());
return terminate(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEndedRemote(@NonNull WebRtcServiceState currentState,
@NonNull String action,
@NonNull RemotePeer remotePeer)
{
Log.i(tag, "handleEndedRemote(): call_id: " + remotePeer.getCallId() + " action: " + action);
WebRtcViewModel.State state = currentState.getCallInfoState().getCallState();
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
boolean remotePeerIsActive = remotePeer.callIdEquals(activePeer);
boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING;
boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING;
if (remotePeerIsActive && ENDED_ACTION_TO_STATE.containsKey(action)) {
state = Objects.requireNonNull(ENDED_ACTION_TO_STATE.get(action));
}
if (action.equals(ACTION_ENDED_REMOTE_HANGUP)) {
if (remotePeerIsActive) {
state = outgoingBeforeAccept ? WebRtcViewModel.State.RECIPIENT_UNAVAILABLE : WebRtcViewModel.State.CALL_DISCONNECTED;
}
if (incomingBeforeAccept) {
webRtcInteractor.insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp());
}
} else if (action.equals(ACTION_ENDED_REMOTE_BUSY) && remotePeerIsActive) {
activePeer.receivedBusy();
OutgoingRinger ringer = new OutgoingRinger(context);
ringer.start(OutgoingRinger.Type.BUSY);
Util.runOnMainDelayed(ringer::stop, BUSY_TONE_LENGTH);
} else if (action.equals(ACTION_ENDED_REMOTE_GLARE) && incomingBeforeAccept) {
webRtcInteractor.insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp());
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(state)
.build();
webRtcInteractor.sendMessage(currentState);
return terminate(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEnded(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleEnded(): call_id: " + remotePeer.getCallId() + " action: " + action);
if (remotePeer.callIdEquals(currentState.getCallInfoState().getActivePeer())) {
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.NETWORK_FAILURE)
.build();
webRtcInteractor.sendMessage(currentState);
}
if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) {
webRtcInteractor.insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp());
}
return terminate(currentState, remotePeer);
}
}

View File

@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
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.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_CONNECTING;
/**
* Encapsulates the logic to begin a 1:1 call from scratch. Other action processors
* delegate the appropriate action to it but it is not intended to be the main processor for the system.
*/
public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
public BeginCallActionProcessorDelegate(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
super(webRtcInteractor, tag);
}
@Override
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull OfferMessage.Type offerType)
{
remotePeer.setCallStartTimestamp(System.currentTimeMillis());
currentState = currentState.builder()
.actionProcessor(new OutgoingCallActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callRecipient(remotePeer.getRecipient())
.callState(WebRtcViewModel.State.CALL_OUTGOING)
.putRemotePeer(remotePeer)
.putParticipant(remotePeer.getRecipient(),
CallParticipant.createRemote(
remotePeer.getRecipient(),
null,
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
false
))
.build();
CallManager.CallMediaType callMediaType = WebRtcUtil.getCallMediaTypeFromOfferType(offerType);
try {
webRtcInteractor.getCallManager().call(remotePeer, callMediaType, 1);
} catch (CallException e) {
return callFailure(currentState, "Unable to create outgoing call: ", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
remotePeer.answering();
Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, remotePeer);
webRtcInteractor.retrieveTurnServers(remotePeer);
return currentState.builder()
.actionProcessor(new IncomingCallActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callRecipient(remotePeer.getRecipient())
.activePeer(remotePeer)
.callState(WebRtcViewModel.State.CALL_INCOMING)
.putParticipant(remotePeer.getRecipient(),
CallParticipant.createRemote(
remotePeer.getRecipient(),
null,
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
false
))
.build();
}
}

View File

@ -0,0 +1,96 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
/**
* Encapsulates the shared logic to setup a 1:1 call. Setup primarily includes retrieving turn servers and
* transitioning to the connected state. Other action processors delegate the appropriate action to it but it is
* not intended to be the main processor for the system.
*/
public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
public CallSetupActionProcessorDelegate(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
super(webRtcInteractor, tag);
}
@Override
public @NonNull WebRtcServiceState handleCallConnected(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
if (!remotePeer.callIdEquals(currentState.getCallInfoState().getActivePeer())) {
Log.w(tag, "handleCallConnected(): Ignoring for inactive call.");
return currentState;
}
Log.i(tag, "handleCallConnected(): call_id: " + remotePeer.getCallId());
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
webRtcInteractor.startAudioCommunication(activePeer.getState() == CallState.REMOTE_RINGING);
webRtcInteractor.setWantsBluetoothConnection(true);
activePeer.connected();
if (currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
} else {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
currentState = currentState.builder()
.actionProcessor(new ConnectedCallActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_CONNECTED)
.callConnectedTime(System.currentTimeMillis())
.build();
webRtcInteractor.unregisterPowerButtonReceiver();
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, activePeer);
try {
CallManager callManager = webRtcInteractor.getCallManager();
callManager.setCommunicationMode();
callManager.setAudioEnable(currentState.getLocalDeviceState().isMicrophoneEnabled());
callManager.setVideoEnable(currentState.getLocalDeviceState().getCameraState().isEnabled());
} catch (CallException e) {
return callFailure(currentState, "Enabling audio/video failed: ", e);
}
if (currentState.getCallSetupState().isAcceptWithVideo()) {
currentState = currentState.getActionProcessor().handleSetEnableVideo(currentState, true);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Camera camera = currentState.getVideoState().requireCamera();
if (camera.isInitialized()) {
camera.setEnabled(enable);
}
currentState = currentState.builder()
.changeCallSetupState()
.enableVideoOnCreate(enable)
.commit()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getCallSetupState().isEnableVideoOnCreate());
return currentState;
}
}

View File

@ -0,0 +1,127 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import java.util.ArrayList;
import java.util.Objects;
/**
* Handles action for a connected/ongoing call. At this point it's mostly responding
* to user actions (local and remote) on video/mic and adjusting accordingly.
*/
public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
private static final String TAG = Log.tag(ConnectedCallActionProcessor.class);
private final ActiveCallActionProcessorDelegate activeCallDelegate;
public ConnectedCallActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
activeCallDelegate = new ActiveCallActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
return activeCallDelegate.handleIsInCallQuery(currentState, resultReceiver);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(TAG, "handleSetEnableVideo(): call_id: " + currentState.getCallInfoState().requireActivePeer().getCallId());
try {
webRtcInteractor.getCallManager().setVideoEnable(enable);
} catch (CallException e) {
return callFailure(currentState, "setVideoEnable() failed: ", e);
}
currentState = currentState.builder()
.changeLocalDeviceState()
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
.build();
if (currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
} else {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getLocalDeviceState().getCameraState().isEnabled());
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
currentState = currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
try {
webRtcInteractor.getCallManager().setAudioEnable(currentState.getLocalDeviceState().isMicrophoneEnabled());
} catch (CallException e) {
return callFailure(currentState, "Enabling audio failed: ", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
}
@Override
protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
boolean broadcast,
@NonNull ArrayList<IceCandidateParcel> iceCandidates)
{
return activeCallDelegate.handleSendIceCandidates(currentState, callMetadata, broadcast, iceCandidates);
}
@Override
protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull ArrayList<IceCandidateParcel> iceCandidateParcels)
{
return activeCallDelegate.handleReceivedIceCandidates(currentState, callMetadata, iceCandidateParcels);
}
@Override
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
return activeCallDelegate.handleLocalHangup(currentState);
}
@Override
protected @NonNull WebRtcServiceState handleEndedRemote(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleEndedRemote(currentState, action, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEnded(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleEnded(currentState, action, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleReceivedOfferWhileActive(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleCallConcluded(currentState, remotePeer);
}
}

View File

@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.ServiceUtil;
/**
* Encapsulates the shared logic to deal with local device actions. Other action processors inherit
* the behavior by extending it instead of delegating. It is not intended to be the main processor
* for the system.
*/
public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
public DeviceAwareActionProcessor(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
super(webRtcInteractor, tag);
}
@Override
protected @NonNull WebRtcServiceState handleWiredHeadsetChange(@NonNull WebRtcServiceState currentState, boolean present) {
Log.i(tag, "handleWiredHeadsetChange():");
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
if (present && androidAudioManager.isSpeakerphoneOn()) {
androidAudioManager.setSpeakerphoneOn(false);
androidAudioManager.setBluetoothScoOn(false);
} else if (!present && !androidAudioManager.isSpeakerphoneOn() && !androidAudioManager.isBluetoothScoOn() && currentState.getLocalDeviceState().getCameraState().isEnabled()) {
androidAudioManager.setSpeakerphoneOn(true);
}
webRtcInteractor.sendMessage(currentState);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleBluetoothChange(@NonNull WebRtcServiceState currentState, boolean available) {
Log.i(tag, "handleBluetoothChange(): " + available);
return currentState.builder()
.changeLocalDeviceState()
.isBluetoothAvailable(available)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetSpeakerAudio(@NonNull WebRtcServiceState currentState, boolean isSpeaker) {
Log.i(tag, "handleSetSpeakerAudio(): " + isSpeaker);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
webRtcInteractor.setWantsBluetoothConnection(false);
androidAudioManager.setSpeakerphoneOn(isSpeaker);
if (!currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.sendMessage(currentState);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetBluetoothAudio(@NonNull WebRtcServiceState currentState, boolean isBluetooth) {
Log.i(tag, "handleSetBluetoothAudio(): " + isBluetooth);
webRtcInteractor.setWantsBluetoothConnection(isBluetooth);
if (!currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.sendMessage(currentState);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetCameraFlip(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleSetCameraFlip():");
if (currentState.getLocalDeviceState().getCameraState().isEnabled() && currentState.getVideoState().getCamera() != null) {
currentState.getVideoState().getCamera().flip();
return currentState.builder()
.changeLocalDeviceState()
.cameraState(currentState.getVideoState().getCamera().getCameraState())
.build();
}
return currentState;
}
@Override
public @NonNull WebRtcServiceState handleCameraSwitchCompleted(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) {
Log.i(tag, "handleCameraSwitchCompleted():");
return currentState.builder()
.changeLocalDeviceState()
.cameraState(newCameraState)
.build();
}
}

View File

@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
/**
* Handles disconnecting state actions. This primairly entails dealing with final
* clean up in the call concluded action, but also allows for transitioning into idle/setup
* via beginning an outgoing or incoming call.
*/
public class DisconnectingCallActionProcessor extends WebRtcActionProcessor {
private static final String TAG = Log.tag(DisconnectingCallActionProcessor.class);
public DisconnectingCallActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleStartIncomingCall():");
currentState = currentState.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor))
.build();
return currentState.getActionProcessor().handleStartIncomingCall(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
Log.i(TAG, "handleOutgoingCall():");
currentState = currentState.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor))
.build();
return currentState.getActionProcessor().handleOutgoingCall(currentState, remotePeer, offerType);
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleCallConcluded():");
Log.i(TAG, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
return currentState.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor))
.changeCallInfoState()
.removeRemotePeer(remotePeer)
.build();
}
}

View File

@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.webrtc.CapturerObserver;
import org.webrtc.VideoFrame;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.Objects;
/**
* Action handler for when the system is at rest. Mainly responsible
* for starting pre-call state, starting an outgoing call, or receiving an
* incoming call.
*/
public class IdleActionProcessor extends WebRtcActionProcessor {
private static final String TAG = Log.tag(IdleActionProcessor.class);
private final BeginCallActionProcessorDelegate beginCallDelegate;
public IdleActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
beginCallDelegate = new BeginCallActionProcessorDelegate(webRtcInteractor, TAG);
}
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleStartIncomingCall():");
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState);
return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull OfferMessage.Type offerType)
{
Log.i(TAG, "handleOutgoingCall():");
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState);
return beginCallDelegate.handleOutgoingCall(currentState, remotePeer, offerType);
}
@Override
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handlePreJoinCall():");
WebRtcServiceState newState = initializeVanityCamera(WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState));
return newState.builder()
.actionProcessor(new PreJoinActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_PRE_JOIN)
.callRecipient(remotePeer.getRecipient())
.build();
}
private @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) {
Camera camera = currentState.getVideoState().requireCamera();
BroadcastVideoSink sink = currentState.getVideoState().requireLocalSink();
if (camera.hasCapturer()) {
camera.initCapturer(new CapturerObserver() {
@Override
public void onFrameCaptured(VideoFrame videoFrame) {
sink.onFrame(videoFrame);
}
@Override
public void onCapturerStarted(boolean success) {}
@Override
public void onCapturerStopped() {}
});
camera.setEnabled(true);
}
return currentState.builder()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.build();
}
}

View File

@ -0,0 +1,238 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.net.Uri;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_RINGING;
/**
* Responsible for setting up and managing the start of an incoming 1:1 call. Transitioned
* to from idle or pre-join and can either move to a connected state (user picks up) or
* a disconnected state (remote hangup, local hangup, etc.).
*/
public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
private static final String TAG = Log.tag(IncomingCallActionProcessor.class);
private final ActiveCallActionProcessorDelegate activeCallDelegate;
private final CallSetupActionProcessorDelegate callSetupDelegate;
public IncomingCallActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
activeCallDelegate = new ActiveCallActionProcessorDelegate(webRtcInteractor, TAG);
callSetupDelegate = new CallSetupActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
return activeCallDelegate.handleIsInCallQuery(currentState, resultReceiver);
}
@Override
protected @NonNull WebRtcServiceState handleSendAnswer(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull WebRtcData.AnswerMetadata answerMetadata,
boolean broadcast)
{
Log.i(TAG, "handleSendAnswer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
AnswerMessage answerMessage = new AnswerMessage(callMetadata.getCallId().longValue(), answerMetadata.getSdp(), answerMetadata.getOpaque());
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forAnswer(answerMessage, true, destinationDeviceId);
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
return currentState;
}
@Override
public @NonNull WebRtcServiceState handleTurnServerUpdate(@NonNull WebRtcServiceState currentState,
@NonNull List<PeerConnection.IceServer> iceServers,
boolean isAlwaysTurn)
{
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn;
VideoState videoState = currentState.getVideoState();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
try {
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context,
videoState.requireEglBase(),
videoState.requireLocalSink(),
callParticipant.getVideoSink(),
videoState.requireCamera(),
iceServers,
hideIp,
false);
} catch (CallException e) {
return callFailure(currentState, "Unable to proceed with call: ", e);
}
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.sendMessage(currentState);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId());
DatabaseFactory.getSmsDatabase(context).insertReceivedCall(activePeer.getId());
currentState = currentState.builder()
.changeCallSetupState()
.acceptWithVideo(answerWithVideo)
.build();
try {
webRtcInteractor.getCallManager().acceptCall(activePeer.getCallId());
} catch (CallException e) {
return callFailure(currentState, "accept() failed: ", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleDenyCall(@NonNull WebRtcServiceState currentState) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
if (activePeer.getState() != CallState.LOCAL_RINGING) {
Log.w(TAG, "Can only deny from ringing!");
return currentState;
}
Log.i(TAG, "handleDenyCall():");
try {
webRtcInteractor.getCallManager().hangup();
DatabaseFactory.getSmsDatabase(context).insertMissedCall(activePeer.getId(), System.currentTimeMillis());
return terminate(currentState, activePeer);
} catch (CallException e) {
return callFailure(currentState, "hangup() failed: ", e);
}
}
protected @NonNull WebRtcServiceState handleLocalRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId());
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
Recipient recipient = remotePeer.getRecipient();
activePeer.localRinging();
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext(), recipient);
if (shouldDisturbUserWithCall) {
webRtcInteractor.startWebRtcCallActivityIfPossible();
}
webRtcInteractor.initializeAudioForCall();
if (shouldDisturbUserWithCall && TextSecurePreferences.isCallNotificationsEnabled(context)) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientDatabase.VibrateState vibrateState = recipient.resolve().getCallVibrate();
if (ringtone == null) {
ringtone = TextSecurePreferences.getCallNotificationRingtone(context);
}
webRtcInteractor.startIncomingRinger(ringtone, vibrateState == RecipientDatabase.VibrateState.ENABLED || (vibrateState == RecipientDatabase.VibrateState.DEFAULT && TextSecurePreferences.isCallNotificationVibrateEnabled(context)));
}
webRtcInteractor.registerPowerButtonReceiver();
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, activePeer);
return currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_INCOMING)
.build();
}
protected @NonNull WebRtcServiceState handleScreenOffChange(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "Silencing incoming ringer...");
webRtcInteractor.silenceIncomingRinger();
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleReceivedOfferWhileActive(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEndedRemote(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleEndedRemote(currentState, action, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEnded(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleEnded(currentState, action, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleCallConcluded(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
boolean broadcast,
@NonNull ArrayList<IceCandidateParcel> iceCandidates)
{
return activeCallDelegate.handleSendIceCandidates(currentState, callMetadata, broadcast, iceCandidates);
}
@Override
protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull ArrayList<IceCandidateParcel> iceCandidateParcels)
{
return activeCallDelegate.handleReceivedIceCandidates(currentState, callMetadata, iceCandidateParcels);
}
@Override
public @NonNull WebRtcServiceState handleCallConnected(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return callSetupDelegate.handleCallConnected(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
return callSetupDelegate.handleSetEnableVideo(currentState, enable);
}
}

View File

@ -0,0 +1,237 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
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.OfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_RINGING;
/**
* Responsible for setting up and managing the start of an outgoing 1:1 call. Transitioned
* to from idle or pre-join and can either move to a connected state (callee picks up) or
* a disconnected state (remote hangup, local hangup, etc.).
*/
public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
private static final String TAG = Log.tag(OutgoingCallActionProcessor.class);
private final ActiveCallActionProcessorDelegate activeCallDelegate;
private final CallSetupActionProcessorDelegate callSetupDelegate;
public OutgoingCallActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
activeCallDelegate = new ActiveCallActionProcessorDelegate(webRtcInteractor, TAG);
callSetupDelegate = new CallSetupActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
return activeCallDelegate.handleIsInCallQuery(currentState, resultReceiver);
}
@Override
protected @NonNull WebRtcServiceState handleStartOutgoingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleStartOutgoingCall():");
WebRtcServiceStateBuilder builder = currentState.builder();
remotePeer.dialing();
Log.i(TAG, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.startOutgoingRinger(OutgoingRinger.Type.RINGING);
webRtcInteractor.setWantsBluetoothConnection(true);
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
DatabaseFactory.getSmsDatabase(context).insertOutgoingCall(remotePeer.getId());
webRtcInteractor.retrieveTurnServers(remotePeer);
return builder.changeCallInfoState()
.activePeer(remotePeer)
.callState(WebRtcViewModel.State.CALL_OUTGOING)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSendOffer(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, @NonNull OfferMetadata offerMetadata, boolean broadcast) {
Log.i(TAG, "handleSendOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
OfferMessage offerMessage = new OfferMessage(callMetadata.getCallId().longValue(), offerMetadata.getSdp(), offerMetadata.getOfferType(), offerMetadata.getOpaque());
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage, true, destinationDeviceId);
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
return currentState;
}
@Override
public @NonNull WebRtcServiceState handleTurnServerUpdate(@NonNull WebRtcServiceState currentState,
@NonNull List<PeerConnection.IceServer> iceServers,
boolean isAlwaysTurn)
{
try {
VideoState videoState = currentState.getVideoState();
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context,
videoState.requireEglBase(),
videoState.requireLocalSink(),
callParticipant.getVideoSink(),
videoState.requireCamera(),
iceServers,
isAlwaysTurn,
currentState.getCallSetupState().isEnableVideoOnCreate());
} catch (CallException e) {
return callFailure(currentState, "Unable to proceed with call: ", e);
}
return currentState.builder()
.changeLocalDeviceState()
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
.build();
}
@Override
protected @NonNull WebRtcServiceState handleRemoteRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId());
currentState.getCallInfoState().requireActivePeer().remoteRinging();
return currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_RINGING)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleReceivedAnswer(@NonNull WebRtcServiceState currentState,
@NonNull CallMetadata callMetadata,
@NonNull WebRtcData.AnswerMetadata answerMetadata,
@NonNull WebRtcData.ReceivedAnswerMetadata receivedAnswerMetadata)
{
Log.i(TAG, "handleReceivedAnswer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
try {
byte[] remoteIdentityKey = WebRtcUtil.getPublicKeyBytes(receivedAnswerMetadata.getRemoteIdentityKey());
byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(context).serialize());
webRtcInteractor.getCallManager().receivedAnswer(callMetadata.getCallId(), callMetadata.getRemoteDevice(), answerMetadata.getOpaque(), answerMetadata.getSdp(), receivedAnswerMetadata.isMultiRing(), remoteIdentityKey, localIdentityKey);
} catch (CallException | InvalidKeyException e) {
return callFailure(currentState, "receivedAnswer() failed: ", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleReceivedBusy(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata) {
Log.i(TAG, "handleReceivedBusy(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
try {
webRtcInteractor.getCallManager().receivedBusy(callMetadata.getCallId(), callMetadata.getRemoteDevice());
} catch (CallException e) {
return callFailure(currentState, "receivedBusy() failed: ", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
}
@Override
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
return activeCallDelegate.handleLocalHangup(currentState);
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleReceivedOfferWhileActive(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEndedRemote(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleEndedRemote(currentState, action, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleEnded(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleEnded(currentState, action, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return activeCallDelegate.handleCallConcluded(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
boolean broadcast,
@NonNull ArrayList<IceCandidateParcel> iceCandidates)
{
return activeCallDelegate.handleSendIceCandidates(currentState, callMetadata, broadcast, iceCandidates);
}
@Override
protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull ArrayList<IceCandidateParcel> iceCandidateParcels)
{
return activeCallDelegate.handleReceivedIceCandidates(currentState, callMetadata, iceCandidateParcels);
}
@Override
public @NonNull WebRtcServiceState handleCallConnected(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
return callSetupDelegate.handleCallConnected(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
return callSetupDelegate.handleSetEnableVideo(currentState, enable);
}
}

View File

@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
/**
* Handles pre-join call actions. This serves as a more capable idle state as no
* call has actually start so incoming and outgoing calls are allowed.
*/
public class PreJoinActionProcessor extends DeviceAwareActionProcessor {
private static final String TAG = Log.tag(PreJoinActionProcessor.class);
private final BeginCallActionProcessorDelegate beginCallDelegate;
public PreJoinActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
beginCallDelegate = new BeginCallActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleCancelPreJoinCall(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "handleCancelPreJoinCall():");
WebRtcVideoUtil.deinitializeVideo(currentState);
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleStartIncomingCall():");
currentState = WebRtcVideoUtil.reinitializeCamera(context, webRtcInteractor.getCameraEventListener(), currentState)
.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_INCOMING)
.build();
webRtcInteractor.sendMessage(currentState);
return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer);
}
@Override
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull OfferMessage.Type offerType)
{
Log.i(TAG, "handleOutgoingCall():");
currentState = WebRtcVideoUtil.reinitializeCamera(context, webRtcInteractor.getCameraEventListener(), currentState);
return beginCallDelegate.handleOutgoingCall(currentState, remotePeer, offerType);
}
@SuppressWarnings("ConstantConditions")
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.w(TAG, "handleSetEnableVideo(): Changing for pre-join call.");
currentState.getVideoState().getCamera().setEnabled(enable);
return currentState.builder()
.changeCallSetupState()
.enableVideoOnCreate(enable)
.commit()
.changeLocalDeviceState()
.cameraState(currentState.getVideoState().getCamera().getCameraState())
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
}

View File

@ -0,0 +1,639 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.content.Context;
import android.content.Intent;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CallState;
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.OfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
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.List;
import java.util.Objects;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ACCEPT_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_BLUETOOTH_CHANGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_CALL_CONCLUDED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_CALL_CONNECTED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_CAMERA_SWITCH_COMPLETED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_DENY_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_CONNECTION_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_INTERNAL_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_BUSY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_GLARE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_ACCEPTED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_BUSY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_DECLINED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_REMOTE_HANGUP_NEED_PERMISSION;
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_IS_IN_CALL_QUERY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_LOCAL_HANGUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_LOCAL_RINGING;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_MESSAGE_SENT_ERROR;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_MESSAGE_SENT_SUCCESS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_OUTGOING_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_PRE_JOIN_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVED_OFFER_EXPIRED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVED_OFFER_WHILE_ACTIVE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVE_ANSWER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIVE_BUSY;
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_REMOTE_RINGING;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_REMOTE_VIDEO_ENABLE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SCREEN_OFF;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_ANSWER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_BUSY;
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_SET_AUDIO_BLUETOOTH;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_AUDIO_SPEAKER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_ENABLE_VIDEO;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_MUTE_AUDIO;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_START_INCOMING_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_START_OUTGOING_CALL;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_TURN_SERVER_UPDATE;
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_ERROR;
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.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.getEnable;
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;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemotePeer;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemotePeerFromMap;
/**
* Base WebRTC action processor and core of the calling state machine. As actions (as intents)
* are sent to the service, they are passed to an instance of the current state's action processor.
* Based on the state of the system, the action processor will either handle the event or do nothing.
*
* For example, the {@link OutgoingCallActionProcessor} responds to the the
* {@link #handleReceivedBusy(WebRtcServiceState, CallMetadata)} event but no others do.
*
* Processing of the actions occur in {@link #processAction(String, Intent, WebRtcServiceState)} and
* result in atomic state updates that are returned to the caller. Part of the state change can be
* the replacement of the current action processor.
*/
public abstract class WebRtcActionProcessor {
protected final Context context;
protected final WebRtcInteractor webRtcInteractor;
protected final String tag;
public WebRtcActionProcessor(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
this.context = webRtcInteractor.getWebRtcCallService();
this.webRtcInteractor = webRtcInteractor;
this.tag = tag;
}
public @NonNull String getTag() {
return tag;
}
public @NonNull WebRtcServiceState processAction(@NonNull String action, @NonNull Intent intent, @NonNull WebRtcServiceState currentState) {
switch (action) {
case ACTION_IS_IN_CALL_QUERY: return handleIsInCallQuery(currentState, intent.getParcelableExtra(EXTRA_RESULT_RECEIVER));
// Pre-Join Actions
case ACTION_PRE_JOIN_CALL: return handlePreJoinCall(currentState, getRemotePeer(intent));
case ACTION_CANCEL_PRE_JOIN_CALL: return handleCancelPreJoinCall(currentState);
// Outgoing Call Actions
case ACTION_OUTGOING_CALL: return handleOutgoingCall(currentState, getRemotePeer(intent), getOfferMessageType(intent));
case ACTION_START_OUTGOING_CALL: return handleStartOutgoingCall(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_SEND_OFFER: return handleSendOffer(currentState, CallMetadata.fromIntent(intent), OfferMetadata.fromIntent(intent), getBroadcastFlag(intent));
case ACTION_REMOTE_RINGING: return handleRemoteRinging(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_RECEIVE_ANSWER: return handleReceivedAnswer(currentState, CallMetadata.fromIntent(intent), AnswerMetadata.fromIntent(intent), ReceivedAnswerMetadata.fromIntent(intent));
case ACTION_RECEIVE_BUSY: return handleReceivedBusy(currentState, CallMetadata.fromIntent(intent));
// Incoming Call Actions
case ACTION_RECEIVE_OFFER: return handleReceivedOffer(currentState, CallMetadata.fromIntent(intent), OfferMetadata.fromIntent(intent), ReceivedOfferMetadata.fromIntent(intent));
case ACTION_RECEIVED_OFFER_EXPIRED: return handleReceivedOfferExpired(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_START_INCOMING_CALL: return handleStartIncomingCall(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_ACCEPT_CALL: return handleAcceptCall(currentState, intent.getBooleanExtra(EXTRA_ANSWER_WITH_VIDEO, false));
case ACTION_LOCAL_RINGING: return handleLocalRinging(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_DENY_CALL: return handleDenyCall(currentState);
case ACTION_SEND_ANSWER: return handleSendAnswer(currentState, CallMetadata.fromIntent(intent), AnswerMetadata.fromIntent(intent), getBroadcastFlag(intent));
// Active Call Actions
case ACTION_CALL_CONNECTED: return handleCallConnected(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_RECEIVED_OFFER_WHILE_ACTIVE: return handleReceivedOfferWhileActive(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_SEND_BUSY: return handleSendBusy(currentState, CallMetadata.fromIntent(intent), getBroadcastFlag(intent));
case ACTION_CALL_CONCLUDED: return handleCallConcluded(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_REMOTE_VIDEO_ENABLE: return handleRemoteVideoEnable(currentState, getEnable(intent));
case ACTION_RECEIVE_HANGUP: return handleReceivedHangup(currentState, CallMetadata.fromIntent(intent), HangupMetadata.fromIntent(intent));
case ACTION_LOCAL_HANGUP: return handleLocalHangup(currentState);
case ACTION_SEND_HANGUP: return handleSendHangup(currentState, CallMetadata.fromIntent(intent), HangupMetadata.fromIntent(intent), getBroadcastFlag(intent));
case ACTION_MESSAGE_SENT_SUCCESS: return handleMessageSentSuccess(currentState, getCallId(intent));
case ACTION_MESSAGE_SENT_ERROR: return handleMessageSentError(currentState, getCallId(intent), (Throwable) intent.getSerializableExtra(EXTRA_ERROR));
// Call Setup Actions
case ACTION_RECEIVE_ICE_CANDIDATES: return handleReceivedIceCandidates(currentState, CallMetadata.fromIntent(intent), getIceCandidates(intent));
case ACTION_SEND_ICE_CANDIDATES: return handleSendIceCandidates(currentState, CallMetadata.fromIntent(intent), getBroadcastFlag(intent), getIceCandidates(intent));
case ACTION_TURN_SERVER_UPDATE: return handleTurnServerUpdate(currentState, getIceServers(intent), intent.getBooleanExtra(EXTRA_IS_ALWAYS_TURN, false));
// Local Device Actions
case ACTION_SET_ENABLE_VIDEO: return handleSetEnableVideo(currentState, getEnable(intent));
case ACTION_SET_MUTE_AUDIO: return handleSetMuteAudio(currentState, intent.getBooleanExtra(EXTRA_MUTE, false));
case ACTION_FLIP_CAMERA: return handleSetCameraFlip(currentState);
case ACTION_SCREEN_OFF: return handleScreenOffChange(currentState);
case ACTION_WIRED_HEADSET_CHANGE: return handleWiredHeadsetChange(currentState, getAvailable(intent));
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));
// End Call Actions
case ACTION_ENDED_REMOTE_HANGUP:
case ACTION_ENDED_REMOTE_HANGUP_ACCEPTED:
case ACTION_ENDED_REMOTE_HANGUP_BUSY:
case ACTION_ENDED_REMOTE_HANGUP_DECLINED:
case ACTION_ENDED_REMOTE_BUSY:
case ACTION_ENDED_REMOTE_HANGUP_NEED_PERMISSION:
case ACTION_ENDED_REMOTE_GLARE: return handleEndedRemote(currentState, action, getRemotePeerFromMap(intent, currentState));
// End Call Failure Actions
case ACTION_ENDED_TIMEOUT:
case ACTION_ENDED_INTERNAL_FAILURE:
case ACTION_ENDED_SIGNALING_FAILURE:
case ACTION_ENDED_CONNECTION_FAILURE: return handleEnded(currentState, action, getRemotePeerFromMap(intent, currentState));
}
return currentState;
}
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
if (resultReceiver != null) {
resultReceiver.send(0, null);
}
return currentState;
}
//region Pre-Join
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handlePreJoinCall not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleCancelPreJoinCall(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleCancelPreJoinCall not processed");
return currentState;
}
//endregion Pre-Join
//region Outgoing Call
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
Log.i(tag, "handleOutgoingCall not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleStartOutgoingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleStartOutgoingCall not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSendOffer(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, @NonNull OfferMetadata offerMetadata, boolean broadcast) {
Log.i(tag, "handleSendOffer not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleRemoteRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleRemoteRinging not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedAnswer(@NonNull WebRtcServiceState currentState,
@NonNull CallMetadata callMetadata,
@NonNull AnswerMetadata answerMetadata,
@NonNull ReceivedAnswerMetadata receivedAnswerMetadata)
{
Log.i(tag, "handleReceivedAnswer not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedBusy(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata) {
Log.i(tag, "handleReceivedBusy not processed");
return currentState;
}
//endregion Outgoing call
//region Incoming call
protected @NonNull WebRtcServiceState handleReceivedOffer(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull WebRtcData.OfferMetadata offerMetadata,
@NonNull ReceivedOfferMetadata receivedOfferMetadata)
{
Log.i(tag, "handleReceivedOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
if (TelephonyUtil.isAnyPstnLineBusy(context)) {
Log.i(tag, "PSTN line is busy.");
currentState = currentState.getActionProcessor().handleSendBusy(currentState, callMetadata, true);
webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), true, receivedOfferMetadata.getServerReceivedTimestamp());
return currentState;
}
if (!RecipientUtil.isCallRequestAccepted(context.getApplicationContext(), callMetadata.getRemotePeer().getRecipient())) {
Log.w(tag, "Caller is untrusted.");
currentState = currentState.getActionProcessor().handleSendHangup(currentState, callMetadata, WebRtcData.HangupMetadata.fromType(HangupMessage.Type.NEED_PERMISSION), true);
webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), true, receivedOfferMetadata.getServerReceivedTimestamp());
return currentState;
}
Log.i(tag, "add remotePeer callId: " + callMetadata.getRemotePeer().getCallId() + " key: " + callMetadata.getRemotePeer().hashCode());
callMetadata.getRemotePeer().setCallStartTimestamp(receivedOfferMetadata.getServerReceivedTimestamp());
currentState = currentState.builder()
.changeCallSetupState()
.isRemoteVideoOffer(offerMetadata.getOfferType() == OfferMessage.Type.VIDEO_CALL)
.commit()
.changeCallInfoState()
.putRemotePeer(callMetadata.getRemotePeer())
.build();
long messageAgeSec = Math.max(receivedOfferMetadata.getServerDeliveredTimestamp() - receivedOfferMetadata.getServerReceivedTimestamp(), 0) / 1000;
Log.i(tag, "messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + receivedOfferMetadata.getServerReceivedTimestamp() + ", serverDeliveredTimestamp: " + receivedOfferMetadata.getServerDeliveredTimestamp());
try {
byte[] remoteIdentityKey = WebRtcUtil.getPublicKeyBytes(receivedOfferMetadata.getRemoteIdentityKey());
byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(context).serialize());
webRtcInteractor.getCallManager().receivedOffer(callMetadata.getCallId(),
callMetadata.getRemotePeer(),
callMetadata.getRemoteDevice(),
offerMetadata.getOpaque(),
offerMetadata.getSdp(),
messageAgeSec,
WebRtcUtil.getCallMediaTypeFromOfferType(offerMetadata.getOfferType()),
1,
receivedOfferMetadata.isMultiRing(),
true,
remoteIdentityKey,
localIdentityKey);
} catch (CallException | InvalidKeyException e) {
return callFailure(currentState, "Unable to process received offer: ", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedOfferExpired(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer)
{
Log.i(tag, "handleReceivedOfferExpired(): call_id: " + remotePeer.getCallId());
webRtcInteractor.insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp());
return terminate(currentState, remotePeer);
}
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleStartIncomingCall not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
Log.i(tag, "handleAcceptCall not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleLocalRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleLocalRinging not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleDenyCall(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleDenyCall not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSendAnswer(@NonNull WebRtcServiceState currentState,
@NonNull CallMetadata callMetadata,
@NonNull AnswerMetadata answerMetadata,
boolean broadcast)
{
Log.i(tag, "handleSendAnswer not processed");
return currentState;
}
//endregion Incoming call
//region Active call
protected @NonNull WebRtcServiceState handleCallConnected(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleCallConnected not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleReceivedOfferWhileActive not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSendBusy(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.CallMetadata callMetadata, boolean broadcast) {
Log.i(tag, "handleSendBusy(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
BusyMessage busyMessage = new BusyMessage(callMetadata.getCallId().longValue());
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forBusy(busyMessage, true, destinationDeviceId);
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
return currentState;
}
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleCallConcluded not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(tag, "handleRemoteVideoEnable not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedHangup(@NonNull WebRtcServiceState currentState,
@NonNull CallMetadata callMetadata,
@NonNull HangupMetadata hangupMetadata)
{
Log.i(tag, "handleReceivedHangup(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
try {
webRtcInteractor.getCallManager().receivedHangup(callMetadata.getCallId(), callMetadata.getRemoteDevice(), hangupMetadata.getCallHangupType(), hangupMetadata.getDeviceId());
} catch (CallException e) {
return callFailure(currentState, "receivedHangup() failed: ", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleLocalHangup not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSendHangup(@NonNull WebRtcServiceState currentState,
@NonNull CallMetadata callMetadata,
@NonNull HangupMetadata hangupMetadata,
boolean broadcast)
{
Log.i(tag, "handleSendHangup(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
HangupMessage hangupMessage = new HangupMessage(callMetadata.getCallId().longValue(), hangupMetadata.getType(), hangupMetadata.getDeviceId(), hangupMetadata.isLegacy());
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forHangup(hangupMessage, true, destinationDeviceId);
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
return currentState;
}
protected @NonNull WebRtcServiceState handleMessageSentSuccess(@NonNull WebRtcServiceState currentState, @NonNull CallId callId) {
try {
webRtcInteractor.getCallManager().messageSent(callId);
} catch (CallException e) {
return callFailure(currentState, "callManager.messageSent() failed: ", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleMessageSentError(@NonNull WebRtcServiceState currentState, @NonNull CallId callId, @Nullable Throwable error) {
Log.w(tag, error);
try {
webRtcInteractor.getCallManager().messageSendFailure(callId);
} catch (CallException e) {
currentState = callFailure(currentState, "callManager.messageSendFailure() failed: ", e);
}
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
if (activePeer == null) {
return currentState;
}
WebRtcServiceStateBuilder builder = currentState.builder();
if (error instanceof UntrustedIdentityException) {
CallParticipant participant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant untrusted = participant.withIdentityKey(((UntrustedIdentityException) error).getIdentityKey());
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.UNTRUSTED_IDENTITY)
.putParticipant(activePeer.getRecipient(), untrusted)
.commit();
} else if (error instanceof UnregisteredUserException) {
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.NO_SUCH_USER)
.commit();
} else if (error instanceof IOException) {
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.NETWORK_FAILURE)
.commit();
}
return builder.build();
}
//endregion Active call
//region Call setup
protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, boolean broadcast, @NonNull ArrayList<IceCandidateParcel> iceCandidates) {
Log.i(tag, "handleSendIceCandidates not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, @NonNull ArrayList<IceCandidateParcel> iceCandidateParcels) {
Log.i(tag, "handleReceivedIceCandidates not processed");
return currentState;
}
public @NonNull WebRtcServiceState handleTurnServerUpdate(@NonNull WebRtcServiceState currentState, @NonNull List<PeerConnection.IceServer> iceServers, boolean isAlwaysTurn) {
Log.i(tag, "handleTurnServerUpdate not processed");
return currentState;
}
//endregion Call setup
//region Local device
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(tag, "handleSetEnableVideo not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
Log.i(tag, "handleSetMuteAudio not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetSpeakerAudio(@NonNull WebRtcServiceState currentState, boolean isSpeaker) {
Log.i(tag, "handleSetSpeakerAudio not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetBluetoothAudio(@NonNull WebRtcServiceState currentState, boolean isBluetooth) {
Log.i(tag, "handleSetBluetoothAudio not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetCameraFlip(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleSetCameraFlip not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleScreenOffChange(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleScreenOffChange not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleBluetoothChange(@NonNull WebRtcServiceState currentState, boolean available) {
Log.i(tag, "handleBluetoothChange not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleWiredHeadsetChange(@NonNull WebRtcServiceState currentState, boolean present) {
Log.i(tag, "handleWiredHeadsetChange not processed");
return currentState;
}
public @NonNull WebRtcServiceState handleCameraSwitchCompleted(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) {
Log.i(tag, "handleCameraSwitchCompleted not processed");
return currentState;
}
//endregion Local device
//region End call
protected @NonNull WebRtcServiceState handleEndedRemote(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleEndedRemote not processed");
return currentState;
}
//endregion End call
//region End call failure
protected @NonNull WebRtcServiceState handleEnded(@NonNull WebRtcServiceState currentState, @NonNull String action, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleEnded not processed");
return currentState;
}
//endregion
//region Global call operations
public @NonNull WebRtcServiceState callFailure(@NonNull WebRtcServiceState currentState,
@Nullable String message,
@Nullable Throwable error)
{
Log.w(tag, "callFailure(): " + message, error);
WebRtcServiceStateBuilder builder = currentState.builder();
if (currentState.getCallInfoState().getActivePeer() != null) {
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED);
}
try {
webRtcInteractor.getCallManager().reset();
} catch (CallException e) {
Log.w(tag, "Unable to reset call manager: ", e);
}
currentState = builder.changeCallInfoState().clearPeerMap().build();
return terminate(currentState, currentState.getCallInfoState().getActivePeer());
}
public synchronized @NonNull WebRtcServiceState terminate(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
Log.i(tag, "terminate():");
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
if (activePeer == null) {
Log.i(tag, "skipping with no active peer");
return currentState;
}
if (!activePeer.callIdEquals(remotePeer)) {
Log.i(tag, "skipping remotePeer is not active peer");
return currentState;
}
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.stopForegroundService();
boolean playDisconnectSound = (activePeer.getState() == CallState.DIALING) ||
(activePeer.getState() == CallState.REMOTE_RINGING) ||
(activePeer.getState() == CallState.RECEIVED_BUSY) ||
(activePeer.getState() == CallState.CONNECTED);
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
return WebRtcVideoUtil.deinitializeVideo(currentState)
.builder()
.changeCallInfoState()
.activePeer(null)
.commit()
.actionProcessor(currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED ? new DisconnectingCallActionProcessor(webRtcInteractor) : new IdleActionProcessor(webRtcInteractor))
.terminate()
.build();
}
//endregion
}

View File

@ -0,0 +1,227 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
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_SERVER_DELIVERED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP;
/**
* Collection of classes to ease parsing data from intents and passing said data
* around.
*/
public class WebRtcData {
/**
* Low-level metadata Information about the call.
*/
static class CallMetadata {
private final @NonNull RemotePeer remotePeer;
private final @NonNull CallId callId;
private final int remoteDevice;
public static @NonNull CallMetadata fromIntent(@NonNull Intent intent) {
return new CallMetadata(WebRtcIntentParser.getRemotePeer(intent), WebRtcIntentParser.getCallId(intent), WebRtcIntentParser.getRemoteDevice(intent));
}
private CallMetadata(@NonNull RemotePeer remotePeer, @NonNull CallId callId, int remoteDevice) {
this.remotePeer = remotePeer;
this.callId = callId;
this.remoteDevice = remoteDevice;
}
@NonNull RemotePeer getRemotePeer() {
return remotePeer;
}
@NonNull CallId getCallId() {
return callId;
}
int getRemoteDevice() {
return remoteDevice;
}
}
/**
* Metadata for a call offer to be sent or received.
*/
static class OfferMetadata {
private final @Nullable byte[] opaque;
private final @Nullable String sdp;
private final @NonNull OfferMessage.Type offerType;
static @NonNull OfferMetadata fromIntent(@NonNull Intent intent) {
return new OfferMetadata(WebRtcIntentParser.getOfferOpaque(intent),
WebRtcIntentParser.getOfferSdp(intent),
WebRtcIntentParser.getOfferMessageType(intent));
}
private OfferMetadata(@Nullable byte[] opaque, @Nullable String sdp, @NonNull OfferMessage.Type offerType) {
this.opaque = opaque;
this.sdp = sdp;
this.offerType = offerType;
}
@Nullable byte[] getOpaque() {
return opaque;
}
@Nullable String getSdp() {
return sdp;
}
@NonNull OfferMessage.Type getOfferType() {
return offerType;
}
}
/**
* Additional metadata for a received call.
*/
static class ReceivedOfferMetadata {
private final @NonNull byte[] remoteIdentityKey;
private final long serverReceivedTimestamp;
private final long serverDeliveredTimestamp;
private final boolean isMultiRing;
static @NonNull ReceivedOfferMetadata fromIntent(@NonNull Intent intent) {
return new ReceivedOfferMetadata(WebRtcIntentParser.getRemoteIdentityKey(intent),
intent.getLongExtra(EXTRA_SERVER_RECEIVED_TIMESTAMP, -1),
intent.getLongExtra(EXTRA_SERVER_DELIVERED_TIMESTAMP, -1),
WebRtcIntentParser.getMultiRingFlag(intent));
}
ReceivedOfferMetadata(@NonNull byte[] remoteIdentityKey, long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean isMultiRing) {
this.remoteIdentityKey = remoteIdentityKey;
this.serverReceivedTimestamp = serverReceivedTimestamp;
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
this.isMultiRing = isMultiRing;
}
@NonNull byte[] getRemoteIdentityKey() {
return remoteIdentityKey;
}
long getServerReceivedTimestamp() {
return serverReceivedTimestamp;
}
long getServerDeliveredTimestamp() {
return serverDeliveredTimestamp;
}
boolean isMultiRing() {
return isMultiRing;
}
}
/**
* Metadata for an answer to be sent or received.
*/
static class AnswerMetadata {
private final @Nullable byte[] opaque;
private final @Nullable String sdp;
static @NonNull AnswerMetadata fromIntent(@NonNull Intent intent) {
return new AnswerMetadata(WebRtcIntentParser.getAnswerOpaque(intent), WebRtcIntentParser.getAnswerSdp(intent));
}
private AnswerMetadata(@Nullable byte[] opaque, @Nullable String sdp) {
this.opaque = opaque;
this.sdp = sdp;
}
@Nullable byte[] getOpaque() {
return opaque;
}
@Nullable String getSdp() {
return sdp;
}
}
/**
* Additional metadata for a received answer.
*/
static class ReceivedAnswerMetadata {
private final @NonNull byte[] remoteIdentityKey;
private final boolean isMultiRing;
static @NonNull ReceivedAnswerMetadata fromIntent(@NonNull Intent intent) {
return new ReceivedAnswerMetadata(WebRtcIntentParser.getRemoteIdentityKey(intent), WebRtcIntentParser.getMultiRingFlag(intent));
}
ReceivedAnswerMetadata(@NonNull byte[] remoteIdentityKey, boolean isMultiRing) {
this.remoteIdentityKey = remoteIdentityKey;
this.isMultiRing = isMultiRing;
}
@NonNull byte[] getRemoteIdentityKey() {
return remoteIdentityKey;
}
boolean isMultiRing() {
return isMultiRing;
}
}
/**
* Metadata for a remote or local hangup.
*/
static class HangupMetadata {
private final @NonNull HangupMessage.Type type;
private final boolean isLegacy;
private final int deviceId;
static @NonNull HangupMetadata fromIntent(@NonNull Intent intent) {
return new HangupMetadata(HangupMessage.Type.fromCode(intent.getStringExtra(EXTRA_HANGUP_TYPE)),
intent.getBooleanExtra(EXTRA_HANGUP_IS_LEGACY, true),
intent.getIntExtra(EXTRA_HANGUP_DEVICE_ID, 0));
}
static @NonNull HangupMetadata fromType(@NonNull HangupMessage.Type type) {
return new HangupMetadata(type, true, 0);
}
HangupMetadata(@NonNull HangupMessage.Type type, boolean isLegacy, int deviceId) {
this.type = type;
this.isLegacy = isLegacy;
this.deviceId = deviceId;
}
@NonNull HangupMessage.Type getType() {
return type;
}
@NonNull CallManager.HangupType getCallHangupType() {
switch (type) {
case ACCEPTED: return CallManager.HangupType.ACCEPTED;
case BUSY: return CallManager.HangupType.BUSY;
case NORMAL: return CallManager.HangupType.NORMAL;
case DECLINED: return CallManager.HangupType.DECLINED;
case NEED_PERMISSION: return CallManager.HangupType.NEED_PERMISSION;
default: throw new IllegalArgumentException("Unexpected hangup type: " + type);
}
}
boolean isLegacy() {
return isLegacy;
}
int getDeviceId() {
return deviceId;
}
}
}

View File

@ -0,0 +1,146 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.ringrtc.TurnServerInfoParcel;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.webrtc.PeerConnection;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
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_ENABLE;
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_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;
/**
* Helper to parse the various attributes out of intents passed to the service.
*/
public final class WebRtcIntentParser {
private static final String TAG = Log.tag(WebRtcIntentParser.class);
private WebRtcIntentParser() {}
public static @NonNull CallId getCallId(@NonNull Intent intent) {
return new CallId(intent.getLongExtra(EXTRA_CALL_ID, -1));
}
public static int getRemoteDevice(@NonNull Intent intent) {
return intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
}
public static @NonNull RemotePeer getRemotePeer(@NonNull Intent intent) {
RemotePeer remotePeer = intent.getParcelableExtra(WebRtcCallService.EXTRA_REMOTE_PEER);
if (remotePeer == null) {
throw new AssertionError("No RemotePeer in intent!");
}
return remotePeer;
}
public static @NonNull RemotePeer getRemotePeerFromMap(@NonNull Intent intent, @NonNull WebRtcServiceState currentState) {
int remotePeerKey = getRemotePeerKey(intent);
RemotePeer remotePeer = currentState.getCallInfoState().getPeer(remotePeerKey);
if (remotePeer == null) {
throw new AssertionError("No RemotePeer in map for key: " + remotePeerKey + "!");
}
return remotePeer;
}
public static int getRemotePeerKey(@NonNull Intent intent) {
if (!intent.hasExtra(EXTRA_REMOTE_PEER_KEY)) {
throw new AssertionError("No RemotePeer key in intent!");
}
// The default of -1 should never be applied since the key exists.
return intent.getIntExtra(EXTRA_REMOTE_PEER_KEY, -1);
}
public static boolean getMultiRingFlag(@NonNull Intent intent) {
return intent.getBooleanExtra(EXTRA_MULTI_RING, false);
}
public static @NonNull byte[] getRemoteIdentityKey(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getByteArrayExtra(EXTRA_REMOTE_IDENTITY_KEY));
}
public static @Nullable String getAnswerSdp(@NonNull Intent intent) {
return intent.getStringExtra(EXTRA_ANSWER_SDP);
}
public static @Nullable String getOfferSdp(@NonNull Intent intent) {
return intent.getStringExtra(EXTRA_OFFER_SDP);
}
public static @Nullable byte[] getAnswerOpaque(@NonNull Intent intent) {
return intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE);
}
public static @Nullable byte[] getOfferOpaque(@NonNull Intent intent) {
return intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE);
}
public static boolean getBroadcastFlag(@NonNull Intent intent) {
return intent.getBooleanExtra(EXTRA_BROADCAST, false);
}
public static boolean getAvailable(@NonNull Intent intent) {
return intent.getBooleanExtra(EXTRA_AVAILABLE, false);
}
public static @NonNull ArrayList<IceCandidateParcel> getIceCandidates(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES));
}
public static @NonNull List<PeerConnection.IceServer> getIceServers(@NonNull Intent intent) {
TurnServerInfoParcel turnServerInfoParcel = Objects.requireNonNull(intent.getParcelableExtra(EXTRA_TURN_SERVER_INFO));
List<PeerConnection.IceServer> iceServers = new LinkedList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
for (String url : turnServerInfoParcel.getUrls()) {
Log.i(TAG, "ice_server: " + url);
if (url.startsWith("turn")) {
iceServers.add(PeerConnection.IceServer.builder(url)
.setUsername(turnServerInfoParcel.getUsername())
.setPassword(turnServerInfoParcel.getPassword())
.createIceServer());
} else {
iceServers.add(PeerConnection.IceServer.builder(url).createIceServer());
}
}
return iceServers;
}
public static @NonNull OfferMessage.Type getOfferMessageType(@NonNull Intent intent) {
return OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
}
public static boolean getEnable(@NonNull Intent intent) {
return intent.getBooleanExtra(EXTRA_ENABLE, false);
}
}

View File

@ -0,0 +1,127 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
/**
* 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
* heavy use of {@link CallManager} throughout, it was exempted from the rule.
*/
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;
public WebRtcInteractor(@NonNull WebRtcCallService webRtcCallService,
@NonNull CallManager callManager,
@NonNull LockManager lockManager,
@NonNull SignalAudioManager audioManager,
@NonNull BluetoothStateManager bluetoothStateManager,
@NonNull CameraEventListener cameraEventListener)
{
this.webRtcCallService = webRtcCallService;
this.callManager = callManager;
this.lockManager = lockManager;
this.audioManager = audioManager;
this.bluetoothStateManager = bluetoothStateManager;
this.cameraEventListener = cameraEventListener;
}
@NonNull CameraEventListener getCameraEventListener() {
return cameraEventListener;
}
@NonNull CallManager getCallManager() {
return callManager;
}
@NonNull WebRtcCallService getWebRtcCallService() {
return webRtcCallService;
}
void setWantsBluetoothConnection(boolean enabled) {
bluetoothStateManager.setWantsConnection(enabled);
}
void updatePhoneState(@NonNull LockManager.PhoneState phoneState) {
lockManager.updatePhoneState(phoneState);
}
void sendMessage(@NonNull WebRtcServiceState state) {
webRtcCallService.sendMessage(state);
}
void sendCallMessage(@NonNull RemotePeer remotePeer, @NonNull SignalServiceCallMessage callMessage) {
webRtcCallService.sendCallMessage(remotePeer, callMessage);
}
void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
webRtcCallService.setCallInProgressNotification(type, remotePeer);
}
void retrieveTurnServers(@NonNull RemotePeer remotePeer) {
webRtcCallService.retrieveTurnServers(remotePeer);
}
void stopForegroundService() {
webRtcCallService.stopForeground(true);
}
void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal, long timestamp) {
webRtcCallService.insertMissedCall(remotePeer, signal, timestamp);
}
void startWebRtcCallActivityIfPossible() {
webRtcCallService.startCallCardActivityIfPossible();
}
void registerPowerButtonReceiver() {
webRtcCallService.registerPowerButtonReceiver();
}
void unregisterPowerButtonReceiver() {
webRtcCallService.unregisterPowerButtonReceiver();
}
void silenceIncomingRinger() {
audioManager.silenceIncomingRinger();
}
void initializeAudioForCall() {
audioManager.initializeAudioForCall();
}
void startIncomingRinger(@Nullable Uri ringtoneUri, boolean vibrate) {
audioManager.startIncomingRinger(ringtoneUri, vibrate);
}
void startOutgoingRinger(@NonNull OutgoingRinger.Type type) {
audioManager.startOutgoingRinger(type);
}
void stopAudio(boolean playDisconnect) {
audioManager.stop(playDisconnect);
}
void startAudioCommunication(boolean preserveSpeakerphone) {
audioManager.startCommunication(preserveSpeakerphone);
}
}

View File

@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.content.Context;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.DjbECPublicKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
/**
* Calling specific helpers.
*/
public final class WebRtcUtil {
private WebRtcUtil() {}
public static @NonNull byte[] getPublicKeyBytes(@NonNull byte[] identityKey) throws InvalidKeyException {
ECPublicKey key = Curve.decodePoint(identityKey, 0);
if (key instanceof DjbECPublicKey) {
return ((DjbECPublicKey) key).getPublicKey();
}
throw new InvalidKeyException();
}
public static @NonNull LockManager.PhoneState getInCallPhoneState(@NonNull Context context) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
if (audioManager.isSpeakerphoneOn() || audioManager.isBluetoothScoOn() || audioManager.isWiredHeadsetOn()) {
return LockManager.PhoneState.IN_HANDS_FREE_CALL;
} else {
return LockManager.PhoneState.IN_CALL;
}
}
public static @NonNull CallManager.CallMediaType getCallMediaTypeFromOfferType(@NonNull OfferMessage.Type offerType) {
return offerType == OfferMessage.Type.VIDEO_CALL ? CallManager.CallMediaType.VIDEO_CALL : CallManager.CallMediaType.AUDIO_CALL;
}
public static void enableSpeakerPhoneIfNeeded(@NonNull Context context, boolean enable) {
if (!enable) {
return;
}
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
//noinspection deprecation
boolean shouldEnable = !(androidAudioManager.isSpeakerphoneOn() || androidAudioManager.isBluetoothScoOn() || androidAudioManager.isWiredHeadsetOn());
if (shouldEnable) {
androidAudioManager.setSpeakerphoneOn(true);
}
}
}

View File

@ -0,0 +1,96 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.Util;
import org.webrtc.EglBase;
/**
* Helper for initializing, reinitializing, and deinitializing the camera and it's related
* infrastructure.
*/
public final class WebRtcVideoUtil {
private WebRtcVideoUtil() {}
public static @NonNull WebRtcServiceState initializeVideo(@NonNull Context context,
@NonNull CameraEventListener cameraEventListener,
@NonNull WebRtcServiceState currentState)
{
final WebRtcServiceStateBuilder builder = currentState.builder();
Util.runOnMainSync(() -> {
EglBase eglBase = EglBase.create();
BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase);
Camera camera = new Camera(context, cameraEventListener, eglBase, CameraState.Direction.FRONT);
builder.changeVideoState()
.eglBase(eglBase)
.localSink(localSink)
.camera(camera)
.commit()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.commit();
});
return builder.build();
}
public static @NonNull WebRtcServiceState reinitializeCamera(@NonNull Context context,
@NonNull CameraEventListener cameraEventListener,
@NonNull WebRtcServiceState currentState)
{
final WebRtcServiceStateBuilder builder = currentState.builder();
Util.runOnMainSync(() -> {
Camera camera = currentState.getVideoState().requireCamera();
camera.setEnabled(false);
camera.dispose();
camera = new Camera(context,
cameraEventListener,
currentState.getVideoState().requireEglBase(),
currentState.getLocalDeviceState().getCameraState().getActiveDirection());
builder.changeVideoState()
.camera(camera)
.commit()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.commit();
});
return builder.build();
}
public static @NonNull WebRtcServiceState deinitializeVideo(@NonNull WebRtcServiceState currentState) {
Camera camera = currentState.getVideoState().getCamera();
if (camera != null) {
camera.dispose();
}
EglBase eglBase = currentState.getVideoState().getEglBase();
if (eglBase != null) {
eglBase.release();
}
return currentState.builder()
.changeVideoState()
.eglBase(null)
.camera(null)
.localSink(null)
.commit()
.changeLocalDeviceState()
.cameraState(CameraState.UNKNOWN)
.build();
}
}

View File

@ -0,0 +1,84 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* General state of ongoing calls.
*/
public class CallInfoState {
WebRtcViewModel.State callState;
Recipient callRecipient;
long callConnectedTime;
Map<Recipient, CallParticipant> remoteParticipants;
Map<Integer, RemotePeer> peerMap;
RemotePeer activePeer;
public CallInfoState() {
this(WebRtcViewModel.State.IDLE, Recipient.UNKNOWN, -1, Collections.emptyMap(), Collections.emptyMap(), null);
}
public CallInfoState(@NonNull CallInfoState toCopy) {
this(toCopy.callState, toCopy.callRecipient, toCopy.callConnectedTime, toCopy.remoteParticipants, toCopy.peerMap, toCopy.activePeer);
}
public CallInfoState(@NonNull WebRtcViewModel.State callState,
@NonNull Recipient callRecipient,
long callConnectedTime,
@NonNull Map<Recipient, CallParticipant> remoteParticipants,
@NonNull Map<Integer, RemotePeer> peerMap,
@Nullable RemotePeer activePeer)
{
this.callState = callState;
this.callRecipient = callRecipient;
this.callConnectedTime = callConnectedTime;
this.remoteParticipants = new LinkedHashMap<>(remoteParticipants);
this.peerMap = new HashMap<>(peerMap);
this.activePeer = activePeer;
}
public @NonNull Recipient getCallRecipient() {
return callRecipient;
}
public long getCallConnectedTime() {
return callConnectedTime;
}
public @Nullable CallParticipant getRemoteParticipant(@NonNull Recipient recipient) {
return remoteParticipants.get(recipient);
}
public @NonNull ArrayList<CallParticipant> getRemoteCallParticipants() {
return new ArrayList<>(remoteParticipants.values());
}
public @NonNull WebRtcViewModel.State getCallState() {
return callState;
}
public @Nullable RemotePeer getPeer(int hashCode) {
return peerMap.get(hashCode);
}
public @Nullable RemotePeer getActivePeer() {
return activePeer;
}
public @NonNull RemotePeer requireActivePeer() {
return Objects.requireNonNull(activePeer);
}
}

View File

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

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.ringrtc.CameraState;
/**
* Local device specific state.
*/
public final class LocalDeviceState {
CameraState cameraState;
boolean microphoneEnabled;
boolean bluetoothAvailable;
LocalDeviceState() {
this(CameraState.UNKNOWN, true, false);
}
LocalDeviceState(@NonNull LocalDeviceState toCopy) {
this(toCopy.cameraState, toCopy.microphoneEnabled, toCopy.bluetoothAvailable);
}
LocalDeviceState(@NonNull CameraState cameraState, boolean microphoneEnabled, boolean bluetoothAvailable) {
this.cameraState = cameraState;
this.microphoneEnabled = microphoneEnabled;
this.bluetoothAvailable = bluetoothAvailable;
}
public @NonNull CameraState getCameraState() {
return cameraState;
}
public boolean isMicrophoneEnabled() {
return microphoneEnabled;
}
public boolean isBluetoothAvailable() {
return bluetoothAvailable;
}
}

View File

@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.webrtc.EglBase;
import java.util.Objects;
/**
* Local device video state and infrastructure.
*/
public final class VideoState {
EglBase eglBase;
BroadcastVideoSink localSink;
Camera camera;
VideoState() {
this(null, null, null);
}
VideoState(@NonNull VideoState toCopy) {
this(toCopy.eglBase, toCopy.localSink, toCopy.camera);
}
VideoState(@Nullable EglBase eglBase, @Nullable BroadcastVideoSink localSink, @Nullable Camera camera) {
this.eglBase = eglBase;
this.localSink = localSink;
this.camera = camera;
}
public @Nullable EglBase getEglBase() {
return eglBase;
}
public @NonNull EglBase requireEglBase() {
return Objects.requireNonNull(eglBase);
}
public @Nullable BroadcastVideoSink getLocalSink() {
return localSink;
}
public @NonNull BroadcastVideoSink requireLocalSink() {
return Objects.requireNonNull(localSink);
}
public @Nullable Camera getCamera() {
return camera;
}
public @NonNull Camera requireCamera() {
return Objects.requireNonNull(camera);
}
}

View File

@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
/**
* Represent the entire state of the call system.
*/
public final class WebRtcServiceState {
WebRtcActionProcessor actionProcessor;
CallSetupState callSetupState;
CallInfoState callInfoState;
LocalDeviceState localDeviceState;
VideoState videoState;
public WebRtcServiceState(@NonNull WebRtcActionProcessor actionProcessor) {
this.actionProcessor = actionProcessor;
this.callSetupState = new CallSetupState();
this.callInfoState = new CallInfoState();
this.localDeviceState = new LocalDeviceState();
this.videoState = new VideoState();
}
public WebRtcServiceState(@NonNull WebRtcServiceState toCopy) {
this.actionProcessor = toCopy.actionProcessor;
this.callSetupState = new CallSetupState(toCopy.callSetupState);
this.callInfoState = new CallInfoState(toCopy.callInfoState);
this.localDeviceState = new LocalDeviceState(toCopy.localDeviceState);
this.videoState = new VideoState(toCopy.videoState);
}
public @NonNull WebRtcActionProcessor getActionProcessor() {
return actionProcessor;
}
public @NonNull CallSetupState getCallSetupState() {
return callSetupState;
}
public @NonNull CallInfoState getCallInfoState() {
return callInfoState;
}
public @NonNull LocalDeviceState getLocalDeviceState() {
return localDeviceState;
}
public @NonNull VideoState getVideoState() {
return videoState;
}
public @NonNull WebRtcServiceStateBuilder builder() {
return new WebRtcServiceStateBuilder(this);
}
}

View File

@ -0,0 +1,220 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
import org.webrtc.EglBase;
/**
* Builder that creates a new {@link WebRtcServiceState} from an existing one and allows
* changes to all normally immutable data.
*/
public class WebRtcServiceStateBuilder {
private WebRtcServiceState toBuild;
public WebRtcServiceStateBuilder(@NonNull WebRtcServiceState webRtcServiceState) {
toBuild = new WebRtcServiceState(webRtcServiceState);
}
public @NonNull WebRtcServiceState build() {
return toBuild;
}
public @NonNull WebRtcServiceStateBuilder actionProcessor(@NonNull WebRtcActionProcessor actionHandler) {
toBuild.actionProcessor = actionHandler;
return this;
}
public @NonNull CallSetupStateBuilder changeCallSetupState() {
return new CallSetupStateBuilder();
}
public @NonNull CallInfoStateBuilder changeCallInfoState() {
return new CallInfoStateBuilder();
}
public @NonNull LocalDeviceStateBuilder changeLocalDeviceState() {
return new LocalDeviceStateBuilder();
}
public @NonNull VideoStateBuilder changeVideoState() {
return new VideoStateBuilder();
}
public @NonNull WebRtcServiceStateBuilder terminate() {
toBuild.callSetupState = new CallSetupState();
toBuild.localDeviceState = new LocalDeviceState();
toBuild.videoState = new VideoState();
CallInfoState newCallInfoState = new CallInfoState();
newCallInfoState.peerMap.putAll(toBuild.callInfoState.peerMap);
toBuild.callInfoState = newCallInfoState;
return this;
}
public class LocalDeviceStateBuilder {
private LocalDeviceState toBuild;
public LocalDeviceStateBuilder() {
toBuild = new LocalDeviceState(WebRtcServiceStateBuilder.this.toBuild.localDeviceState);
}
public @NonNull WebRtcServiceStateBuilder commit() {
WebRtcServiceStateBuilder.this.toBuild.localDeviceState = toBuild;
return WebRtcServiceStateBuilder.this;
}
public @NonNull WebRtcServiceState build() {
commit();
return WebRtcServiceStateBuilder.this.build();
}
public @NonNull LocalDeviceStateBuilder cameraState(@NonNull CameraState cameraState) {
toBuild.cameraState = cameraState;
return this;
}
public @NonNull LocalDeviceStateBuilder isMicrophoneEnabled(boolean enabled) {
toBuild.microphoneEnabled = enabled;
return this;
}
public @NonNull LocalDeviceStateBuilder isBluetoothAvailable(boolean available) {
toBuild.bluetoothAvailable = available;
return this;
}
}
public class CallSetupStateBuilder {
private CallSetupState toBuild;
public CallSetupStateBuilder() {
toBuild = new CallSetupState(WebRtcServiceStateBuilder.this.toBuild.callSetupState);
}
public @NonNull WebRtcServiceStateBuilder commit() {
WebRtcServiceStateBuilder.this.toBuild.callSetupState = toBuild;
return WebRtcServiceStateBuilder.this;
}
public @NonNull WebRtcServiceState build() {
commit();
return WebRtcServiceStateBuilder.this.build();
}
public @NonNull CallSetupStateBuilder enableVideoOnCreate(boolean enableVideoOnCreate) {
toBuild.enableVideoOnCreate = enableVideoOnCreate;
return this;
}
public @NonNull CallSetupStateBuilder isRemoteVideoOffer(boolean isRemoteVideoOffer) {
toBuild.isRemoteVideoOffer = isRemoteVideoOffer;
return this;
}
public @NonNull CallSetupStateBuilder acceptWithVideo(boolean acceptWithVideo) {
toBuild.acceptWithVideo = acceptWithVideo;
return this;
}
}
public class VideoStateBuilder {
private VideoState toBuild;
public VideoStateBuilder() {
toBuild = new VideoState(WebRtcServiceStateBuilder.this.toBuild.videoState);
}
public @NonNull WebRtcServiceStateBuilder commit() {
WebRtcServiceStateBuilder.this.toBuild.videoState = toBuild;
return WebRtcServiceStateBuilder.this;
}
public @NonNull WebRtcServiceState build() {
commit();
return WebRtcServiceStateBuilder.this.build();
}
public @NonNull VideoStateBuilder eglBase(@Nullable EglBase eglBase) {
toBuild.eglBase = eglBase;
return this;
}
public @NonNull VideoStateBuilder localSink(@Nullable BroadcastVideoSink localSink) {
toBuild.localSink = localSink;
return this;
}
public @NonNull VideoStateBuilder camera(@Nullable Camera camera) {
toBuild.camera = camera;
return this;
}
}
public class CallInfoStateBuilder {
private CallInfoState toBuild;
public CallInfoStateBuilder() {
toBuild = new CallInfoState(WebRtcServiceStateBuilder.this.toBuild.callInfoState);
}
public @NonNull WebRtcServiceStateBuilder commit() {
WebRtcServiceStateBuilder.this.toBuild.callInfoState = toBuild;
return WebRtcServiceStateBuilder.this;
}
public @NonNull WebRtcServiceState build() {
commit();
return WebRtcServiceStateBuilder.this.build();
}
public @NonNull CallInfoStateBuilder callState(@NonNull WebRtcViewModel.State callState) {
toBuild.callState = callState;
return this;
}
public @NonNull CallInfoStateBuilder callRecipient(@NonNull Recipient callRecipient) {
toBuild.callRecipient = callRecipient;
return this;
}
public @NonNull CallInfoStateBuilder callConnectedTime(long callConnectedTime) {
toBuild.callConnectedTime = callConnectedTime;
return this;
}
public @NonNull CallInfoStateBuilder putParticipant(@NonNull Recipient recipient, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(recipient, callParticipant);
return this;
}
public @NonNull CallInfoStateBuilder putRemotePeer(@NonNull RemotePeer remotePeer) {
toBuild.peerMap.put(remotePeer.hashCode(), remotePeer);
return this;
}
public @NonNull CallInfoStateBuilder clearPeerMap() {
toBuild.peerMap.clear();
return this;
}
public @NonNull CallInfoStateBuilder removeRemotePeer(@NonNull RemotePeer remotePeer) {
toBuild.peerMap.remove(remotePeer.hashCode());
return this;
}
public @NonNull CallInfoStateBuilder activePeer(@Nullable RemotePeer activePeer) {
toBuild.activePeer = activePeer;
return this;
}
}
}