diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java index 0981b9dd58..1b4186143a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index f6818b6ee1..56472b8945 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -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 previouslyVisibleViewSet, boolean useSmallMargins) { Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS); + TransitionManager.endTransitions(parent); TransitionManager.beginDelayedTransition(parent, transition); ConstraintSet constraintSet = new ConstraintSet(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java index 863bf82c09..787b4f3d8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java @@ -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 + + '}'; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java index 608ee5af09..cd0a62473b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java @@ -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 + + '}'; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java index 083011a7fc..7c026935e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java index 1e96643a9b..1b6c554fdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraState.java @@ -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 CREATOR = new Creator() { + @Override + public CameraState createFromParcel(Parcel in) { + return new CameraState(in); + } + + @Override + public CameraState[] newArray(int size) { + return new CameraState[size]; + } + }; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java index 7c5d9cfcee..10577a09fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/TurnServerInfoParcel.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/TurnServerInfoParcel.java new file mode 100644 index 0000000000..472a420682 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/TurnServerInfoParcel.java @@ -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 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 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 CREATOR = new Creator() { + @Override + public TurnServerInfoParcel createFromParcel(Parcel in) { + return new TurnServerInfoParcel(in); + } + + @Override + public TurnServerInfoParcel[] newArray(int size) { + return new TurnServerInfoParcel[size]; + } + }; +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index bc04d493cc..dbdcd80acd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -6,13 +6,11 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; -import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.os.ResultReceiver; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; -import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -26,63 +24,40 @@ import org.signal.ringrtc.IceCandidate; import org.signal.ringrtc.Remote; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.WebRtcCallActivity; -import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; 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.notifications.DoNotDisturbUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.ringrtc.CallState; -import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.CameraEventListener; import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel; import org.thoughtcrime.securesms.ringrtc.RemotePeer; +import org.thoughtcrime.securesms.ringrtc.TurnServerInfoParcel; +import org.thoughtcrime.securesms.service.webrtc.IdleActionProcessor; +import org.thoughtcrime.securesms.service.webrtc.WebRtcInteractor; +import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.FutureTaskListener; import org.thoughtcrime.securesms.util.ListenableFutureTask; -import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TelephonyUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager; 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.webrtc.CapturerObserver; -import org.webrtc.EglBase; -import org.webrtc.PeerConnection; -import org.webrtc.VideoFrame; -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.libsignal.util.Pair; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; -import org.whispersystems.signalservice.api.messages.calls.BusyMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage; -import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; -import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; -import java.io.IOException; import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -90,11 +65,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -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; - public class WebRtcCallService extends Service implements CallManager.Observer, BluetoothStateManager.BluetoothStateListener, CameraEventListener @@ -127,6 +97,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String EXTRA_ENABLE = "enable_value"; public static final String EXTRA_BROADCAST = "broadcast"; public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video"; + public static final String EXTRA_ERROR = "error"; + public static final String EXTRA_CAMERA_STATE = "camera_state"; + public static final String EXTRA_IS_ALWAYS_TURN = "is_always_turn"; + public static final String EXTRA_TURN_SERVER_INFO = "turn_server_info"; public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN"; public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL"; @@ -173,50 +147,43 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String ACTION_RECEIVED_OFFER_EXPIRED = "RECEIVED_OFFER_EXPIRED"; public static final String ACTION_RECEIVED_OFFER_WHILE_ACTIVE = "RECEIVED_OFFER_WHILE_ACTIVE"; public static final String ACTION_CALL_CONCLUDED = "CALL_CONCLUDED"; + public static final String ACTION_MESSAGE_SENT_SUCCESS = "MESSAGE_SENT_SUCCESS"; + public static final String ACTION_MESSAGE_SENT_ERROR = "MESSAGE_SENT_ERROR"; + public static final String ACTION_CAMERA_SWITCH_COMPLETED = "CAMERA_FLIP_COMPLETE"; + public static final String ACTION_TURN_SERVER_UPDATE = "TURN_SERVER_UPDATE"; public static final int BUSY_TONE_LENGTH = 2000; - private CameraState localCameraState = CameraState.UNKNOWN; - private boolean microphoneEnabled = true; - private boolean bluetoothAvailable = false; - private boolean enableVideoOnCreate = false; - private boolean isRemoteVideoOffer = false; - private boolean acceptWithVideo = false; - - private long callConnectedTime = -1; - private SignalServiceMessageSender messageSender; private SignalServiceAccountManager accountManager; - private SignalAudioManager audioManager; private BluetoothStateManager bluetoothStateManager; private WiredHeadsetStateReceiver wiredHeadsetStateReceiver; private PowerButtonReceiver powerButtonReceiver; private LockManager lockManager; private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager; + private WebRtcInteractor webRtcInteractor; - @Nullable private CallManager callManager; - @Nullable private RemotePeer activePeer; - @Nullable private RemotePeer busyPeer; - @Nullable private RemotePeer preJoinPeer; - @Nullable private SparseArray peerMap; - - @Nullable private EglBase eglBase; - @Nullable private BroadcastVideoSink localSink; - @Nullable private Camera camera; - - private final Map remoteParticipantMap = new LinkedHashMap<>(); + @Nullable private CallManager callManager; private final ExecutorService serviceExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService networkExecutor = Executors.newSingleThreadExecutor(); private final PhoneStateListener hangUpRtcOnDeviceCallAnswered = new HangUpRtcOnPstnCallAnsweredListener(); + private WebRtcServiceState serviceState; + @Override public void onCreate() { super.onCreate(); Log.i(TAG, "onCreate"); - initializeResources(); + boolean successful = initializeResources(); + if (!successful) { + stopSelf(); + return; + } + + serviceState = new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor)); registerUncaughtExceptionHandler(); registerWiredHeadsetStateReceiver(); @@ -225,57 +192,45 @@ public class WebRtcCallService extends Service implements CallManager.Observer, .listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_CALL_STATE); } + private boolean initializeResources() { + this.messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + this.accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + this.lockManager = new LockManager(this); + this.bluetoothStateManager = new BluetoothStateManager(this, this); + + this.messageSender.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); + this.accountManager.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); + + try { + this.callManager = Objects.requireNonNull(CallManager.createCallManager(this)); + } catch (NullPointerException | CallException e) { + Log.e(TAG, "Unable to create Call Manager: ", e); + return false; + } + + webRtcInteractor = new WebRtcInteractor(this, callManager, lockManager, new SignalAudioManager(this), bluetoothStateManager, this); + return true; + } + @Override - public int onStartCommand(final Intent intent, int flags, int startId) { - Log.i(TAG, "onStartCommand..."); + public int onStartCommand(final @Nullable Intent intent, int flags, int startId) { + Log.i(TAG, "onStartCommand... action: " + (intent == null ? "NA" : intent.getAction())); if (intent == null || intent.getAction() == null) return START_NOT_STICKY; serviceExecutor.execute(() -> { - if (intent.getAction().equals(ACTION_RECEIVE_OFFER)) handleReceivedOffer(intent); - else if (intent.getAction().equals(ACTION_RECEIVE_BUSY)) handleReceivedBusy(intent); - else if (intent.getAction().equals(ACTION_PRE_JOIN_CALL)) handlePreJoinCall(intent); - else if (intent.getAction().equals(ACTION_CANCEL_PRE_JOIN_CALL)) handleCancelPreJoinCall(); - else if (intent.getAction().equals(ACTION_OUTGOING_CALL) && isIdle()) handleOutgoingCall(intent); - else if (intent.getAction().equals(ACTION_DENY_CALL)) handleDenyCall(intent); - else if (intent.getAction().equals(ACTION_LOCAL_HANGUP)) handleLocalHangup(intent); - else if (intent.getAction().equals(ACTION_SET_MUTE_AUDIO)) handleSetMuteAudio(intent); - else if (intent.getAction().equals(ACTION_FLIP_CAMERA)) handleSetCameraFlip(intent); - else if (intent.getAction().equals(ACTION_BLUETOOTH_CHANGE)) handleBluetoothChange(intent); - else if (intent.getAction().equals(ACTION_WIRED_HEADSET_CHANGE)) handleWiredHeadsetChange(intent); - else if (intent.getAction().equals(ACTION_SCREEN_OFF)) handleScreenOffChange(intent); - else if (intent.getAction().equals(ACTION_CALL_CONNECTED)) handleCallConnected(intent); - else if (intent.getAction().equals(ACTION_IS_IN_CALL_QUERY)) handleIsInCallQuery(intent); - else if (intent.getAction().equals(ACTION_SET_AUDIO_SPEAKER)) handleSetSpeakerAudio(intent); - else if (intent.getAction().equals(ACTION_SET_AUDIO_BLUETOOTH)) handleSetBluetoothAudio(intent); - else if (intent.getAction().equals(ACTION_START_OUTGOING_CALL)) handleStartOutgoingCall(intent); - else if (intent.getAction().equals(ACTION_START_INCOMING_CALL)) handleStartIncomingCall(intent); - else if (intent.getAction().equals(ACTION_ACCEPT_CALL)) handleAcceptCall(intent); - else if (intent.getAction().equals(ACTION_LOCAL_RINGING)) handleLocalRinging(intent); - else if (intent.getAction().equals(ACTION_REMOTE_RINGING)) handleRemoteRinging(intent); - else if (intent.getAction().equals(ACTION_SEND_OFFER)) handleSendOffer(intent); - else if (intent.getAction().equals(ACTION_SEND_ANSWER)) handleSendAnswer(intent); - else if (intent.getAction().equals(ACTION_SEND_ICE_CANDIDATES)) handleSendIceCandidates(intent); - else if (intent.getAction().equals(ACTION_SEND_HANGUP)) handleSendHangup(intent); - else if (intent.getAction().equals(ACTION_SEND_BUSY)) handleSendBusy(intent); - else if (intent.getAction().equals(ACTION_RECEIVE_ANSWER)) handleReceivedAnswer(intent); - else if (intent.getAction().equals(ACTION_RECEIVE_ICE_CANDIDATES)) handleReceivedIceCandidates(intent); - else if (intent.getAction().equals(ACTION_RECEIVE_HANGUP)) handleReceivedHangup(intent); - else if (intent.getAction().equals(ACTION_REMOTE_VIDEO_ENABLE)) handleRemoteVideoEnable(intent); - else if (intent.getAction().equals(ACTION_SET_ENABLE_VIDEO)) handleSetEnableVideo(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_HANGUP)) handleEndedRemoteHangup(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_HANGUP_ACCEPTED)) handleEndedRemoteHangupAccepted(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_HANGUP_BUSY)) handleEndedRemoteHangupBusy(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_HANGUP_DECLINED)) handleEndedRemoteHangupDeclined(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_BUSY)) handleEndedRemoteBusy(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_HANGUP_NEED_PERMISSION)) handleEndedRemoteNeedPermission(intent); - else if (intent.getAction().equals(ACTION_ENDED_REMOTE_GLARE)) handleEndedRemoteGlare(intent); - else if (intent.getAction().equals(ACTION_ENDED_TIMEOUT)) handleEndedTimeout(intent); - else if (intent.getAction().equals(ACTION_ENDED_INTERNAL_FAILURE)) handleEndedInternalFailure(intent); - else if (intent.getAction().equals(ACTION_ENDED_SIGNALING_FAILURE)) handleEndedSignalingFailure(intent); - else if (intent.getAction().equals(ACTION_ENDED_CONNECTION_FAILURE)) handleEndedConnectionFailure(intent); - else if (intent.getAction().equals(ACTION_RECEIVED_OFFER_EXPIRED)) handleReceivedOfferExpired(intent); - else if (intent.getAction().equals(ACTION_RECEIVED_OFFER_WHILE_ACTIVE)) handleReceivedOfferWhileActive(intent); - else if (intent.getAction().equals(ACTION_CALL_CONCLUDED)) handleCallConcluded(intent); + Log.d(TAG, "action: " + intent.getAction() + " action handler: " + serviceState.getActionProcessor().getTag()); + try { + WebRtcServiceState previous = serviceState; + serviceState = serviceState.getActionProcessor().processAction(intent.getAction(), intent, serviceState); + + if (previous != serviceState) { + if (serviceState.getCallInfoState().getCallState() != WebRtcViewModel.State.IDLE) { + sendMessage(); + } + } + } catch (AssertionError e) { + throw new AssertionError("Invalid state for action: " + intent.getAction() + " processor: " + serviceState.getActionProcessor().getTag(), e); + } }); return START_NOT_STICKY; @@ -330,30 +285,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer, @Override public void onCameraSwitchCompleted(@NonNull CameraState newCameraState) { - localCameraState = newCameraState; - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } else if (preJoinPeer != null) { - sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } + Intent intent = new Intent(this, WebRtcCallService.class); + intent.setAction(ACTION_CAMERA_SWITCH_COMPLETED) + .putExtra(EXTRA_CAMERA_STATE, newCameraState); - private void initializeResources() { - this.messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - this.accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - this.lockManager = new LockManager(this); - this.audioManager = new SignalAudioManager(this); - this.bluetoothStateManager = new BluetoothStateManager(this, this); - this.peerMap = new SparseArray<>(); - - this.messageSender.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); - this.accountManager.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); - - try { - this.callManager = CallManager.createCallManager(this); - } catch (CallException e) { - callFailure("Unable to create Call Manager: ", e); - } + startService(intent); } private void registerUncaughtExceptionHandler() { @@ -375,7 +311,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, registerReceiver(wiredHeadsetStateReceiver, new IntentFilter(action)); } - private void registerPowerButtonReceiver() { + public void registerPowerButtonReceiver() { if (powerButtonReceiver == null) { powerButtonReceiver = new PowerButtonReceiver(); @@ -383,7 +319,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - private void unregisterPowerButtonReceiver() { + public void unregisterPowerButtonReceiver() { if (powerButtonReceiver != null) { unregisterReceiver(powerButtonReceiver); @@ -391,1080 +327,57 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - private void handleReceivedOffer(Intent intent) { - CallId callId = getCallId(intent); - RemotePeer remotePeer = getRemotePeer(intent); - byte[] remoteIdentityKey = intent.getByteArrayExtra(EXTRA_REMOTE_IDENTITY_KEY); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - byte[] opaque = intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE); - String sdp = intent.getStringExtra(EXTRA_OFFER_SDP); - long serverReceivedTimestamp = intent.getLongExtra(EXTRA_SERVER_RECEIVED_TIMESTAMP, -1); - long serverDeliveredTimestamp = intent.getLongExtra(EXTRA_SERVER_DELIVERED_TIMESTAMP, -1); - OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); - boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false); - - Log.i(TAG, "handleReceivedOffer(): id: " + callId.format(remoteDevice)); - - if (TelephonyUtil.isAnyPstnLineBusy(this)) { - Log.i(TAG, "PSTN line is busy."); - intent.putExtra(EXTRA_BROADCAST, true); - handleSendBusy(intent); - insertMissedCall(remotePeer, true, serverReceivedTimestamp); - return; - } - - if (remotePeer.getRecipient() == null || !RecipientUtil.isCallRequestAccepted(getApplicationContext(), remotePeer.getRecipient())) { - Log.w(TAG, "Caller is untrusted."); - intent.putExtra(EXTRA_BROADCAST, true); - intent.putExtra(EXTRA_HANGUP_TYPE, HangupMessage.Type.NEED_PERMISSION.getCode()); - handleSendHangup(intent); - insertMissedCall(remotePeer, true, serverReceivedTimestamp); - return; - } - - peerMap.append(remotePeer.hashCode(), remotePeer); - remotePeer.setCallStartTimestamp(serverReceivedTimestamp); - Log.i(TAG, "add remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); - - isRemoteVideoOffer = offerType == OfferMessage.Type.VIDEO_CALL; - - CallManager.CallMediaType callType = getCallMediaTypeFromOfferType(offerType); - - long messageAgeSec = Math.max(serverDeliveredTimestamp - serverReceivedTimestamp, 0) / 1000; - Log.i(TAG, "messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + serverReceivedTimestamp + ", serverDeliveredTimestamp: " + serverDeliveredTimestamp); - - try { - remoteIdentityKey = getPublicKeyBytes(remoteIdentityKey); - - byte[] localIdentityKey = getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(this).serialize()); - - callManager.receivedOffer(callId, remotePeer, remoteDevice, opaque, sdp, messageAgeSec, callType, 1, isMultiRing, true, remoteIdentityKey, localIdentityKey); - } catch (CallException | InvalidKeyException e) { - callFailure("Unable to process received offer: ", e); - } - } - - private void handlePreJoinCall(Intent intent) { - Log.i(TAG, "handlePreJoinCall():"); - - RemotePeer remotePeer = getRemotePeer(intent); - - if (remotePeer.getState() != CallState.IDLE) { - throw new IllegalStateException("Dialing from non-idle?"); - } - - preJoinPeer = remotePeer; - - initializeVideo(); - - localCameraState = initializeVanityCamera(); - - EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); - - sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, - remotePeer, - localCameraState, - bluetoothAvailable, - microphoneEnabled, - false); - } - - private @NonNull CameraState initializeVanityCamera() { - if (camera == null || localSink == null) { - return CameraState.UNKNOWN; - } - - if (camera.hasCapturer()) { - camera.initCapturer(new CapturerObserver() { - @Override - public void onFrameCaptured(VideoFrame videoFrame) { - localSink.onFrame(videoFrame); - } - - @Override - public void onCapturerStarted(boolean success) {} - - @Override - public void onCapturerStopped() {} - }); - camera.setEnabled(true); - } - return camera.getCameraState(); - } - - private void handleCancelPreJoinCall() { - cleanupVideo(); - enableVideoOnCreate = false; - preJoinPeer = null; - - EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); - } - - private void handleOutgoingCall(Intent intent) { - Log.i(TAG, "handleOutgoingCall():"); - - RemotePeer remotePeer = getRemotePeer(intent); - - if (remotePeer.getState() != CallState.IDLE) { - throw new IllegalStateException("Dialing from non-idle?"); - } - - preJoinPeer = null; - - EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); - - peerMap.append(remotePeer.hashCode(), remotePeer); - remotePeer.setCallStartTimestamp(System.currentTimeMillis()); - Log.i(TAG, "add remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); - - initializeVideo(); - - remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote( - remotePeer.getRecipient(), - null, - new BroadcastVideoSink(eglBase), - false - )); - - OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); - CallManager.CallMediaType callMediaType = getCallMediaTypeFromOfferType(offerType); - - try { - callManager.call(remotePeer, callMediaType, 1); - } catch (CallException e) { - callFailure("Unable to create outgoing call: ", e); - } - } - - private void handleIsInCallQuery(Intent intent) { - ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); - - if (resultReceiver != null) { - resultReceiver.send(activePeer != null ? 1 : 0, null); - } - } - - private void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal, long timestamp) { + public void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal, long timestamp) { Pair messageAndThreadId = DatabaseFactory.getSmsDatabase(this).insertMissedCall(remotePeer.getId(), timestamp); ApplicationDependencies.getMessageNotifier().updateNotification(this, messageAndThreadId.second(), signal); } - private void handleDenyCall(Intent intent) { - if (activePeer == null) { - Log.i(TAG, "handleDenyCall(): Ignoring for inactive call."); - return; - } + public void retrieveTurnServers(@NonNull RemotePeer remotePeer) { + retrieveTurnServers().addListener(new SuccessOnlyListener(remotePeer.getState(), remotePeer.getCallId()) { + @Override + public void onSuccessContinue(@Nullable TurnServerInfoParcel turnServerInfoParcel) { + boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); - if (activePeer.getState() != CallState.LOCAL_RINGING) { - Log.w(TAG, "Can only deny from ringing!"); - return; - } + Intent intent = new Intent(WebRtcCallService.this, WebRtcCallService.class); + intent.setAction(ACTION_TURN_SERVER_UPDATE) + .putExtra(EXTRA_IS_ALWAYS_TURN, isAlwaysTurn) + .putExtra(EXTRA_TURN_SERVER_INFO, turnServerInfoParcel); - Log.i(TAG, "handleDenyCall():"); - - try { - callManager.hangup(); - DatabaseFactory.getSmsDatabase(this).insertMissedCall(activePeer.getId(), System.currentTimeMillis()); - terminate(activePeer); - } catch (CallException e) { - callFailure("hangup() failed: ", e); - } - } - - private void handleSetSpeakerAudio(Intent intent) { - boolean isSpeaker = intent.getBooleanExtra(EXTRA_SPEAKER, false); - AudioManager audioManager = ServiceUtil.getAudioManager(this); - - bluetoothStateManager.setWantsConnection(false); - audioManager.setSpeakerphoneOn(isSpeaker); - - if (!localCameraState.isEnabled()) { - lockManager.updatePhoneState(getInCallPhoneState()); - } - - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - - private void handleSetBluetoothAudio(Intent intent) { - boolean isBluetooth = intent.getBooleanExtra(EXTRA_BLUETOOTH, false); - - bluetoothStateManager.setWantsConnection(isBluetooth); - - if (!localCameraState.isEnabled()) { - lockManager.updatePhoneState(getInCallPhoneState()); - } - - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - - private void handleSetMuteAudio(Intent intent) { - boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false); - microphoneEnabled = !muted; - - if (activePeer == null) { - Log.w(TAG, "handleSetMuteAudio(): Ignoring for inactive call."); - return; - } - - if (activePeer.getState() == CallState.CONNECTED) { - try { - callManager.setAudioEnable(microphoneEnabled); - } catch (CallException e) { - callFailure("Enabling audio failed: ", e); + WebRtcCallService.this.startService(intent); } - } - - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - - private void handleSetCameraFlip(Intent intent) { - Log.i(TAG, "handleSetCameraFlip()..."); - - if (localCameraState.isEnabled() && camera != null) { - camera.flip(); - localCameraState = camera.getCameraState(); - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } else if (preJoinPeer != null) { - sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - } - - private void handleBluetoothChange(Intent intent) { - bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false); - - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - - private void handleWiredHeadsetChange(Intent intent) { - Log.i(TAG, "handleWiredHeadsetChange..."); - - if ((activePeer != null) && - (activePeer.getState() == CallState.CONNECTED || - activePeer.getState() == CallState.DIALING || - activePeer.getState() == CallState.RECEIVED_BUSY || - activePeer.getState() == CallState.REMOTE_RINGING)) - { - AudioManager audioManager = ServiceUtil.getAudioManager(this); - boolean present = intent.getBooleanExtra(EXTRA_AVAILABLE, false); - - if (present && audioManager.isSpeakerphoneOn()) { - audioManager.setSpeakerphoneOn(false); - audioManager.setBluetoothScoOn(false); - } else if (!present && !audioManager.isSpeakerphoneOn() && !audioManager.isBluetoothScoOn() && localCameraState.isEnabled()) { - audioManager.setSpeakerphoneOn(true); - } - - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - - private void handleScreenOffChange(Intent intent) { - if ((activePeer != null) && - (activePeer.getState() == CallState.ANSWERING || - activePeer.getState() == CallState.LOCAL_RINGING)) - { - Log.i(TAG, "Silencing incoming ringer..."); - audioManager.silenceIncomingRinger(); - } - } - - private void handleStartOutgoingCall(Intent intent) { - Log.i(TAG, "handleStartOutgoingCall():"); - - if (activePeer != null) { - throw new IllegalStateException("handleStartOutgoingCall(): activePeer already set"); - } - - RemotePeer remotePeer = getRemotePeerFromMap(intent); - activePeer = remotePeer; - activePeer.dialing(); - Log.i(TAG, "assign activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); - - AudioManager androidAudioManager = ServiceUtil.getAudioManager(this); - androidAudioManager.setSpeakerphoneOn(false); - - sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - lockManager.updatePhoneState(getInCallPhoneState()); - audioManager.initializeAudioForCall(); - audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING); - bluetoothStateManager.setWantsConnection(true); - - setCallInProgressNotification(TYPE_OUTGOING_RINGING, activePeer); - - DatabaseFactory.getSmsDatabase(this).insertOutgoingCall(activePeer.getId()); - - retrieveTurnServers().addListener(new SuccessOnlyListener>(this.activePeer.getState(), this.activePeer.getCallId()) { - @Override - public void onSuccessContinue(List iceServers) { - - boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); - - try { - callManager.proceed(activePeer.getCallId(), - WebRtcCallService.this, - eglBase, - localSink, - remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(), - camera, - iceServers, - isAlwaysTurn, - enableVideoOnCreate); - } catch (CallException e) { - callFailure("Unable to proceed with call: ", e); - } - - localCameraState = camera.getCameraState(); - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - }); - } - - private void handleStartIncomingCall(Intent intent) { - Log.i(TAG, "handleStartIncomingCall():"); - - if (activePeer != null) { - throw new IllegalStateException("handleStartIncomingCall(): activePeer already set"); - } - - RemotePeer remotePeer = getRemotePeerFromMap(intent); - activePeer = remotePeer; - activePeer.answering(); - Log.i(TAG, "assign activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); - - AudioManager androidAudioManager = ServiceUtil.getAudioManager(this); - androidAudioManager.setSpeakerphoneOn(false); - - initializeVideo(); - - remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote( - remotePeer.getRecipient(), - null, - new BroadcastVideoSink(eglBase), - false - )); - - setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer); - - retrieveTurnServers().addListener(new SuccessOnlyListener>(this.activePeer.getState(), this.activePeer.getCallId()) { - @Override - public void onSuccessContinue(List iceServers) { - boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); - boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn; - - try { - callManager.proceed(activePeer.getCallId(), - WebRtcCallService.this, - eglBase, - localSink, - remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(), - camera, - iceServers, - hideIp, - false); - } catch (CallException e) { - callFailure("Unable to proceed with call: ", e); - } - - lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); - if (activePeer != null) { - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - }); - } - - private void handleAcceptCall(Intent intent) { - if (activePeer != null && activePeer.getState() != CallState.LOCAL_RINGING) { - Log.w(TAG, "handleAcceptCall(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId()); - - DatabaseFactory.getSmsDatabase(this).insertReceivedCall(activePeer.getId()); - - acceptWithVideo = intent.getBooleanExtra(EXTRA_ANSWER_WITH_VIDEO, false); - - try { - callManager.acceptCall(activePeer.getCallId()); - } catch (CallException e) { - callFailure("accept() failed: ", e); - } - } - - private void handleSendOffer(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); - byte[] opaque = intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE); - String sdp = intent.getStringExtra(EXTRA_OFFER_SDP); - OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); - - Log.i(TAG, "handleSendOffer(): id: " + callId.format(remoteDevice)); - - OfferMessage offerMessage = new OfferMessage(callId.longValue(), sdp, offerType, opaque); - Integer destinationDeviceId = broadcast ? null : remoteDevice; - SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage, true, destinationDeviceId); - - sendCallMessage(remotePeer, callMessage); - } - - private void handleSendAnswer(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); - byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE); - String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP); - - Log.i(TAG, "handleSendAnswer(): id: " + callId.format(remoteDevice)); - - AnswerMessage answerMessage = new AnswerMessage(callId.longValue(), sdp, opaque); - Integer destinationDeviceId = broadcast ? null : remoteDevice; - SignalServiceCallMessage callMessage = SignalServiceCallMessage.forAnswer(answerMessage, true, destinationDeviceId); - - sendCallMessage(remotePeer, callMessage); - } - - private void handleSendIceCandidates(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); - ArrayList iceCandidates = intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES); - - Log.i(TAG, "handleSendIceCandidates(): id: " + callId.format(remoteDevice)); - - LinkedList iceUpdateMessages = new LinkedList(); - for (IceCandidateParcel parcel : iceCandidates) { - iceUpdateMessages.add(parcel.getIceUpdateMessage(callId)); - } - - Integer destinationDeviceId = broadcast ? null : remoteDevice; - SignalServiceCallMessage callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdateMessages, true, destinationDeviceId); - - sendCallMessage(remotePeer, callMessage); - } - - private void handleSendHangup(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); - HangupMessage.Type type = HangupMessage.Type.fromCode(intent.getStringExtra(EXTRA_HANGUP_TYPE)); - boolean isLegacy = intent.getBooleanExtra(EXTRA_HANGUP_IS_LEGACY, true); - int deviceId = intent.getIntExtra(EXTRA_HANGUP_DEVICE_ID, 0); - - Log.i(TAG, "handleSendHangup(): id: " + callId.format(remoteDevice)); - - HangupMessage hangupMessage = new HangupMessage(callId.longValue(), type, deviceId, isLegacy); - Integer destinationDeviceId = broadcast ? null : remoteDevice; - SignalServiceCallMessage callMessage = SignalServiceCallMessage.forHangup(hangupMessage, true, destinationDeviceId); - - sendCallMessage(remotePeer, callMessage); - } - - private void handleSendBusy(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); - - Log.i(TAG, "handleSendBusy(): id: " + callId.format(remoteDevice)); - - BusyMessage busyMessage = new BusyMessage(callId.longValue()); - Integer destinationDeviceId = broadcast ? null : remoteDevice; - SignalServiceCallMessage callMessage = SignalServiceCallMessage.forBusy(busyMessage, true, destinationDeviceId); - - sendCallMessage(remotePeer, callMessage); - } - - private void handleReceivedAnswer(Intent intent) { - CallId callId = getCallId(intent); - byte[] remoteIdentityKey = intent.getByteArrayExtra(EXTRA_REMOTE_IDENTITY_KEY); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE); - String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP); - boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false); - - Log.i(TAG, "handleReceivedAnswer(): id: " + callId.format(remoteDevice)); - - try { - remoteIdentityKey = getPublicKeyBytes(remoteIdentityKey); - - byte[] localIdentityKey = getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(this).serialize()); - - callManager.receivedAnswer(callId, remoteDevice, opaque, sdp, isMultiRing, remoteIdentityKey, localIdentityKey); - } catch (CallException | InvalidKeyException e) { - callFailure("receivedAnswer() failed: ", e); - } - } - - private void handleReceivedIceCandidates(Intent intent) { - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - ArrayList iceCandidateParcels = intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES); - - Log.i(TAG, "handleReceivedIceCandidates(): id: " + callId.format(remoteDevice) + ", count: " + iceCandidateParcels.size()); - - LinkedList iceCandidates = new LinkedList(); - for (IceCandidateParcel parcel : iceCandidateParcels) { - iceCandidates.add(parcel.getIceCandidate()); - } - - try { - callManager.receivedIceCandidates(callId, remoteDevice, iceCandidates); - } catch (CallException e) { - callFailure("receivedIceCandidates() failed: ", e); - } - } - - private void handleReceivedHangup(Intent intent) { - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - HangupMessage.Type hangupType = HangupMessage.Type.fromCode(intent.getStringExtra(EXTRA_HANGUP_TYPE)); - Integer deviceId = intent.getIntExtra(EXTRA_HANGUP_DEVICE_ID, 0); - CallManager.HangupType callHangupType = getCallHangupTypeFromHangupType(hangupType); - - Log.i(TAG, "handleReceivedHangup(): id: " + callId.format(remoteDevice)); - - try { - callManager.receivedHangup(callId, remoteDevice, callHangupType, deviceId); - } catch (CallException e) { - callFailure("receivedHangup() failed: ", e); - } - } - - private void handleReceivedBusy(Intent intent) { - CallId callId = getCallId(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - - Log.i(TAG, "handleReceivedBusy(): id: " + callId.format(remoteDevice)); - - try { - callManager.receivedBusy(callId, remoteDevice); - } catch (CallException e) { - callFailure("receivedBusy() failed: ", e); - } - } - - private void handleLocalRinging(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - Recipient recipient = remotePeer.getRecipient(); - - if (!remotePeer.callIdEquals(activePeer)) { - Log.w(TAG, "handleLocalRinging(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId()); - - activePeer.localRinging(); - lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE); - - sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(getApplicationContext(), recipient); - if (shouldDisturbUserWithCall) { - startCallCardActivityIfPossible(); - } - - audioManager.initializeAudioForCall(); - if (shouldDisturbUserWithCall && TextSecurePreferences.isCallNotificationsEnabled(this)) { - Uri ringtone = recipient.resolve().getCallRingtone(); - VibrateState vibrateState = recipient.resolve().getCallVibrate(); - - if (ringtone == null) ringtone = TextSecurePreferences.getCallNotificationRingtone(this); - - audioManager.startIncomingRinger(ringtone, vibrateState == VibrateState.ENABLED || (vibrateState == VibrateState.DEFAULT && TextSecurePreferences.isCallNotificationVibrateEnabled(this))); - } - - registerPowerButtonReceiver(); - setCallInProgressNotification(TYPE_INCOMING_RINGING, activePeer); - } - - private void handleRemoteRinging(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - if (!remotePeer.callIdEquals(activePeer)) { - Log.w(TAG, "handleRemoteRinging(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId()); - - activePeer.remoteRinging(); - sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - private void handleCallConnected(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - if (!remotePeer.callIdEquals(activePeer)) { - Log.w(TAG, "handleCallConnected(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleCallConnected(): call_id: " + remotePeer.getCallId()); - - audioManager.startCommunication(activePeer.getState() == CallState.REMOTE_RINGING); - bluetoothStateManager.setWantsConnection(true); - - activePeer.connected(); - - if (localCameraState.isEnabled()) { - lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); - } else { - lockManager.updatePhoneState(getInCallPhoneState()); - } - - callConnectedTime = System.currentTimeMillis(); - - sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - - unregisterPowerButtonReceiver(); - - setCallInProgressNotification(TYPE_ESTABLISHED, activePeer); - - try { - callManager.setCommunicationMode(); - callManager.setAudioEnable(microphoneEnabled); - callManager.setVideoEnable(localCameraState.isEnabled()); - } catch (CallException e) { - callFailure("Enabling audio/video failed: ", e); - } - - if (acceptWithVideo) { - handleSetEnableVideo(new Intent().putExtra(EXTRA_ENABLE, true)); - } - } - - private void handleRemoteVideoEnable(Intent intent) { - Boolean enable = intent.getBooleanExtra(EXTRA_ENABLE, false); - - if (activePeer == null) { - Log.w(TAG, "handleRemoteVideoEnable(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId()); - - CallParticipant oldParticipant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient())); - CallParticipant newParticipant = oldParticipant.withVideoEnabled(enable); - remoteParticipantMap.put(activePeer.getRecipient(), newParticipant); - - sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - - } - - private void handleSetEnableVideo(Intent intent) { - boolean enable = intent.getBooleanExtra(EXTRA_ENABLE, false); - AudioManager audioManager = ServiceUtil.getAudioManager(this); - - if (activePeer == null) { - if (preJoinPeer != null) { - Log.w(TAG, "handleSetEnableVideo(): Changing for pre-join call."); - camera.setEnabled(enable); - enableVideoOnCreate = enable; - localCameraState = camera.getCameraState(); - sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } else { - Log.w(TAG, "handleSetEnableVideo(): Ignoring for inactive call."); - } - return; - } - - Log.i(TAG, "handleSetEnableVideo(): call_id: " + activePeer.getCallId()); - - if (activePeer.getState() != CallState.CONNECTED) { - enableVideoOnCreate = enable; - - if (enableVideoOnCreate && - !audioManager.isSpeakerphoneOn() && - !audioManager.isBluetoothScoOn() && - !audioManager.isWiredHeadsetOn()) - { - audioManager.setSpeakerphoneOn(true); - } - - return; - } - - try { - callManager.setVideoEnable(enable); - } catch (CallException e) { - callFailure("setVideoEnable() failed: ", e); - return; - } - - localCameraState = camera.getCameraState(); - - if (localCameraState.isEnabled()) { - lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); - } else { - lockManager.updatePhoneState(getInCallPhoneState()); - } - - if (localCameraState.isEnabled() && - !audioManager.isSpeakerphoneOn() && - !audioManager.isBluetoothScoOn() && - !audioManager.isWiredHeadsetOn()) - { - audioManager.setSpeakerphoneOn(true); - } - - sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - private void handleLocalHangup(Intent intent) { - if (activePeer == null) { - if (busyPeer != null) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, busyPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - busyPeer = null; - } - - Log.w(TAG, "handleLocalHangup(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleLocalHangup(): call_id: " + activePeer.getCallId()); - - accountManager.cancelInFlightRequests(); - messageSender.cancelInFlightRequests(); - - try { - callManager.hangup(); - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - terminate(activePeer); - } catch (CallException e) { - callFailure("hangup() failed: ", e); - } - } - - private void handleReceivedOfferExpired(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleReceivedOfferExpired(): call_id: " + remotePeer.getCallId()); - - insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp()); - - terminate(remotePeer); - } - - private void handleReceivedOfferWhileActive(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - if (activePeer == null) { - Log.w(TAG, "handleReceivedOfferWhileActive(): Ignoring for inactive call."); - return; - } - - Log.i(TAG, "handleReceivedOfferWhileActive(): call_id: " + remotePeer.getCallId()); - - switch (activePeer.getState()) { - case DIALING: - case REMOTE_RINGING: setCallInProgressNotification(TYPE_OUTGOING_RINGING, activePeer); break; - case IDLE: setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer); break; - case ANSWERING: setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer); break; - case LOCAL_RINGING: setCallInProgressNotification(TYPE_INCOMING_RINGING, activePeer); break; - case CONNECTED: setCallInProgressNotification(TYPE_ESTABLISHED, activePeer); break; - default: throw new IllegalStateException(); - } - - if (activePeer.getState() == CallState.IDLE) { - stopForeground(true); - } - - insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp()); - - terminate(remotePeer); - } - - private void handleEndedRemoteHangup(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteHangup(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING; - if (outgoingBeforeAccept) { - sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } else { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - } - - boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING; - if (incomingBeforeAccept) { - insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp()); - } - - terminate(remotePeer); - } - - private void handleEndedRemoteHangupAccepted(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteHangupAccepted(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - terminate(remotePeer); - } - - private void handleEndedRemoteHangupBusy(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteHangupBusy(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - terminate(remotePeer); - } - - private void handleEndedRemoteHangupDeclined(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteHangupDeclined(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - terminate(remotePeer); - } - - private void handleEndedRemoteBusy(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteBusy(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - activePeer.receivedBusy(); - busyPeer = activePeer; - - OutgoingRinger ringer = new OutgoingRinger(this); - ringer.start(OutgoingRinger.Type.BUSY); - Util.runOnMainDelayed(() -> { - ringer.stop(); - busyPeer = null; - }, BUSY_TONE_LENGTH); - - sendMessage(WebRtcViewModel.State.CALL_BUSY, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - terminate(remotePeer); - } - - private void handleEndedRemoteNeedPermission(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteNeedPermission(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_NEEDS_PERMISSION, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - terminate(remotePeer); - } - - private void handleEndedRemoteGlare(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedRemoteGlare(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING; - if (incomingBeforeAccept) { - insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp()); - } - - terminate(remotePeer); - } - - private void handleEndedFailure(Intent intent) { - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "handleEndedFailure(): call_id: " + remotePeer.getCallId()); - - if (remotePeer.callIdEquals(activePeer)) { - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) { - insertMissedCall(remotePeer, true, remotePeer.getCallStartTimestamp()); - } - - terminate(remotePeer); - } - - private void handleEndedTimeout(Intent intent) { - Log.i(TAG, "handleEndedTimeout():"); - - handleEndedFailure(intent); - } - - private void handleEndedInternalFailure(Intent intent) { - Log.i(TAG, "handleEndedInternalFailure():"); - - handleEndedFailure(intent); - } - - private void handleEndedSignalingFailure(Intent intent) { - Log.i(TAG, "handleEndedSignalingFailure():"); - - handleEndedFailure(intent); - } - - private void handleEndedConnectionFailure(Intent intent) { - Log.i(TAG, "handleEndedConnectionFailure():"); - - handleEndedFailure(intent); - } - - private void handleCallConcluded(Intent intent) { - Log.i(TAG, "handleCallConcluded():"); - - RemotePeer remotePeer = getRemotePeerFromMap(intent); - - Log.i(TAG, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); - - peerMap.delete(remotePeer.hashCode()); - } - - private boolean isIdle() { - return activePeer == null; - } - - private static byte[] getPublicKeyBytes(byte[] identityKey) throws InvalidKeyException { - ECPublicKey key = Curve.decodePoint(identityKey, 0); - - if (key instanceof DjbECPublicKey) { - return ((DjbECPublicKey) key).getPublicKey(); - } - throw new InvalidKeyException(); - } - - private void initializeVideo() { - Util.runOnMainSync(() -> { - if (eglBase == null) { - eglBase = EglBase.create(); - localSink = new BroadcastVideoSink(eglBase); - } - - if (camera != null) { - camera.setEnabled(false); - camera.dispose(); - } - - camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase, localCameraState.getActiveDirection()); - localCameraState = camera.getCameraState(); }); } - private void cleanupVideo() { - if (camera != null) { - camera.dispose(); - camera = null; - } - - if (eglBase != null) { - eglBase.release(); - eglBase = null; - } - - localCameraState = CameraState.UNKNOWN; - } - - private void setCallInProgressNotification(int type, RemotePeer remotePeer) { + public void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) { startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type), CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient())); } - private synchronized void terminate(RemotePeer remotePeer) { - Log.i(TAG, "terminate():"); - - if (activePeer == null) { - Log.i(TAG, "skipping with no active peer"); - return; - } - - if (!remotePeer.callIdEquals(activePeer)) { - Log.i(TAG, "skipping remotePeer is not active peer"); - return; - } - - lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); - stopForeground(true); - boolean playDisconnectSound = (activePeer.getState() == CallState.DIALING) || - (activePeer.getState() == CallState.REMOTE_RINGING) || - (activePeer.getState() == CallState.RECEIVED_BUSY) || - (activePeer.getState() == CallState.CONNECTED); - audioManager.stop(playDisconnectSound); - bluetoothStateManager.setWantsConnection(false); - - cleanupVideo(); - - this.microphoneEnabled = true; - this.enableVideoOnCreate = false; - - Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); - this.activePeer = null; - - remoteParticipantMap.clear(); - - lockManager.updatePhoneState(LockManager.PhoneState.IDLE); + public void sendMessage() { + sendMessage(serviceState); } - private void sendMessage(@NonNull WebRtcViewModel.State state, - @NonNull RemotePeer remotePeer, - @NonNull CameraState localCameraState, - boolean bluetoothAvailable, - boolean microphoneEnabled, - boolean isRemoteVideoOffer) - { - EventBus.getDefault().postSticky(new WebRtcViewModel(state, - remotePeer.getRecipient(), - localCameraState, - localSink, - bluetoothAvailable, - microphoneEnabled, - isRemoteVideoOffer, - callConnectedTime, - new ArrayList<>(remoteParticipantMap.values()))); + public void sendMessage(@NonNull WebRtcServiceState state) { + EventBus.getDefault().postSticky(new WebRtcViewModel(state.getCallInfoState().getCallState(), + state.getCallInfoState().getCallRecipient(), + state.getLocalDeviceState().getCameraState(), + state.getVideoState().getLocalSink(), + state.getLocalDeviceState().isBluetoothAvailable(), + state.getLocalDeviceState().isMicrophoneEnabled(), + state.getCallSetupState().isRemoteVideoOffer(), + state.getCallInfoState().getCallConnectedTime(), + state.getCallInfoState().getRemoteCallParticipants())); } - private ListenableFutureTask sendMessage(@NonNull final RemotePeer remotePeer, - @NonNull final SignalServiceCallMessage callMessage) + private @NonNull ListenableFutureTask sendMessage(@NonNull final RemotePeer remotePeer, + @NonNull final SignalServiceCallMessage callMessage) { - Callable callable = new Callable() { - @Override - public Boolean call() throws Exception { - Recipient recipient = remotePeer.getRecipient(); - messageSender.sendCallMessage(RecipientUtil.toSignalServiceAddress(WebRtcCallService.this, recipient), - UnidentifiedAccessUtil.getAccessFor(WebRtcCallService.this, recipient), - callMessage); - return true; - } + Callable callable = () -> { + Recipient recipient = remotePeer.getRecipient(); + messageSender.sendCallMessage(RecipientUtil.toSignalServiceAddress(WebRtcCallService.this, recipient), + UnidentifiedAccessUtil.getAccessFor(WebRtcCallService.this, recipient), + callMessage); + return true; }; ListenableFutureTask listenableFutureTask = new ListenableFutureTask<>(callable, null, serviceExecutor); @@ -1473,7 +386,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, return listenableFutureTask; } - private void startCallCardActivityIfPossible() { + public void startCallCardActivityIfPossible() { if (Build.VERSION.SDK_INT >= 29 && !ApplicationContext.getInstance(getApplicationContext()).isAppVisible()) { return; } @@ -1481,88 +394,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Intent activityIntent = new Intent(); activityIntent.setClass(this, WebRtcCallActivity.class); activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - this.startActivity(activityIntent); - } - - private static @NonNull CallId getCallId(Intent intent) { - return new CallId(intent.getLongExtra(EXTRA_CALL_ID, -1)); - } - - private static @NonNull RemotePeer getRemotePeer(Intent intent) { - RemotePeer remotePeer = intent.getParcelableExtra(EXTRA_REMOTE_PEER); - if (remotePeer == null) throw new AssertionError("No RemotePeer in intent!"); - - return remotePeer; - } - - private static @NonNull int getRemotePeerKey(Intent intent) { - if (!intent.getExtras().containsKey(EXTRA_REMOTE_PEER_KEY)) { - throw new AssertionError("No RemotePeer key in intent!"); - } - - // The default of -1 should never be applied since the key exists. - int remotePeerKey = intent.getIntExtra(EXTRA_REMOTE_PEER_KEY, -1); - - return remotePeerKey; - } - - private @NonNull RemotePeer getRemotePeerFromMap(Intent intent) { - int remotePeerKey = getRemotePeerKey(intent); - RemotePeer remotePeer = peerMap.get(remotePeerKey); - if (remotePeer == null) { - throw new AssertionError("No RemotePeer in map for key: " + remotePeerKey + "!"); - } - - return remotePeer; - } - - private void callFailure(String message, Throwable error) { - Log.w(TAG, "callFailure(): " + message, error); - - if (activePeer != null) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } - - if (callManager != null) { - try { - callManager.reset(); - } catch (CallException e) { - Log.w(TAG, "Unable to reset call manager: ", e); - } - } else { - Log.w(TAG, "No call manager, not reseting. Error message: " + message , error); - } - - terminate(activePeer); - - peerMap.clear(); - } - - private static @NonNull CallManager.CallMediaType getCallMediaTypeFromOfferType(@NonNull OfferMessage.Type offerType) { - return offerType == OfferMessage.Type.VIDEO_CALL ? CallManager.CallMediaType.VIDEO_CALL : CallManager.CallMediaType.AUDIO_CALL; + startActivity(activityIntent); } private static @NonNull OfferMessage.Type getOfferTypeFromCallMediaType(@NonNull CallManager.CallMediaType mediaType) { return mediaType == CallManager.CallMediaType.VIDEO_CALL ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL; } - private static @NonNull CallManager.HangupType getCallHangupTypeFromHangupType(@NonNull HangupMessage.Type hangupType) { - switch (hangupType) { - 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: " + hangupType); - } - } - private static @NonNull HangupMessage.Type getHangupTypeFromCallHangupType(@NonNull CallManager.HangupType hangupType) { switch (hangupType) { case ACCEPTED: @@ -1580,32 +418,18 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - @Nullable @Override - public IBinder onBind(Intent intent) { + public @Nullable IBinder onBind(@NonNull Intent intent) { return null; } - private WebRtcViewModel.State viewModelStateFor(@NonNull RemotePeer remotePeer) { - switch (remotePeer.getState()) { - case CONNECTED: return WebRtcViewModel.State.CALL_CONNECTED; - case DIALING: return WebRtcViewModel.State.CALL_OUTGOING; - case REMOTE_RINGING: return WebRtcViewModel.State.CALL_RINGING; - case LOCAL_RINGING: return WebRtcViewModel.State.CALL_INCOMING; - case ANSWERING: return WebRtcViewModel.State.CALL_INCOMING; - case IDLE: return WebRtcViewModel.State.CALL_DISCONNECTED; - } - - return WebRtcViewModel.State.CALL_DISCONNECTED; - } - private static class WiredHeadsetStateReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(@NonNull Context context, @NonNull Intent intent) { int state = intent.getIntExtra("state", -1); Intent serviceIntent = new Intent(context, WebRtcCallService.class); - serviceIntent.setAction(WebRtcCallService.ACTION_WIRED_HEADSET_CHANGE); + serviceIntent.setAction(ACTION_WIRED_HEADSET_CHANGE); serviceIntent.putExtra(WebRtcCallService.EXTRA_AVAILABLE, state != 0); context.startService(serviceIntent); } @@ -1613,10 +437,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private static class PowerButtonReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(@NonNull Context context, @NonNull Intent intent) { if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { Intent serviceIntent = new Intent(context, WebRtcCallService.class); - serviceIntent.setAction(WebRtcCallService.ACTION_SCREEN_OFF); + serviceIntent.setAction(ACTION_SCREEN_OFF); context.startService(serviceIntent); } } @@ -1625,12 +449,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private static class ProximityLockRelease implements Thread.UncaughtExceptionHandler { private final LockManager lockManager; - private ProximityLockRelease(LockManager lockManager) { + private ProximityLockRelease(@NonNull LockManager lockManager) { this.lockManager = lockManager; } @Override - public void uncaughtException(Thread thread, Throwable throwable) { + public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { Log.i(TAG, "Uncaught exception - releasing proximity lock", throwable); lockManager.updatePhoneState(LockManager.PhoneState.IDLE); } @@ -1646,7 +470,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private class HangUpRtcOnPstnCallAnsweredListener extends PhoneStateListener { @Override - public void onCallStateChanged(int state, String phoneNumber) { + public void onCallStateChanged(int state, @NonNull String phoneNumber) { super.onCallStateChanged(state, phoneNumber); if (state == TelephonyManager.CALL_STATE_OFFHOOK) { hangup(); @@ -1662,30 +486,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - private ListenableFutureTask> retrieveTurnServers() { - Callable> callable = () -> { - LinkedList results = new LinkedList<>(); + private @NonNull ListenableFutureTask retrieveTurnServers() { + Callable callable = () -> new TurnServerInfoParcel(accountManager.getTurnServerInfo()); - results.add(new PeerConnection.IceServer("stun:stun1.l.google.com:19302")); - try { - TurnServerInfo turnServerInfo = accountManager.getTurnServerInfo(); - - for (String url : turnServerInfo.getUrls()) { - Log.i(TAG, "ice_server: " + url); - if (url.startsWith("turn")) { - results.add(new PeerConnection.IceServer(url, turnServerInfo.getUsername(), turnServerInfo.getPassword())); - } else { - results.add(new PeerConnection.IceServer(url)); - } - } - } catch (IOException e) { - Log.w(TAG, e); - } - - return results; - }; - - ListenableFutureTask> futureTask = new ListenableFutureTask<>(callable, null, serviceExecutor); + ListenableFutureTask futureTask = new ListenableFutureTask<>(callable, null, serviceExecutor); networkExecutor.execute(futureTask); return futureTask; @@ -1695,17 +499,17 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private final CallState expectedState; private final CallId expectedCallId; - StateAwareListener(CallState expectedState, CallId expectedCallId) { + StateAwareListener(@NonNull CallState expectedState, @NonNull CallId expectedCallId) { this.expectedState = expectedState; this.expectedCallId = expectedCallId; } - public CallId getCallId() { + public @NonNull CallId getCallId() { return this.expectedCallId; } @Override - public void onSuccess(V result) { + public void onSuccess(@Nullable V result) { if (!isConsistentState()) { Log.i(TAG, "State has changed since request, skipping success callback..."); onStateChangeContinue(); @@ -1715,7 +519,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onFailure(ExecutionException throwable) { + public void onFailure(@NonNull ExecutionException throwable) { if (!isConsistentState()) { Log.w(TAG, throwable); Log.w(TAG, "State has changed since request, skipping failure callback..."); @@ -1727,119 +531,80 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public void onStateChangeContinue() {} + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean isConsistentState() { + RemotePeer activePeer = serviceState.getCallInfoState().getActivePeer(); return activePeer != null && expectedState == activePeer.getState() && expectedCallId.equals(activePeer.getCallId()); } - public abstract void onSuccessContinue(V result); - public abstract void onFailureContinue(Throwable throwable); - } - - private abstract class FailureListener extends StateAwareListener { - FailureListener(CallState expectedState, CallId expectedCallId) { - super(expectedState, expectedCallId); - } - - @Override - public void onSuccessContinue(V result) {} + public abstract void onSuccessContinue(@Nullable V result); + public abstract void onFailureContinue(@Nullable Throwable throwable); } private abstract class SuccessOnlyListener extends StateAwareListener { - SuccessOnlyListener(CallState expectedState, CallId expectedCallId) { + SuccessOnlyListener(@NonNull CallState expectedState, @NonNull CallId expectedCallId) { super(expectedState, expectedCallId); } @Override - public void onFailureContinue(Throwable throwable) { + public void onFailureContinue(@Nullable Throwable throwable) { Log.w(TAG, throwable); throw new AssertionError(throwable); } } private class SendCallMessageListener extends StateAwareListener { - SendCallMessageListener(RemotePeer expectedRemotePeer) { + SendCallMessageListener(@NonNull RemotePeer expectedRemotePeer) { super(expectedRemotePeer.getState(), expectedRemotePeer.getCallId()); } @Override - public void onSuccessContinue(V result) { - if (callManager != null) { - try { - callManager.messageSent(getCallId()); - } catch (CallException e) { - callFailure("callManager.messageSent() failed: ", e); - } - } + public void onSuccessContinue(@Nullable V result) { + Intent intent = new Intent(WebRtcCallService.this, WebRtcCallService.class); + intent.setAction(ACTION_MESSAGE_SENT_SUCCESS); + intent.putExtra(EXTRA_CALL_ID, getCallId().longValue()); + + startService(intent); } @Override public void onStateChangeContinue() { - if (callManager != null) { - try { - callManager.messageSent(getCallId()); - } catch (CallException e) { - callFailure("callManager.messageSent() failed: ", e); - } - } + Intent intent = new Intent(WebRtcCallService.this, WebRtcCallService.class); + intent.setAction(ACTION_MESSAGE_SENT_SUCCESS) + .putExtra(EXTRA_CALL_ID, getCallId().longValue()); + + startService(intent); } @Override - public void onFailureContinue(Throwable error) { - Log.w(TAG, error); + public void onFailureContinue(@Nullable Throwable error) { + Intent intent = new Intent(WebRtcCallService.this, WebRtcCallService.class); + intent.setAction(ACTION_MESSAGE_SENT_ERROR) + .putExtra(EXTRA_CALL_ID, getCallId().longValue()) + .putExtra(EXTRA_ERROR, error); - if (callManager != null) { - try { - callManager.messageSendFailure(getCallId()); - } catch (CallException e) { - callFailure("callManager.messageSendFailure() failed: ", e); - } - } - - if (activePeer == null) { - return; - } - - if (error instanceof UntrustedIdentityException) { - CallParticipant participant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient())); - CallParticipant untrusted = participant.withIdentityKey(((UntrustedIdentityException) error).getIdentityKey()); - - remoteParticipantMap.put(activePeer.getRecipient(), untrusted); - - sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } else if (error instanceof UnregisteredUserException) { - sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } else if (error instanceof IOException) { - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - } + startService(intent); } } - private void sendCallMessage(RemotePeer remotePeer, SignalServiceCallMessage callMessage) { + public void sendCallMessage(@NonNull RemotePeer remotePeer, @NonNull SignalServiceCallMessage callMessage) { ListenableFutureTask listenableFutureTask = sendMessage(remotePeer, callMessage); listenableFutureTask.addListener(new SendCallMessageListener<>(remotePeer)); } - private LockManager.PhoneState getInCallPhoneState() { - AudioManager audioManager = ServiceUtil.getAudioManager(this); - if (audioManager.isSpeakerphoneOn() || audioManager.isBluetoothScoOn() || audioManager.isWiredHeadsetOn()) { - return LockManager.PhoneState.IN_HANDS_FREE_CALL; - } else { - return LockManager.PhoneState.IN_CALL; - } - } - @Override - public void onStartCall(Remote remote, CallId callId, Boolean isOutgoing, CallManager.CallMediaType callMediaType) { + public void onStartCall(@Nullable Remote remote, @NonNull CallId callId, @NonNull Boolean isOutgoing, @Nullable CallManager.CallMediaType callMediaType) { Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType); if (remote instanceof RemotePeer) { RemotePeer remotePeer = (RemotePeer) remote; - if (peerMap.get(remotePeer.hashCode()) == null) { + if (serviceState.getCallInfoState().getPeer(remotePeer.hashCode()) == null) { Log.w(TAG, "remotePeer not found in map with key: " + remotePeer.hashCode() + "! Dropping."); try { + //noinspection ConstantConditions callManager.drop(callId); } catch (CallException e) { - callFailure("callManager.drop() failed: ", e); + serviceState = serviceState.getActionProcessor().callFailure(serviceState, "callManager.drop() failed: ", e); } } @@ -1862,10 +627,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onCallEvent(Remote remote, CallEvent event) { + public void onCallEvent(@Nullable Remote remote, @NonNull CallEvent event) { if (remote instanceof RemotePeer) { RemotePeer remotePeer = (RemotePeer) remote; - if (peerMap.get(remotePeer.hashCode()) == null) { + if (serviceState.getCallInfoState().getPeer(remotePeer.hashCode()) == null) { throw new AssertionError("remotePeer not found in map!"); } @@ -1955,7 +720,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onCallConcluded(Remote remote) { + public void onCallConcluded(@Nullable Remote remote) { if (remote instanceof RemotePeer) { RemotePeer remotePeer = (RemotePeer)remote; @@ -1972,7 +737,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onSendOffer(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, byte[] opaque, String sdp, CallManager.CallMediaType callMediaType) { + public void onSendOffer(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @Nullable byte[] opaque, @Nullable String sdp, @NonNull CallManager.CallMediaType callMediaType) { Log.i(TAG, "onSendOffer: id: " + callId.format(remoteDevice) + " type: " + callMediaType.name()); if (remote instanceof RemotePeer) { @@ -1996,7 +761,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onSendAnswer(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, byte[] opaque, String sdp) { + public void onSendAnswer(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @Nullable byte[] opaque, @Nullable String sdp) { Log.i(TAG, "onSendAnswer: id: " + callId.format(remoteDevice)); if (remote instanceof RemotePeer) { @@ -2018,7 +783,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onSendIceCandidates(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, List iceCandidates) { + public void onSendIceCandidates(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull List iceCandidates) { Log.i(TAG, "onSendIceCandidates: id: " + callId.format(remoteDevice)); if (remote instanceof RemotePeer) { @@ -2044,7 +809,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onSendHangup(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast, CallManager.HangupType hangupType, Integer deviceId, Boolean useLegacyHangupMessage) { + public void onSendHangup(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull CallManager.HangupType hangupType, @NonNull Integer deviceId, @NonNull Boolean useLegacyHangupMessage) { Log.i(TAG, "onSendHangup: id: " + callId.format(remoteDevice) + " type: " + hangupType.name() + " isLegacy: " + useLegacyHangupMessage); if (remote instanceof RemotePeer) { @@ -2067,7 +832,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } @Override - public void onSendBusy(CallId callId, Remote remote, Integer remoteDevice, Boolean broadcast) { + public void onSendBusy(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast) { Log.i(TAG, "onSendBusy: id: " + callId.format(remoteDevice)); if (remote instanceof RemotePeer) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java new file mode 100644 index 0000000000..57f945ff35 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java @@ -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 ENDED_ACTION_TO_STATE = new HashMap() {{ + 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 iceCandidates) + { + Log.i(tag, "handleSendIceCandidates(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice())); + + LinkedList 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 iceCandidateParcels) + { + Log.i(tag, "handleReceivedIceCandidates(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()) + ", count: " + iceCandidateParcels.size()); + + LinkedList 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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java new file mode 100644 index 0000000000..c87730fc5f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java new file mode 100644 index 0000000000..3efe909028 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java @@ -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; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java new file mode 100644 index 0000000000..67e61e4436 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java @@ -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 iceCandidates) + { + return activeCallDelegate.handleSendIceCandidates(currentState, callMetadata, broadcast, iceCandidates); + } + + @Override + protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState, + @NonNull WebRtcData.CallMetadata callMetadata, + @NonNull ArrayList 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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java new file mode 100644 index 0000000000..6bb1b605d3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DisconnectingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DisconnectingCallActionProcessor.java new file mode 100644 index 0000000000..1157c9a821 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DisconnectingCallActionProcessor.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java new file mode 100644 index 0000000000..875c514907 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java new file mode 100644 index 0000000000..5884e77c2d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java @@ -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 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 iceCandidates) + { + return activeCallDelegate.handleSendIceCandidates(currentState, callMetadata, broadcast, iceCandidates); + } + + @Override + protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState, + @NonNull WebRtcData.CallMetadata callMetadata, + @NonNull ArrayList 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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java new file mode 100644 index 0000000000..0331a3fb88 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java @@ -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 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 iceCandidates) + { + return activeCallDelegate.handleSendIceCandidates(currentState, callMetadata, broadcast, iceCandidates); + } + + @Override + protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState, + @NonNull WebRtcData.CallMetadata callMetadata, + @NonNull ArrayList 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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java new file mode 100644 index 0000000000..a572d5ba65 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java new file mode 100644 index 0000000000..f2e6ac85a1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -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 iceCandidates) { + Log.i(tag, "handleSendIceCandidates not processed"); + return currentState; + } + + protected @NonNull WebRtcServiceState handleReceivedIceCandidates(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, @NonNull ArrayList iceCandidateParcels) { + Log.i(tag, "handleReceivedIceCandidates not processed"); + return currentState; + } + + public @NonNull WebRtcServiceState handleTurnServerUpdate(@NonNull WebRtcServiceState currentState, @NonNull List 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 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java new file mode 100644 index 0000000000..16fdd9cab9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java @@ -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; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java new file mode 100644 index 0000000000..88684092cf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java @@ -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 getIceCandidates(@NonNull Intent intent) { + return Objects.requireNonNull(intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES)); + } + + public static @NonNull List getIceServers(@NonNull Intent intent) { + TurnServerInfoParcel turnServerInfoParcel = Objects.requireNonNull(intent.getParcelableExtra(EXTRA_TURN_SERVER_INFO)); + List 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); + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java new file mode 100644 index 0000000000..64a7719b68 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java new file mode 100644 index 0000000000..51072a0757 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java @@ -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); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java new file mode 100644 index 0000000000..8b3352ba7b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java new file mode 100644 index 0000000000..1dce4ee10c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java @@ -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 remoteParticipants; + Map 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 remoteParticipants, + @NonNull Map 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 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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.java new file mode 100644 index 0000000000..a4b19e69ba --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.java @@ -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; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.java new file mode 100644 index 0000000000..541e12e540 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.java @@ -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; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java new file mode 100644 index 0000000000..4fd1278762 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java new file mode 100644 index 0000000000..999ba342c1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java new file mode 100644 index 0000000000..175b6bb454 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -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; + } + } +}