kopia lustrzana https://github.com/ryukoposting/Signal-Android
944 wiersze
41 KiB
Java
944 wiersze
41 KiB
Java
package org.thoughtcrime.securesms.service.webrtc;
|
|
|
|
import android.app.Application;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.os.Build;
|
|
import android.os.ResultReceiver;
|
|
|
|
import androidx.annotation.AnyThread;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.annimon.stream.Stream;
|
|
|
|
import org.greenrobot.eventbus.EventBus;
|
|
import org.signal.core.util.concurrent.SignalExecutors;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.signal.libsignal.protocol.util.Pair;
|
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
|
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
|
import org.signal.libsignal.zkgroup.groups.GroupIdentifier;
|
|
import org.signal.ringrtc.CallException;
|
|
import org.signal.ringrtc.CallId;
|
|
import org.signal.ringrtc.CallManager;
|
|
import org.signal.ringrtc.GroupCall;
|
|
import org.signal.ringrtc.HttpHeader;
|
|
import org.signal.ringrtc.NetworkRoute;
|
|
import org.signal.ringrtc.PeekInfo;
|
|
import org.signal.ringrtc.Remote;
|
|
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
|
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
|
|
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
|
import org.thoughtcrime.securesms.groups.GroupId;
|
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
|
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
|
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
|
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
|
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
|
|
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
|
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
|
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
|
|
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
|
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
|
import org.thoughtcrime.securesms.util.BubbleUtil;
|
|
import org.thoughtcrime.securesms.util.RecipientAccessList;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
import org.thoughtcrime.securesms.util.rx.RxStore;
|
|
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
|
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
|
|
import org.webrtc.PeerConnection;
|
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
|
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
|
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.stream.Collectors;
|
|
|
|
import io.reactivex.rxjava3.core.Flowable;
|
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
import kotlin.jvm.functions.Function1;
|
|
|
|
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
|
|
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
|
|
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
|
|
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
|
|
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
|
|
import static org.thoughtcrime.securesms.service.webrtc.WebRtcUtil.getUrgencyFromCallUrgency;
|
|
|
|
/**
|
|
* Entry point for all things calling. Lives for the life of the app instance and will spin up a foreground service when needed to
|
|
* handle "active" calls.
|
|
*/
|
|
public final class SignalCallManager implements CallManager.Observer, GroupCall.Observer, CameraEventListener, AppForegroundObserver.Listener {
|
|
|
|
private static final String TAG = Log.tag(SignalCallManager.class);
|
|
|
|
public static final int BUSY_TONE_LENGTH = 2000;
|
|
|
|
@Nullable private final CallManager callManager;
|
|
|
|
private final Context context;
|
|
private final ExecutorService serviceExecutor;
|
|
private final Executor networkExecutor;
|
|
private final LockManager lockManager;
|
|
|
|
private WebRtcServiceState serviceState;
|
|
private RxStore<WebRtcEphemeralState> ephemeralStateStore;
|
|
private boolean needsToSetSelfUuid = true;
|
|
|
|
public SignalCallManager(@NonNull Application application) {
|
|
this.context = application.getApplicationContext();
|
|
this.lockManager = new LockManager(this.context);
|
|
this.serviceExecutor = Executors.newSingleThreadExecutor();
|
|
this.networkExecutor = Executors.newSingleThreadExecutor();
|
|
this.ephemeralStateStore = new RxStore<>(new WebRtcEphemeralState(), Schedulers.from(serviceExecutor));
|
|
|
|
CallManager callManager = null;
|
|
try {
|
|
callManager = CallManager.createCallManager(this);
|
|
} catch (CallException e) {
|
|
Log.w(TAG, "Unable to create CallManager", e);
|
|
}
|
|
this.callManager = callManager;
|
|
|
|
this.serviceState = new WebRtcServiceState(new IdleActionProcessor(new WebRtcInteractor(this.context,
|
|
this,
|
|
lockManager,
|
|
this,
|
|
this,
|
|
this)));
|
|
}
|
|
|
|
public @NonNull Flowable<WebRtcEphemeralState> ephemeralStates() {
|
|
return ephemeralStateStore.getStateFlowable().distinctUntilChanged();
|
|
}
|
|
|
|
@NonNull CallManager getRingRtcCallManager() {
|
|
//noinspection ConstantConditions
|
|
return callManager;
|
|
}
|
|
|
|
@NonNull LockManager getLockManager() {
|
|
return lockManager;
|
|
}
|
|
|
|
private void process(@NonNull ProcessAction action) {
|
|
Throwable t = new Throwable();
|
|
String caller = t.getStackTrace().length > 1 ? t.getStackTrace()[1].getMethodName() : "unknown";
|
|
|
|
if (callManager == null) {
|
|
Log.w(TAG, "Unable to process action, call manager is not initialized");
|
|
return;
|
|
}
|
|
|
|
serviceExecutor.execute(() -> {
|
|
if (needsToSetSelfUuid) {
|
|
try {
|
|
callManager.setSelfUuid(SignalStore.account().requireAci().uuid());
|
|
needsToSetSelfUuid = false;
|
|
} catch (CallException e) {
|
|
Log.w(TAG, "Unable to set self UUID on CallManager", e);
|
|
}
|
|
}
|
|
|
|
Log.v(TAG, "Processing action: " + caller + ", handler: " + serviceState.getActionProcessor().getTag());
|
|
WebRtcServiceState previous = serviceState;
|
|
serviceState = action.process(previous, previous.getActionProcessor());
|
|
|
|
if (previous != serviceState) {
|
|
if (serviceState.getCallInfoState().getCallState() != WebRtcViewModel.State.IDLE) {
|
|
postStateUpdate(serviceState);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Processes the given update to {@link WebRtcEphemeralState}.
|
|
*
|
|
* @param transformer The transformation to apply to the state. Runs on the {@link #serviceExecutor}.
|
|
*/
|
|
@AnyThread
|
|
private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEphemeralState> transformer) {
|
|
ephemeralStateStore.update(transformer);
|
|
}
|
|
|
|
public void startPreJoinCall(@NonNull Recipient recipient) {
|
|
process((s, p) -> p.handlePreJoinCall(s, new RemotePeer(recipient.getId())));
|
|
}
|
|
|
|
public void startOutgoingAudioCall(@NonNull Recipient recipient) {
|
|
process((s, p) -> p.handleOutgoingCall(s, new RemotePeer(recipient.getId()), OfferMessage.Type.AUDIO_CALL));
|
|
}
|
|
|
|
public void startOutgoingVideoCall(@NonNull Recipient recipient) {
|
|
process((s, p) -> p.handleOutgoingCall(s, new RemotePeer(recipient.getId()), OfferMessage.Type.VIDEO_CALL));
|
|
}
|
|
|
|
public void cancelPreJoin() {
|
|
process((s, p) -> p.handleCancelPreJoinCall(s));
|
|
}
|
|
|
|
public void updateRenderedResolutions() {
|
|
process((s, p) -> p.handleUpdateRenderedResolutions(s));
|
|
}
|
|
|
|
public void orientationChanged(boolean isLandscapeEnabled, int degrees) {
|
|
process((s, p) -> p.handleOrientationChanged(s, isLandscapeEnabled, degrees));
|
|
}
|
|
|
|
public void setMuteAudio(boolean enabled) {
|
|
process((s, p) -> p.handleSetMuteAudio(s, enabled));
|
|
}
|
|
|
|
public void setMuteVideo(boolean enabled) {
|
|
process((s, p) -> p.handleSetEnableVideo(s, enabled));
|
|
}
|
|
|
|
public void flipCamera() {
|
|
process((s, p) -> p.handleSetCameraFlip(s));
|
|
}
|
|
|
|
public void acceptCall(boolean answerWithVideo) {
|
|
process((s, p) -> p.handleAcceptCall(s, answerWithVideo));
|
|
}
|
|
|
|
public void denyCall() {
|
|
process((s, p) -> p.handleDenyCall(s));
|
|
}
|
|
|
|
public void localHangup() {
|
|
process((s, p) -> p.handleLocalHangup(s));
|
|
}
|
|
|
|
public void requestUpdateGroupMembers() {
|
|
process((s, p) -> p.handleGroupRequestUpdateMembers(s));
|
|
}
|
|
|
|
public void groupApproveSafetyChange(@NonNull List<RecipientId> changedRecipients) {
|
|
process((s, p) -> p.handleGroupApproveSafetyNumberChange(s, changedRecipients));
|
|
}
|
|
|
|
public void isCallActive(@Nullable ResultReceiver resultReceiver) {
|
|
process((s, p) -> p.handleIsInCallQuery(s, resultReceiver));
|
|
}
|
|
|
|
public void networkChange(boolean available) {
|
|
process((s, p) -> p.handleNetworkChanged(s, available));
|
|
}
|
|
|
|
public void bandwidthModeUpdate() {
|
|
process((s, p) -> p.handleBandwidthModeUpdate(s));
|
|
}
|
|
|
|
public void screenOff() {
|
|
process((s, p) -> p.handleScreenOffChange(s));
|
|
}
|
|
|
|
public void postStateUpdate(@NonNull WebRtcServiceState state) {
|
|
EventBus.getDefault().postSticky(new WebRtcViewModel(state));
|
|
}
|
|
|
|
public void receivedOffer(@NonNull WebRtcData.CallMetadata callMetadata,
|
|
@NonNull WebRtcData.OfferMetadata offerMetadata,
|
|
@NonNull WebRtcData.ReceivedOfferMetadata receivedOfferMetadata)
|
|
{
|
|
process((s, p) -> p.handleReceivedOffer(s, callMetadata, offerMetadata, receivedOfferMetadata));
|
|
}
|
|
|
|
public void receivedAnswer(@NonNull WebRtcData.CallMetadata callMetadata,
|
|
@NonNull WebRtcData.AnswerMetadata answerMetadata,
|
|
@NonNull WebRtcData.ReceivedAnswerMetadata receivedAnswerMetadata)
|
|
{
|
|
process((s, p) -> p.handleReceivedAnswer(s, callMetadata, answerMetadata, receivedAnswerMetadata));
|
|
}
|
|
|
|
public void receivedIceCandidates(@NonNull WebRtcData.CallMetadata callMetadata, @NonNull List<byte[]> iceCandidates) {
|
|
process((s, p) -> p.handleReceivedIceCandidates(s, callMetadata, iceCandidates));
|
|
}
|
|
|
|
public void receivedCallHangup(@NonNull WebRtcData.CallMetadata callMetadata, @NonNull WebRtcData.HangupMetadata hangupMetadata) {
|
|
process((s, p) -> p.handleReceivedHangup(s, callMetadata, hangupMetadata));
|
|
}
|
|
|
|
public void receivedCallBusy(@NonNull WebRtcData.CallMetadata callMetadata) {
|
|
process((s, p) -> p.handleReceivedBusy(s, callMetadata));
|
|
}
|
|
|
|
public void receivedOpaqueMessage(@NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
|
|
process((s, p) -> p.handleReceivedOpaqueMessage(s, opaqueMessageMetadata));
|
|
}
|
|
|
|
public void setRingGroup(boolean ringGroup) {
|
|
process((s, p) -> p.handleSetRingGroup(s, ringGroup));
|
|
}
|
|
|
|
private void receivedGroupCallPeekForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo, @NonNull PeekInfo peekInfo) {
|
|
process((s, p) -> p.handleReceivedGroupCallPeekForRingingCheck(s, groupCallRingCheckInfo, peekInfo));
|
|
}
|
|
|
|
public void onAudioDeviceChanged(@NonNull SignalAudioManager.AudioDevice activeDevice, @NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
|
|
process((s, p) -> p.handleAudioDeviceChanged(s, activeDevice, availableDevices));
|
|
}
|
|
|
|
public void onBluetoothPermissionDenied() {
|
|
process((s, p) -> p.handleBluetoothPermissionDenied(s));
|
|
}
|
|
|
|
public void selectAudioDevice(@NonNull SignalAudioManager.AudioDevice desiredDevice) {
|
|
process((s, p) -> p.handleSetUserAudioDevice(s, desiredDevice));
|
|
}
|
|
|
|
public void setTelecomApproved(long callId, @NonNull RecipientId recipientId) {
|
|
process((s, p) -> p.handleSetTelecomApproved(s, callId, recipientId));
|
|
}
|
|
|
|
public void dropCall(long callId) {
|
|
process((s, p) -> p.handleDropCall(s, callId));
|
|
}
|
|
|
|
public void peekGroupCall(@NonNull RecipientId id) {
|
|
if (callManager == null) {
|
|
Log.i(TAG, "Unable to peekGroupCall, call manager is null");
|
|
return;
|
|
}
|
|
|
|
networkExecutor.execute(() -> {
|
|
try {
|
|
Recipient group = Recipient.resolved(id);
|
|
GroupId.V2 groupId = group.requireGroupId().requireV2();
|
|
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(context, groupId);
|
|
|
|
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(context, groupId))
|
|
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
|
|
.toList();
|
|
|
|
callManager.peekGroupCall(SignalStore.internalValues().groupCallingServer(), credential.getTokenBytes().toByteArray(), members, peekInfo -> {
|
|
Long threadId = SignalDatabase.threads().getThreadIdFor(group.getId());
|
|
|
|
if (threadId != null) {
|
|
SignalDatabase.sms()
|
|
.updatePreviousGroupCall(threadId,
|
|
peekInfo.getEraId(),
|
|
peekInfo.getJoinedMembers(),
|
|
WebRtcUtil.isCallFull(peekInfo));
|
|
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(threadId), true, 0, BubbleUtil.BubbleState.HIDDEN);
|
|
|
|
EventBus.getDefault().postSticky(new GroupCallPeekEvent(id, peekInfo.getEraId(), peekInfo.getDeviceCount(), peekInfo.getMaxDevices()));
|
|
}
|
|
});
|
|
} catch (IOException | VerificationFailedException | CallException e) {
|
|
Log.e(TAG, "error peeking from active conversation", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo info) {
|
|
if (callManager == null) {
|
|
Log.i(TAG, "Unable to peekGroupCall, call manager is null");
|
|
return;
|
|
}
|
|
|
|
networkExecutor.execute(() -> {
|
|
try {
|
|
Recipient group = Recipient.resolved(info.getRecipientId());
|
|
GroupId.V2 groupId = group.requireGroupId().requireV2();
|
|
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(context, groupId);
|
|
|
|
List<GroupCall.GroupMemberInfo> members = GroupManager.getUuidCipherTexts(context, groupId)
|
|
.entrySet()
|
|
.stream()
|
|
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
|
|
.collect(Collectors.toList());
|
|
|
|
callManager.peekGroupCall(SignalStore.internalValues().groupCallingServer(),
|
|
credential.getTokenBytes().toByteArray(),
|
|
members,
|
|
peekInfo -> receivedGroupCallPeekForRingingCheck(info, peekInfo));
|
|
} catch (IOException | VerificationFailedException | CallException e) {
|
|
Log.e(TAG, "error peeking for ringing check", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
void requestGroupMembershipToken(@NonNull GroupId.V2 groupId, int groupCallHashCode) {
|
|
networkExecutor.execute(() -> {
|
|
try {
|
|
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(context, groupId);
|
|
process((s, p) -> p.handleGroupMembershipProofResponse(s, groupCallHashCode, credential.getTokenBytes().toByteArray()));
|
|
} catch (IOException e) {
|
|
Log.w(TAG, "Unable to get group membership proof from service", e);
|
|
process((s, p) -> p.handleGroupCallEnded(s, groupCallHashCode, GroupCall.GroupCallEndReason.SFU_CLIENT_FAILED_TO_JOIN));
|
|
} catch (VerificationFailedException e) {
|
|
Log.w(TAG, "Unable to verify group membership proof", e);
|
|
process((s, p) -> p.handleGroupCallEnded(s, groupCallHashCode, GroupCall.GroupCallEndReason.DEVICE_EXPLICITLY_DISCONNECTED));
|
|
}
|
|
});
|
|
}
|
|
|
|
public boolean startCallCardActivityIfPossible() {
|
|
if (Build.VERSION.SDK_INT >= 29 && !ApplicationDependencies.getAppForegroundObserver().isForegrounded()) {
|
|
return false;
|
|
}
|
|
|
|
context.startActivity(new Intent(context, WebRtcCallActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onStartCall(@Nullable Remote remote,
|
|
@NonNull CallId callId,
|
|
@NonNull Boolean isOutgoing,
|
|
@Nullable CallManager.CallMediaType callMediaType)
|
|
{
|
|
Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType);
|
|
|
|
if (callManager == null) {
|
|
Log.w(TAG, "Unable to start call, call manager is not initialized");
|
|
return;
|
|
}
|
|
|
|
if (remote == null) {
|
|
return;
|
|
}
|
|
|
|
process((s, p) -> {
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
if (s.getCallInfoState().getPeer(remotePeer.hashCode()) == null) {
|
|
Log.w(TAG, "remotePeer not found in map with key: " + remotePeer.hashCode() + "! Dropping.");
|
|
try {
|
|
callManager.drop(callId);
|
|
} catch (CallException e) {
|
|
s = p.callFailure(s, "callManager.drop() failed: ", e);
|
|
}
|
|
}
|
|
|
|
remotePeer.setCallId(callId);
|
|
|
|
if (isOutgoing) {
|
|
return p.handleStartOutgoingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType));
|
|
} else {
|
|
return p.handleStartIncomingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType));
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onCallEvent(@Nullable Remote remote, @NonNull CallManager.CallEvent event) {
|
|
if (callManager == null) {
|
|
Log.w(TAG, "Unable to process call event, call manager is not initialized");
|
|
return;
|
|
}
|
|
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
|
|
process((s, p) -> {
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
if (s.getCallInfoState().getPeer(remotePeer.hashCode()) == null) {
|
|
Log.w(TAG, "remotePeer not found in map with key: " + remotePeer.hashCode() + "! Dropping.");
|
|
try {
|
|
callManager.drop(remotePeer.getCallId());
|
|
} catch (CallException e) {
|
|
return p.callFailure(s, "callManager.drop() failed: ", e);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
Log.i(TAG, "onCallEvent(): call_id: " + remotePeer.getCallId() + ", state: " + remotePeer.getState() + ", event: " + event);
|
|
|
|
switch (event) {
|
|
case LOCAL_RINGING:
|
|
return p.handleLocalRinging(s, remotePeer);
|
|
case REMOTE_RINGING:
|
|
return p.handleRemoteRinging(s, remotePeer);
|
|
case RECONNECTING:
|
|
case RECONNECTED:
|
|
return p.handleCallReconnect(s, event);
|
|
case LOCAL_CONNECTED:
|
|
case REMOTE_CONNECTED:
|
|
return p.handleCallConnected(s, remotePeer);
|
|
case REMOTE_VIDEO_ENABLE:
|
|
return p.handleRemoteVideoEnable(s, true);
|
|
case REMOTE_VIDEO_DISABLE:
|
|
return p.handleRemoteVideoEnable(s, false);
|
|
case REMOTE_SHARING_SCREEN_ENABLE:
|
|
return p.handleScreenSharingEnable(s, true);
|
|
case REMOTE_SHARING_SCREEN_DISABLE:
|
|
return p.handleScreenSharingEnable(s, false);
|
|
case ENDED_REMOTE_HANGUP:
|
|
case ENDED_REMOTE_HANGUP_NEED_PERMISSION:
|
|
case ENDED_REMOTE_HANGUP_ACCEPTED:
|
|
case ENDED_REMOTE_HANGUP_BUSY:
|
|
case ENDED_REMOTE_HANGUP_DECLINED:
|
|
case ENDED_REMOTE_BUSY:
|
|
case ENDED_REMOTE_GLARE:
|
|
case ENDED_REMOTE_RECALL:
|
|
return p.handleEndedRemote(s, event, remotePeer);
|
|
case ENDED_TIMEOUT:
|
|
case ENDED_INTERNAL_FAILURE:
|
|
case ENDED_SIGNALING_FAILURE:
|
|
case ENDED_GLARE_HANDLING_FAILURE:
|
|
case ENDED_CONNECTION_FAILURE:
|
|
return p.handleEnded(s, event, remotePeer);
|
|
case RECEIVED_OFFER_EXPIRED:
|
|
return p.handleReceivedOfferExpired(s, remotePeer);
|
|
case RECEIVED_OFFER_WHILE_ACTIVE:
|
|
case RECEIVED_OFFER_WITH_GLARE:
|
|
return p.handleReceivedOfferWhileActive(s, remotePeer);
|
|
case ENDED_LOCAL_HANGUP:
|
|
case ENDED_APP_DROPPED_CALL:
|
|
Log.i(TAG, "Ignoring event: " + event);
|
|
break;
|
|
default:
|
|
throw new AssertionError("Unexpected event: " + event.toString());
|
|
}
|
|
|
|
return s;
|
|
});
|
|
}
|
|
|
|
@Override public void onNetworkRouteChanged(Remote remote, NetworkRoute networkRoute) {
|
|
process((s, p) -> p.handleNetworkRouteChanged(s, networkRoute));
|
|
}
|
|
|
|
@Override
|
|
public void onAudioLevels(Remote remote, int capturedLevel, int receivedLevel) {
|
|
processStateless(s -> serviceState.getActionProcessor().handleAudioLevelsChanged(serviceState, s, capturedLevel, receivedLevel));
|
|
}
|
|
|
|
@Override
|
|
public void onCallConcluded(@Nullable Remote remote) {
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
Log.i(TAG, "onCallConcluded: call_id: " + remotePeer.getCallId());
|
|
process((s, p) -> p.handleCallConcluded(s, remotePeer));
|
|
}
|
|
|
|
@Override
|
|
public void onSendOffer(@NonNull CallId callId,
|
|
@Nullable Remote remote,
|
|
@NonNull Integer remoteDevice,
|
|
@NonNull Boolean broadcast,
|
|
@NonNull byte[] opaque,
|
|
@NonNull CallManager.CallMediaType callMediaType)
|
|
{
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
|
|
Log.i(TAG, "onSendOffer: id: " + remotePeer.getCallId().format(remoteDevice) + " type: " + callMediaType.name());
|
|
|
|
OfferMessage.Type offerType = WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType);
|
|
WebRtcData.CallMetadata callMetadata = new WebRtcData.CallMetadata(remotePeer, remoteDevice);
|
|
WebRtcData.OfferMetadata offerMetadata = new WebRtcData.OfferMetadata(opaque, null, offerType);
|
|
|
|
process((s, p) -> p.handleSendOffer(s, callMetadata, offerMetadata, broadcast));
|
|
}
|
|
|
|
@Override
|
|
public void onSendAnswer(@NonNull CallId callId,
|
|
@Nullable Remote remote,
|
|
@NonNull Integer remoteDevice,
|
|
@NonNull Boolean broadcast,
|
|
@NonNull byte[] opaque)
|
|
{
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
|
|
Log.i(TAG, "onSendAnswer: id: " + remotePeer.getCallId().format(remoteDevice));
|
|
|
|
WebRtcData.CallMetadata callMetadata = new WebRtcData.CallMetadata(remotePeer, remoteDevice);
|
|
WebRtcData.AnswerMetadata answerMetadata = new WebRtcData.AnswerMetadata(opaque, null);
|
|
|
|
process((s, p) -> p.handleSendAnswer(s, callMetadata, answerMetadata, broadcast));
|
|
}
|
|
|
|
@Override
|
|
public void onSendIceCandidates(@NonNull CallId callId,
|
|
@Nullable Remote remote,
|
|
@NonNull Integer remoteDevice,
|
|
@NonNull Boolean broadcast,
|
|
@NonNull List<byte[]> iceCandidates)
|
|
{
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
|
|
Log.i(TAG, "onSendIceCandidates: id: " + remotePeer.getCallId().format(remoteDevice));
|
|
|
|
WebRtcData.CallMetadata callMetadata = new WebRtcData.CallMetadata(remotePeer, remoteDevice);
|
|
|
|
process((s, p) -> p.handleSendIceCandidates(s, callMetadata, broadcast, iceCandidates));
|
|
}
|
|
|
|
@Override
|
|
public void onSendHangup(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull CallManager.HangupType hangupType, @NonNull Integer deviceId) {
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
|
|
Log.i(TAG, "onSendHangup: id: " + remotePeer.getCallId().format(remoteDevice) + " type: " + hangupType.name());
|
|
|
|
WebRtcData.CallMetadata callMetadata = new WebRtcData.CallMetadata(remotePeer, remoteDevice);
|
|
WebRtcData.HangupMetadata hangupMetadata = new WebRtcData.HangupMetadata(WebRtcUtil.getHangupTypeFromCallHangupType(hangupType), false, deviceId);
|
|
|
|
process((s, p) -> p.handleSendHangup(s, callMetadata, hangupMetadata, broadcast));
|
|
}
|
|
|
|
@Override
|
|
public void onSendBusy(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast) {
|
|
if (!(remote instanceof RemotePeer)) {
|
|
return;
|
|
}
|
|
|
|
RemotePeer remotePeer = (RemotePeer) remote;
|
|
|
|
Log.i(TAG, "onSendBusy: id: " + remotePeer.getCallId().format(remoteDevice));
|
|
|
|
WebRtcData.CallMetadata callMetadata = new WebRtcData.CallMetadata(remotePeer, remoteDevice);
|
|
|
|
process((s, p) -> p.handleSendBusy(s, callMetadata, broadcast));
|
|
}
|
|
|
|
@Override
|
|
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] bytes, @NonNull CallManager.CallMessageUrgency urgency) {
|
|
Log.i(TAG, "onSendCallMessage():");
|
|
|
|
OpaqueMessage opaqueMessage = new OpaqueMessage(bytes, getUrgencyFromCallUrgency(urgency));
|
|
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOpaque(opaqueMessage, true, null);
|
|
|
|
networkExecutor.execute(() -> {
|
|
Recipient recipient = Recipient.resolved(RecipientId.from(ServiceId.from(uuid)));
|
|
if (recipient.isBlocked()) {
|
|
return;
|
|
}
|
|
try {
|
|
ApplicationDependencies.getSignalServiceMessageSender()
|
|
.sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
|
|
recipient.isSelf() ? Optional.empty() : UnidentifiedAccessUtil.getAccessFor(context, recipient),
|
|
callMessage);
|
|
} catch (UntrustedIdentityException e) {
|
|
Log.i(TAG, "sendOpaqueCallMessage onFailure: ", e);
|
|
RetrieveProfileJob.enqueue(recipient.getId());
|
|
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), UNTRUSTED_IDENTITY));
|
|
} catch (IOException e) {
|
|
Log.i(TAG, "sendOpaqueCallMessage onFailure: ", e);
|
|
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), NETWORK_FAILURE));
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onSendCallMessageToGroup(@NonNull byte[] groupIdBytes, @NonNull byte[] message, @NonNull CallManager.CallMessageUrgency urgency) {
|
|
Log.i(TAG, "onSendCallMessageToGroup():");
|
|
|
|
networkExecutor.execute(() -> {
|
|
try {
|
|
GroupId groupId = GroupId.v2(new GroupIdentifier(groupIdBytes));
|
|
List<Recipient> recipients = SignalDatabase.groups().getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
|
|
|
recipients = RecipientUtil.getEligibleForSending((recipients.stream()
|
|
.map(Recipient::resolve)
|
|
.collect(Collectors.toList())));
|
|
|
|
OpaqueMessage opaqueMessage = new OpaqueMessage(message, getUrgencyFromCallUrgency(urgency));
|
|
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOutgoingGroupOpaque(groupId.getDecodedId(), System.currentTimeMillis(), opaqueMessage, true, null);
|
|
RecipientAccessList accessList = new RecipientAccessList(recipients);
|
|
|
|
List<SendMessageResult> results = GroupSendUtil.sendCallMessage(context,
|
|
groupId.requireV2(),
|
|
recipients,
|
|
callMessage);
|
|
|
|
Set<RecipientId> identifyFailureRecipientIds = results.stream()
|
|
.filter(result -> result.getIdentityFailure() != null)
|
|
.map(result -> accessList.requireIdByAddress(result.getAddress()))
|
|
.collect(Collectors.toSet());
|
|
|
|
if (Util.hasItems(identifyFailureRecipientIds)) {
|
|
process((s, p) -> p.handleGroupMessageSentError(s, identifyFailureRecipientIds, UNTRUSTED_IDENTITY));
|
|
|
|
RetrieveProfileJob.enqueue(identifyFailureRecipientIds);
|
|
}
|
|
} catch (UntrustedIdentityException | IOException | InvalidInputException e) {
|
|
Log.w(TAG, "onSendCallMessageToGroup failed", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onSendHttpRequest(long requestId, @NonNull String url, @NonNull CallManager.HttpMethod httpMethod, @Nullable List<HttpHeader> headers, @Nullable byte[] body) {
|
|
if (callManager == null) {
|
|
Log.w(TAG, "Unable to send http request, call manager is not initialized");
|
|
return;
|
|
}
|
|
|
|
Log.i(TAG, "onSendHttpRequest(): request_id: " + requestId);
|
|
networkExecutor.execute(() -> {
|
|
List<Pair<String, String>> headerPairs;
|
|
if (headers != null) {
|
|
headerPairs = Stream.of(headers)
|
|
.map(header -> new Pair<>(header.getName(), header.getValue()))
|
|
.toList();
|
|
} else {
|
|
headerPairs = Collections.emptyList();
|
|
}
|
|
|
|
CallingResponse response = ApplicationDependencies.getSignalServiceMessageSender()
|
|
.makeCallingRequest(requestId, url, httpMethod.name(), headerPairs, body);
|
|
|
|
try {
|
|
if (response instanceof CallingResponse.Success) {
|
|
CallingResponse.Success success = (CallingResponse.Success) response;
|
|
callManager.receivedHttpResponse(requestId, success.getResponseStatus(), success.getResponseBody());
|
|
} else {
|
|
callManager.httpRequestFailed(requestId);
|
|
}
|
|
} catch (CallException e) {
|
|
Log.i(TAG, "Failed to process HTTP response/failure", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onGroupCallRingUpdate(@NonNull byte[] groupIdBytes, long ringId, @NonNull UUID sender, @NonNull CallManager.RingUpdate ringUpdate) {
|
|
try {
|
|
GroupId.V2 groupId = GroupId.v2(new GroupIdentifier(groupIdBytes));
|
|
GroupDatabase.GroupRecord group = SignalDatabase.groups().getGroup(groupId).orElse(null);
|
|
Recipient senderRecipient = Recipient.externalPush(ServiceId.from(sender));
|
|
|
|
if (group != null &&
|
|
group.isActive() &&
|
|
!Recipient.resolved(group.getRecipientId()).isBlocked() &&
|
|
(!group.isAnnouncementGroup() || group.isAdmin(senderRecipient)))
|
|
{
|
|
process((s, p) -> p.handleGroupCallRingUpdate(s, new RemotePeer(group.getRecipientId()), groupId, ringId, sender, ringUpdate));
|
|
} else {
|
|
Log.w(TAG, "Unable to ring unknown/inactive/blocked group.");
|
|
}
|
|
} catch (InvalidInputException e) {
|
|
Log.w(TAG, "Unable to ring group due to invalid group id", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void requestMembershipProof(@NonNull final GroupCall groupCall) {
|
|
Log.i(TAG, "requestMembershipProof():");
|
|
process((s, p) -> p.handleGroupRequestMembershipProof(s, groupCall.hashCode()));
|
|
}
|
|
|
|
@Override
|
|
public void requestGroupMembers(@NonNull GroupCall groupCall) {
|
|
process((s, p) -> p.handleGroupRequestUpdateMembers(s));
|
|
}
|
|
|
|
@Override
|
|
public void onLocalDeviceStateChanged(@NonNull GroupCall groupCall) {
|
|
Log.i(TAG, "onLocalDeviceStateChanged: localAdapterType: " + groupCall.getLocalDeviceState().getNetworkRoute().getLocalAdapterType());
|
|
process((s, p) -> p.handleGroupLocalDeviceStateChanged(s));
|
|
}
|
|
|
|
@Override
|
|
public void onAudioLevels(@NonNull GroupCall groupCall) {
|
|
processStateless(s -> serviceState.getActionProcessor().handleGroupAudioLevelsChanged(serviceState, s));
|
|
}
|
|
|
|
@Override
|
|
public void onRemoteDeviceStatesChanged(@NonNull GroupCall groupCall) {
|
|
process((s, p) -> p.handleGroupRemoteDeviceStateChanged(s));
|
|
}
|
|
|
|
@Override
|
|
public void onPeekChanged(@NonNull GroupCall groupCall) {
|
|
process((s, p) -> p.handleGroupJoinedMembershipChanged(s));
|
|
}
|
|
|
|
@Override
|
|
public void onEnded(@NonNull GroupCall groupCall, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
|
|
process((s, p) -> p.handleGroupCallEnded(s, groupCall.hashCode(), groupCallEndReason));
|
|
}
|
|
|
|
@Override
|
|
public void onFullyInitialized() {
|
|
process((s, p) -> p.handleOrientationChanged(s, s.getLocalDeviceState().isLandscapeEnabled(), s.getLocalDeviceState().getDeviceOrientation().getDegrees()));
|
|
}
|
|
|
|
@Override
|
|
public void onCameraSwitchCompleted(@NonNull final CameraState newCameraState) {
|
|
process((s, p) -> p.handleCameraSwitchCompleted(s, newCameraState));
|
|
}
|
|
|
|
@Override
|
|
public void onForeground() {
|
|
process((s, p) -> {
|
|
WebRtcViewModel.State callState = s.getCallInfoState().getCallState();
|
|
WebRtcViewModel.GroupCallState groupCallState = s.getCallInfoState().getGroupCallState();
|
|
|
|
if (callState == CALL_INCOMING && (groupCallState == IDLE || groupCallState.isRinging())) {
|
|
Log.i(TAG, "Starting call activity from foreground listener");
|
|
startCallCardActivityIfPossible();
|
|
}
|
|
ApplicationDependencies.getAppForegroundObserver().removeListener(this);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
public void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal, long timestamp, boolean isVideoOffer) {
|
|
Pair<Long, Long> messageAndThreadId = SignalDatabase.sms().insertMissedCall(remotePeer.getId(), timestamp, isVideoOffer);
|
|
|
|
ApplicationDependencies.getMessageNotifier()
|
|
.updateNotification(context, ConversationId.forConversation(messageAndThreadId.second()), signal);
|
|
}
|
|
|
|
public void insertReceivedCall(@NonNull RemotePeer remotePeer, boolean signal, boolean isVideoOffer) {
|
|
Pair<Long, Long> messageAndThreadId = SignalDatabase.sms().insertReceivedCall(remotePeer.getId(), isVideoOffer);
|
|
|
|
ApplicationDependencies.getMessageNotifier()
|
|
.updateNotification(context, ConversationId.forConversation(messageAndThreadId.second()), signal);
|
|
}
|
|
|
|
public void retrieveTurnServers(@NonNull RemotePeer remotePeer) {
|
|
networkExecutor.execute(() -> {
|
|
try {
|
|
TurnServerInfo turnServerInfo = ApplicationDependencies.getSignalServiceAccountManager().getTurnServerInfo();
|
|
|
|
List<PeerConnection.IceServer> iceServers = new LinkedList<>();
|
|
iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
|
|
for (String url : turnServerInfo.getUrls()) {
|
|
Log.i(TAG, "ice_server: " + url);
|
|
if (url.startsWith("turn")) {
|
|
iceServers.add(PeerConnection.IceServer.builder(url)
|
|
.setUsername(turnServerInfo.getUsername())
|
|
.setPassword(turnServerInfo.getPassword())
|
|
.createIceServer());
|
|
} else {
|
|
iceServers.add(PeerConnection.IceServer.builder(url).createIceServer());
|
|
}
|
|
}
|
|
|
|
process((s, p) -> {
|
|
RemotePeer activePeer = s.getCallInfoState().getActivePeer();
|
|
if (activePeer != null && activePeer.getCallId().equals(remotePeer.getCallId())) {
|
|
return p.handleTurnServerUpdate(s, iceServers, TextSecurePreferences.isTurnOnly(context));
|
|
}
|
|
|
|
Log.w(TAG, "Ignoring received turn servers for incorrect call id. requesting_call_id: " + remotePeer.getCallId() + " current_call_id: " + (activePeer != null ? activePeer.getCallId() : "null"));
|
|
return s;
|
|
});
|
|
} catch (IOException e) {
|
|
Log.w(TAG, "Unable to retrieve turn servers: ", e);
|
|
process((s, p) -> p.handleSetupFailure(s, remotePeer.getCallId()));
|
|
}
|
|
});
|
|
}
|
|
|
|
public void sendGroupCallUpdateMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
|
|
SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(GroupCallUpdateSendJob.create(recipient.getId(), groupCallEraId)));
|
|
}
|
|
|
|
public void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection<UUID> joinedMembers, boolean isCallFull) {
|
|
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.sms().insertOrUpdateGroupCall(groupId,
|
|
Recipient.self().getId(),
|
|
System.currentTimeMillis(),
|
|
groupCallEraId,
|
|
joinedMembers,
|
|
isCallFull));
|
|
}
|
|
|
|
public void sendCallMessage(@NonNull final RemotePeer remotePeer,
|
|
@NonNull final SignalServiceCallMessage callMessage)
|
|
{
|
|
networkExecutor.execute(() -> {
|
|
Recipient recipient = Recipient.resolved(remotePeer.getId());
|
|
if (recipient.isBlocked()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
ApplicationDependencies.getSignalServiceMessageSender()
|
|
.sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
|
|
UnidentifiedAccessUtil.getAccessFor(context, recipient),
|
|
callMessage);
|
|
process((s, p) -> p.handleMessageSentSuccess(s, remotePeer.getCallId()));
|
|
} catch (UntrustedIdentityException e) {
|
|
RetrieveProfileJob.enqueue(remotePeer.getId());
|
|
processSendMessageFailureWithChangeDetection(remotePeer,
|
|
(s, p) -> p.handleMessageSentError(s,
|
|
remotePeer.getCallId(),
|
|
UNTRUSTED_IDENTITY,
|
|
Optional.ofNullable(e.getIdentityKey())));
|
|
} catch (IOException e) {
|
|
processSendMessageFailureWithChangeDetection(remotePeer,
|
|
(s, p) -> p.handleMessageSentError(s,
|
|
remotePeer.getCallId(),
|
|
e instanceof UnregisteredUserException ? NO_SUCH_USER : NETWORK_FAILURE,
|
|
Optional.empty()));
|
|
}
|
|
});
|
|
}
|
|
|
|
private void processSendMessageFailureWithChangeDetection(@NonNull RemotePeer remotePeer,
|
|
@NonNull ProcessAction failureProcessAction)
|
|
{
|
|
process((s, p) -> {
|
|
RemotePeer activePeer = s.getCallInfoState().getActivePeer();
|
|
|
|
boolean stateChanged = activePeer == null ||
|
|
remotePeer.getState() != activePeer.getState() ||
|
|
!remotePeer.getCallId().equals(activePeer.getCallId());
|
|
|
|
if (stateChanged) {
|
|
return p.handleMessageSentSuccess(s, remotePeer.getCallId());
|
|
} else {
|
|
return failureProcessAction.process(s, p);
|
|
}
|
|
});
|
|
}
|
|
|
|
interface ProcessAction {
|
|
@NonNull WebRtcServiceState process(@NonNull WebRtcServiceState currentState, @NonNull WebRtcActionProcessor processor);
|
|
}
|
|
}
|