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 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 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 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 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 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 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 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 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 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 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 results = GroupSendUtil.sendCallMessage(context, groupId.requireV2(), recipients, callMessage); Set 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 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> 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 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 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 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 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); } }