Add initial support for Group Calling.

fork-5.53.8
Cody Henthorne 2020-11-11 15:11:03 -05:00
rodzic 696fffb603
commit b1f6786392
53 zmienionych plików z 1887 dodań i 130 usunięć

Wyświetl plik

@ -207,6 +207,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private void initializeResources() {
callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener());
callScreen.setEventListener(new EventListener());
}
private void initializeViewModel() {
@ -381,8 +382,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent);
}
private void handleOutgoingCall() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
if (event.getGroupState().isNotIdle()) {
callScreen.setStatusFromGroupCallState(event.getGroupState());
} else {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
}
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
@ -408,8 +413,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
}
private void handleCallConnected() {
private void handleCallConnected(@NonNull WebRtcViewModel event) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
if (event.getGroupState().isNotIdleOrConnected()) {
callScreen.setStatusFromGroupCallState(event.getGroupState());
}
}
private void handleRecipientUnavailable() {
@ -486,7 +494,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
callScreen.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(); break;
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
@ -496,7 +505,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
case CALL_OUTGOING: handleOutgoingCall(); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
@ -511,6 +520,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
}
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
if (event.getGroupState().isNotIdle()) {
callScreen.setStatusFromGroupCallState(event.getGroupState());
}
}
private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
@ -605,4 +620,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
}
private class EventListener implements WebRtcCallView.EventListener {
@Override
public void onPotentialLayoutChange() {
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
startService(intent);
}
}
}

Wyświetl plik

@ -1,43 +1,91 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.graphics.Point;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.webrtc.EglBase;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import org.whispersystems.libsignal.util.Pair;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.WeakHashMap;
public class BroadcastVideoSink implements VideoSink {
private final EglBase eglBase;
private final WeakHashMap<VideoSink, Boolean> sinks;
private final WeakHashMap<Object, Point> requestingSizes;
public BroadcastVideoSink(@Nullable EglBase eglBase) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.requestingSizes = new WeakHashMap<>();
}
public @Nullable EglBase getEglBase() {
return eglBase;
}
public void addSink(@NonNull VideoSink sink) {
public synchronized void addSink(@NonNull VideoSink sink) {
sinks.put(sink, true);
}
public void removeSink(@NonNull VideoSink sink) {
public synchronized void removeSink(@NonNull VideoSink sink) {
sinks.remove(sink);
}
@Override
public void onFrame(@NonNull VideoFrame videoFrame) {
public synchronized void onFrame(@NonNull VideoFrame videoFrame) {
for (VideoSink sink : sinks.keySet()) {
sink.onFrame(videoFrame);
}
}
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
synchronized (requestingSizes) {
requestingSizes.put(object, size);
}
}
void removeRequestingSize(@NonNull Object object) {
synchronized (requestingSizes) {
requestingSizes.remove(object);
}
}
public @NonNull RequestedSize getMaxRequestingSize() {
int width = 0;
int height = 0;
synchronized (requestingSizes) {
for (Point size : requestingSizes.values()) {
if (width < size.x) {
width = size.x;
height = size.y;
}
}
}
return new RequestedSize(width, height);
}
public static class RequestedSize {
private final int width;
private final int height;
private RequestedSize(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
}

Wyświetl plik

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
@ -32,6 +34,9 @@ public class CallParticipantView extends ConstraintLayout {
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private static final int SMALL_AVATAR = ViewUtil.dpToPx(96);
private static final int LARGE_AVATAR = ViewUtil.dpToPx(112);
private RecipientId recipientId;
private AvatarImageView avatar;
private TextureViewRenderer renderer;
@ -59,6 +64,7 @@ public class CallParticipantView extends ConstraintLayout {
renderer = findViewById(R.id.call_participant_renderer);
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
useLargeAvatar();
}
void setCallParticipant(@NonNull CallParticipant participant) {
@ -89,6 +95,23 @@ public class CallParticipantView extends ConstraintLayout {
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
}
void useLargeAvatar() {
changeAvatarParams(LARGE_AVATAR);
}
void useSmallAvatar() {
changeAvatarParams(SMALL_AVATAR);
}
private void changeAvatarParams(int dimension) {
ViewGroup.LayoutParams params = avatar.getLayoutParams();
if (params.height != dimension) {
params.height = dimension;
params.width = dimension;
avatar.setLayoutParams(params);
}
}
private void setPipAvatar(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);

Wyświetl plik

@ -7,6 +7,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import com.google.android.flexbox.AlignItems;
import com.google.android.flexbox.FlexboxLayout;
@ -14,6 +15,7 @@ import com.google.android.flexbox.FlexboxLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Collections;
import java.util.List;
@ -24,6 +26,9 @@ import java.util.List;
*/
public class CallParticipantsLayout extends FlexboxLayout {
private static final int MULTIPLE_PARTICIPANT_SPACING = ViewUtil.dpToPx(3);
private static final int CORNER_RADIUS = ViewUtil.dpToPx(10);
private List<CallParticipant> callParticipants = Collections.emptyList();
private boolean shouldRenderInPip;
@ -46,17 +51,33 @@ public class CallParticipantsLayout extends FlexboxLayout {
}
private void updateLayout() {
int previousChildCount = getChildCount();
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
updateChildrenCount(1);
update(0, callParticipants.get(0));
update(0, 1, callParticipants.get(0));
} else {
int count = callParticipants.size();
updateChildrenCount(count);
for (int i = 0; i < callParticipants.size(); i++) {
update(i, callParticipants.get(i));
for (int i = 0; i < count; i++) {
update(i, count, callParticipants.get(i));
}
}
if (previousChildCount != getChildCount()) {
updateMarginsForLayout();
}
}
private void updateMarginsForLayout() {
MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
if (callParticipants.size() > 1 && !shouldRenderInPip) {
layoutParams.setMargins(MULTIPLE_PARTICIPANT_SPACING, ViewUtil.getStatusBarHeight(this), MULTIPLE_PARTICIPANT_SPACING, 0);
} else {
layoutParams.setMargins(0, 0, 0, 0);
}
setLayoutParams(layoutParams);
}
private void updateChildrenCount(int count) {
@ -72,15 +93,33 @@ public class CallParticipantsLayout extends FlexboxLayout {
}
}
private void update(int index, @NonNull CallParticipant participant) {
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
private void update(int index, int count, @NonNull CallParticipant participant) {
View view = getChildAt(index);
CardView cardView = view.findViewById(R.id.group_call_participant_card_wrapper);
CallParticipantView callParticipantView = view.findViewById(R.id.group_call_participant);
callParticipantView.setCallParticipant(participant);
callParticipantView.setRenderInPip(shouldRenderInPip);
setChildLayoutParams(callParticipantView, index, getChildCount());
if (count > 1) {
view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING);
cardView.setRadius(CORNER_RADIUS);
} else {
view.setPadding(0, 0, 0, 0);
cardView.setRadius(0);
}
if (count > 2) {
callParticipantView.useSmallAvatar();
} else {
callParticipantView.useLargeAvatar();
}
setChildLayoutParams(view, index, getChildCount());
}
private void addCallParticipantView() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
View view = LayoutInflater.from(getContext()).inflate(R.layout.group_call_participant_item, this, false);
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
params.setAlignSelf(AlignItems.STRETCH);

Wyświetl plik

@ -1,8 +1,11 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
@ -21,24 +24,27 @@ public final class CallParticipantsState {
private static final int SMALL_GROUP_MAX = 6;
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
Collections.emptyList(),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false);
WebRtcViewModel.GroupCallState.IDLE,
Collections.emptyList(),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false);
private final WebRtcViewModel.State callState;
private final List<CallParticipant> remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
private final WebRtcViewModel.State callState;
private final WebRtcViewModel.GroupCallState groupCallState;
private final List<CallParticipant> remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
@NonNull WebRtcViewModel.GroupCallState groupCallState,
@NonNull List<CallParticipant> remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@ -48,6 +54,7 @@ public final class CallParticipantsState {
boolean isViewingFocusedParticipant)
{
this.callState = callState;
this.groupCallState = groupCallState;
this.remoteParticipants = remoteParticipants;
this.localParticipant = localParticipant;
this.localRenderState = localRenderState;
@ -61,6 +68,10 @@ public final class CallParticipantsState {
return callState;
}
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
return groupCallState;
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
@ -87,6 +98,30 @@ public final class CallParticipantsState {
return listParticipants;
}
public @NonNull String getRemoteParticipantsDescription(@NonNull Context context) {
switch (remoteParticipants.size()) {
case 0:
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
case 1:
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getRecipient().getShortDisplayName(context));
} else {
return remoteParticipants.get(0).getRecipient().getDisplayName(context);
}
case 2:
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context));
default:
int others = remoteParticipants.size() - 2;
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
others,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context),
others);
}
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
return remoteParticipants;
}
@ -132,6 +167,7 @@ public final class CallParticipantsState {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
webRtcViewModel.getRemoteParticipants(),
webRtcViewModel.getLocalParticipant(),
focused,
@ -152,6 +188,7 @@ public final class CallParticipantsState {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
@ -172,6 +209,7 @@ public final class CallParticipantsState {
selectedPage == SelectedPage.FOCUSED);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
@ -193,8 +231,8 @@ public final class CallParticipantsState {
if (displayLocal || showVideoForOutgoing) {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}

Wyświetl plik

@ -10,8 +10,12 @@ import android.view.TextureView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.EglBase;
import org.webrtc.EglRenderer;
import org.webrtc.GlRectDrawer;
@ -38,6 +42,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
private int surfaceHeight;
private boolean isInitialized;
private BroadcastVideoSink attachedVideoSink;
private Lifecycle lifecycle;
public TextureViewRenderer(@NonNull Context context) {
super(context);
@ -59,7 +64,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.init(eglBase.getEglBaseContext(), null, EglBase.CONFIG_PLAIN, new GlRectDrawer());
}
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
public void init(@NonNull EglBase.Context sharedContext, @Nullable RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
ThreadUtils.checkIsOnMainThread();
this.rendererEvents = rendererEvents;
@ -67,6 +72,16 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.rotatedFrameHeight = 0;
this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
this.lifecycle = ViewUtil.getActivityLifecycle(this);
if (lifecycle != null) {
lifecycle.addObserver(new DefaultLifecycleObserver() {
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
release();
}
});
}
}
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
@ -76,10 +91,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
attachedVideoSink.removeRequestingSize(this);
}
if (videoSink != null) {
videoSink.addSink(this);
videoSink.putRequestingSize(this, new Point(getWidth(), getHeight()));
} else {
clearImage();
}
@ -90,11 +107,17 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
release();
if (lifecycle == null || lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
release();
}
}
public void release() {
eglRenderer.release();
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
attachedVideoSink.removeRequestingSize(this);
}
}
public void addFrameListener(@NonNull EglRenderer.FrameListener listener, float scale, @NonNull RendererCommon.GlDrawer drawerParam) {
@ -163,6 +186,10 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
setMeasuredDimension(size.x, size.y);
Log.d(TAG, "onMeasure(). New size: " + size.x + "x" + size.y);
if (attachedVideoSink != null) {
attachedVideoSink.putRequestingSize(this, size);
}
}
@Override

Wyświetl plik

@ -5,6 +5,7 @@ import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@ -30,12 +31,14 @@ import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.google.android.material.button.MaterialButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -88,8 +91,10 @@ public class WebRtcCallView extends FrameLayout {
private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler;
private Toolbar toolbar;
private MaterialButton startCall;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private EventListener eventListener;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
@ -142,13 +147,13 @@ public class WebRtcCallView extends FrameLayout {
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
startCall = findViewById(R.id.call_screen_start_call_start_call);
View topGradient = findViewById(R.id.call_screen_header_gradient);
View decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View startCall = findViewById(R.id.call_screen_start_call_start_call);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
@ -163,6 +168,7 @@ public class WebRtcCallView extends FrameLayout {
@Override
public void onPageSelected(int position) {
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
runIfNonNull(eventListener, EventListener::onPotentialLayoutChange);
}
});
@ -233,6 +239,10 @@ public class WebRtcCallView extends FrameLayout {
this.controlsListener = controlsListener;
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
public void setMicEnabled(boolean isMicEnabled) {
micToggle.setChecked(isMicEnabled, false);
}
@ -248,6 +258,10 @@ public class WebRtcCallView extends FrameLayout {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
}
if (state.getGroupCallState().isConnected()) {
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
@ -257,6 +271,10 @@ public class WebRtcCallView extends FrameLayout {
} else {
layoutParticipantsForSmallCount();
}
if (eventListener != null) {
eventListener.onPotentialLayoutChange();
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
@ -283,17 +301,17 @@ public class WebRtcCallView extends FrameLayout {
case SMALL_RECTANGLE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToRectangle();
animatePipToLargeRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case SMALL_SQUARE:
case SMALLER_RECTANGLE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToSquare();
animatePipToSmallRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
@ -341,7 +359,7 @@ public class WebRtcCallView extends FrameLayout {
recipientId = recipient.getId();
if (recipient.isGroup()) {
recipientName.setText(R.string.WebRtcCallView__group_call);
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, recipient.getDisplayName(getContext())));
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
toolbar.inflateMenu(R.menu.group_call);
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
@ -375,6 +393,27 @@ public class WebRtcCallView extends FrameLayout {
}
}
public void setStatusFromGroupCallState(@NonNull WebRtcViewModel.GroupCallState groupCallState) {
switch (groupCallState) {
case DISCONNECTED:
status.setText(R.string.WebRtcCallView__disconnected);
break;
case CONNECTING:
status.setText(R.string.WebRtcCallView__connecting);
break;
case RECONNECTING:
status.setText(R.string.WebRtcCallView__reconnecting);
break;
case CONNECTED_AND_JOINING:
status.setText(R.string.WebRtcCallView__joining);
break;
case CONNECTED_AND_JOINED:
case CONNECTED:
status.setText("");
break;
}
}
public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
@ -383,6 +422,14 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(startCallControls);
startCall.setText(webRtcControls.getStartCallButtonText());
}
MenuItem item = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list);
if (item != null) {
item.setVisible(webRtcControls.displayGroupMembersButton());
item.setEnabled(webRtcControls.displayGroupMembersButton());
}
if (webRtcControls.displayTopViews()) {
@ -462,7 +509,7 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle;
}
private void animatePipToRectangle() {
private void animatePipToLargeRectangle() {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@ -476,11 +523,11 @@ public class WebRtcCallView extends FrameLayout {
smallLocalRenderFrame.startAnimation(animation);
}
private void animatePipToSquare() {
private void animatePipToSmallRectangle() {
pictureInPictureGestureHelper.lockToBottomEnd();
pictureInPictureGestureHelper.performAfterFling(() -> {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(40), ViewUtil.dpToPx(72));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
@ -606,9 +653,9 @@ public class WebRtcCallView extends FrameLayout {
getHandler().removeCallbacks(fadeOutRunnable);
}
private static void runIfNonNull(@Nullable ControlsListener controlsListener, @NonNull Consumer<ControlsListener> controlsListenerConsumer) {
if (controlsListener != null) {
controlsListenerConsumer.accept(controlsListener);
private static <T> void runIfNonNull(@Nullable T listener, @NonNull Consumer<T> listenerConsumer) {
if (listener != null) {
listenerConsumer.accept(listener);
}
}
@ -648,4 +695,8 @@ public class WebRtcCallView extends FrameLayout {
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
}
public interface EventListener {
void onPotentialLayoutChange();
}
}

Wyświetl plik

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel {
@ -104,11 +105,13 @@ public class WebRtcCallViewModel extends ViewModel {
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
localParticipant.getCameraState().isEnabled(),
webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(),
localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(),
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
repository.getAudioOutput());
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
@ -133,11 +136,13 @@ public class WebRtcCallViewModel extends ViewModel {
}
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
@NonNull WebRtcViewModel.GroupCallState groupState,
boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean hasAtLeastOneRemote,
@NonNull WebRtcAudioOutput audioOutput)
{
final WebRtcControls.CallState callState;
@ -166,12 +171,34 @@ public class WebRtcCallViewModel extends ViewModel {
callState = WebRtcControls.CallState.ONGOING;
}
final WebRtcControls.GroupCallState groupCallState;
switch (groupState) {
case DISCONNECTED:
groupCallState = WebRtcControls.GroupCallState.DISCONNECTED;
break;
case CONNECTING:
case RECONNECTING:
groupCallState = WebRtcControls.GroupCallState.CONNECTING;
break;
case CONNECTED:
case CONNECTED_AND_JOINING:
case CONNECTED_AND_JOINED:
groupCallState = WebRtcControls.GroupCallState.CONNECTED;
break;
default:
groupCallState = WebRtcControls.GroupCallState.NONE;
break;
}
webRtcControls.setValue(new WebRtcControls(isLocalVideoEnabled,
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
Boolean.TRUE.equals(isInPipMode.getValue()),
hasAtLeastOneRemote,
callState,
groupCallState,
audioOutput));
}

Wyświetl plik

@ -1,22 +1,27 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
public final class WebRtcControls {
public static final WebRtcControls NONE = new WebRtcControls();
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, CallState.NONE, WebRtcAudioOutput.HANDSET);
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
private final boolean isMoreThanOneCameraAvailable;
private final boolean isBluetoothAvailable;
private final boolean isInPipMode;
private final boolean hasAtLeastOneRemote;
private final CallState callState;
private final GroupCallState groupCallState;
private final WebRtcAudioOutput audioOutput;
private WebRtcControls() {
this(false, false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
}
WebRtcControls(boolean isLocalVideoEnabled,
@ -24,7 +29,9 @@ public final class WebRtcControls {
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean isInPipMode,
boolean hasAtLeastOneRemote,
@NonNull CallState callState,
@NonNull GroupCallState groupCallState,
@NonNull WebRtcAudioOutput audioOutput)
{
this.isLocalVideoEnabled = isLocalVideoEnabled;
@ -32,7 +39,9 @@ public final class WebRtcControls {
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
this.isInPipMode = isInPipMode;
this.hasAtLeastOneRemote = hasAtLeastOneRemote;
this.callState = callState;
this.groupCallState = groupCallState;
this.audioOutput = audioOutput;
}
@ -40,6 +49,17 @@ public final class WebRtcControls {
return isPreJoin();
}
@StringRes int getStartCallButtonText() {
if (isGroupCall() && hasAtLeastOneRemote) {
return R.string.WebRtcCallView__join_call;
}
return R.string.WebRtcCallView__start_call;
}
boolean displayGroupMembersButton() {
return groupCallState == GroupCallState.CONNECTED;
}
boolean displayEndCall() {
return isAtLeastOutgoing();
}
@ -116,6 +136,10 @@ public final class WebRtcControls {
return callState.isAtLeast(CallState.OUTGOING);
}
private boolean isGroupCall() {
return groupCallState != GroupCallState.NONE;
}
public enum CallState {
NONE,
PRE_JOIN,
@ -124,8 +148,16 @@ public final class WebRtcControls {
ONGOING,
ENDING;
boolean isAtLeast(@NonNull CallState other) {
boolean isAtLeast(@SuppressWarnings("SameParameterValue") @NonNull CallState other) {
return compareTo(other) >= 0;
}
}
public enum GroupCallState {
NONE,
DISCONNECTED,
CONNECTING,
CONNECTED,
RECONNECTING
}
}

Wyświetl plik

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState {
GONE,
SMALL_RECTANGLE,
SMALL_SQUARE,
SMALLER_RECTANGLE,
LARGE,
LARGE_NO_VIDEO
}

Wyświetl plik

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.MappingModel;
@ -79,9 +80,14 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
List<MappingModel<?>> items = new ArrayList<>();
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
if (includeSelf) {
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
}
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}

Wyświetl plik

@ -818,6 +818,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
} else if (isGroupConversation()) {
if (isActiveV2Group && FeatureFlags.groupCalling()) {
inflater.inflate(R.menu.conversation_callable_groupv2, menu);
}
inflater.inflate(R.menu.conversation_group_options, menu);
if (!isPushGroupConversation()) {

Wyświetl plik

@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Objects;
/**
* Allow system to identify a call participant by their device demux id and their
* recipient id.
*/
public final class CallParticipantId {
private static final long DEFAULT_ID = -1;
private final long demuxId;
private final RecipientId recipientId;
public CallParticipantId(@NonNull Recipient recipient) {
this(DEFAULT_ID, recipient.getId());
}
public CallParticipantId(long demuxId, @NonNull RecipientId recipientId) {
this.demuxId = demuxId;
this.recipientId = recipientId;
}
public long getDemuxId() {
return demuxId;
}
public @NonNull RecipientId getRecipientId() {
return recipientId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final CallParticipantId that = (CallParticipantId) o;
return demuxId == that.demuxId &&
recipientId.equals(that.recipientId);
}
@Override
public int hashCode() {
return Objects.hash(demuxId, recipientId);
}
}

Wyświetl plik

@ -45,8 +45,45 @@ public class WebRtcViewModel {
}
}
private final @NonNull State state;
private final @NonNull Recipient recipient;
public enum GroupCallState {
IDLE,
DISCONNECTED,
CONNECTING,
RECONNECTING,
CONNECTED,
CONNECTED_AND_JOINING,
CONNECTED_AND_JOINED;
public boolean isNotIdle() {
return this != IDLE;
}
public boolean isConnected() {
switch (this) {
case CONNECTED:
case CONNECTED_AND_JOINING:
case CONNECTED_AND_JOINED:
return true;
}
return false;
}
public boolean isNotIdleOrConnected() {
switch (this) {
case DISCONNECTED:
case CONNECTING:
case RECONNECTING:
return true;
}
return false;
}
}
private final @NonNull State state;
private final @NonNull GroupCallState groupState;
private final @NonNull Recipient recipient;
private final boolean isBluetoothAvailable;
private final boolean isRemoteVideoOffer;
@ -56,6 +93,7 @@ public class WebRtcViewModel {
private final List<CallParticipant> remoteParticipants;
public WebRtcViewModel(@NonNull State state,
@NonNull GroupCallState groupState,
@NonNull Recipient recipient,
@NonNull CameraState localCameraState,
@Nullable BroadcastVideoSink localSink,
@ -66,6 +104,7 @@ public class WebRtcViewModel {
@NonNull List<CallParticipant> remoteParticipants)
{
this.state = state;
this.groupState = groupState;
this.recipient = recipient;
this.isBluetoothAvailable = isBluetoothAvailable;
this.isRemoteVideoOffer = isRemoteVideoOffer;
@ -79,12 +118,16 @@ public class WebRtcViewModel {
return state;
}
public @NonNull GroupCallState getGroupState() {
return groupState;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public boolean isRemoteVideoEnabled() {
return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled);
return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled) || (groupState.isNotIdle() && remoteParticipants.size() > 1);
}
public boolean isBluetoothAvailable() {

Wyświetl plik

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.zkgroup.VerificationFailedException;
@ -25,7 +26,9 @@ import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public final class GroupManager {
@ -390,6 +393,19 @@ public final class GroupManager {
new GroupManagerV2(context).sendNoopGroupUpdate(groupMasterKey, currentState);
}
@WorkerThread
public static @NonNull GroupExternalCredential getGroupExternalCredential(@NonNull Context context,
@NonNull GroupId.V2 groupId)
throws IOException, VerificationFailedException
{
return new GroupManagerV2(context).getGroupExternalCredential(groupId);
}
@WorkerThread
public static @NonNull Map<UUID, UuidCiphertext> getUuidCipherTexts(@NonNull Context context, @NonNull GroupId.V2 groupId) {
return new GroupManagerV2(context).getUuidCipherTexts(groupId);
}
public static class GroupActionResult {
private final Recipient groupRecipient;
private final long threadId;

Wyświetl plik

@ -13,6 +13,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
@ -22,6 +23,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.groups.UuidCiphertext;
@ -30,7 +32,6 @@ import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -69,8 +70,10 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@ -109,6 +112,35 @@ final class GroupManagerV2 {
authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
}
@WorkerThread
@NonNull GroupExternalCredential getGroupExternalCredential(@NonNull GroupId.V2 groupId)
throws IOException, VerificationFailedException
{
GroupMasterKey groupMasterKey = DatabaseFactory.getGroupDatabase(context)
.requireGroup(groupId)
.requireV2GroupProperties()
.getGroupMasterKey();
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return groupsV2Api.getGroupExternalCredential(authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
}
@WorkerThread
@NonNull Map<UUID, UuidCiphertext> getUuidCipherTexts(@NonNull GroupId.V2 groupId) {
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId);
GroupMasterKey groupMasterKey = groupRecord.requireV2GroupProperties().getGroupMasterKey();
ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
List<Recipient> recipients = Recipient.resolvedList(groupRecord.getMembers());
Map<UUID, UuidCiphertext> uuidCipherTexts = new HashMap<>();
for (Recipient recipient : recipients) {
uuidCipherTexts.put(recipient.requireUuid(), clientZkGroupCipher.encryptUuid(recipient.requireUuid()));
}
return uuidCipherTexts;
}
@WorkerThread
GroupCreator create() throws GroupChangeBusyException {
return new GroupCreator(GroupsV2ProcessingLock.acquireGroupProcessingLock());

Wyświetl plik

@ -109,6 +109,7 @@ 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.OpaqueMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
@ -423,6 +424,7 @@ public final class PushProcessMessageJob extends BaseJob {
else if (message.getIceUpdateMessages().isPresent()) handleCallIceUpdateMessage(content, message.getIceUpdateMessages().get());
else if (message.getHangupMessage().isPresent()) handleCallHangupMessage(content, message.getHangupMessage().get(), smsMessageId);
else if (message.getBusyMessage().isPresent()) handleCallBusyMessage(content, message.getBusyMessage().get());
else if (message.getOpaqueMessage().isPresent()) handleCallOpaqueMessage(content, message.getOpaqueMessage().get());
} else if (content.getReceiptMessage().isPresent()) {
SignalServiceReceiptMessage message = content.getReceiptMessage().get();
@ -625,6 +627,27 @@ public final class PushProcessMessageJob extends BaseJob {
context.startService(intent);
}
private void handleCallOpaqueMessage(@NonNull SignalServiceContent content,
@NonNull OpaqueMessage message)
{
log(TAG, String.valueOf(content.getTimestamp()), "handleCallOpaqueMessage");
Intent intent = new Intent(context, WebRtcCallService.class);
long messageAgeSeconds = 0;
if (content.getServerReceivedTimestamp() > 0 && content.getServerDeliveredTimestamp() >= content.getServerReceivedTimestamp()) {
messageAgeSeconds = (content.getServerDeliveredTimestamp() - content.getServerReceivedTimestamp()) / 1000;
}
intent.setAction(WebRtcCallService.ACTION_RECEIVE_OPAQUE_MESSAGE)
.putExtra(WebRtcCallService.EXTRA_OPAQUE_MESSAGE, message.getOpaque())
.putExtra(WebRtcCallService.EXTRA_UUID, Recipient.externalHighTrustPush(context, content.getSender()).requireUuid().toString())
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_MESSAGE_AGE_SECONDS, messageAgeSeconds);
context.startService(intent);
}
private void handleEndSessionMessage(@NonNull SignalServiceContent content,
@NonNull Optional<Long> smsMessageId)
{

Wyświetl plik

@ -15,14 +15,19 @@ import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.CallManager.CallEvent;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.HttpHeader;
import org.signal.ringrtc.IceCandidate;
import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.zkgroup.VerificationFailedException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
@ -30,8 +35,10 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
@ -55,12 +62,15 @@ 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.CallingResponse;
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.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@ -71,8 +81,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WebRtcCallService extends Service implements CallManager.Observer,
BluetoothStateManager.BluetoothStateListener,
CameraEventListener
BluetoothStateManager.BluetoothStateListener,
CameraEventListener,
GroupCall.Observer
{
private static final String TAG = WebRtcCallService.class.getSimpleName();
@ -107,6 +118,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
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 EXTRA_GROUP_EXTERNAL_TOKEN = "group_external_token";
public static final String EXTRA_HTTP_REQUEST_ID = "http_request_id";
public static final String EXTRA_HTTP_RESPONSE_STATUS = "http_response_status";
public static final String EXTRA_HTTP_RESPONSE_BODY = "http_response_body";
public static final String EXTRA_OPAQUE_MESSAGE = "opaque";
public static final String EXTRA_UUID = "uuid";
public static final String EXTRA_MESSAGE_AGE_SECONDS = "message_age_seconds";
public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN";
public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL";
@ -158,6 +176,17 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
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 String ACTION_SETUP_FAILURE = "SETUP_FAILURE";
public static final String ACTION_HTTP_SUCCESS = "HTTP_SUCCESS";
public static final String ACTION_HTTP_FAILURE = "HTTP_FAILURE";
public static final String ACTION_SEND_OPAQUE_MESSAGE = "SEND_OPAQUE_MESSAGE";
public static final String ACTION_RECEIVE_OPAQUE_MESSAGE = "RECEIVE_OPAQUE_MESSAGE";
public static final String ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED = "GROUP_LOCAL_DEVICE_CHANGE";
public static final String ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED = "GROUP_REMOTE_DEVICE_CHANGE";
public static final String ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED = "GROUP_JOINED_MEMBERS_CHANGE";
public static final String ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF = "GROUP_REQUEST_MEMBERSHIP_PROOF";
public static final String ACTION_GROUP_REQUEST_UPDATE_MEMBERS = "GROUP_REQUEST_UPDATE_MEMBERS";
public static final String ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS = "GROUP_UPDATE_RENDERED_RESOLUTIONS";
public static final int BUSY_TONE_LENGTH = 2000;
@ -215,7 +244,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
return false;
}
webRtcInteractor = new WebRtcInteractor(this, callManager, lockManager, new SignalAudioManager(this), bluetoothStateManager, this);
webRtcInteractor = new WebRtcInteractor(this,
callManager,
lockManager,
new SignalAudioManager(this),
bluetoothStateManager,
this,
this);
return true;
}
@ -366,9 +401,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
});
}
public void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
public void setCallInProgressNotification(int type, @NonNull Recipient recipient) {
startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type),
CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient()));
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient));
}
public void sendMessage() {
@ -377,6 +412,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public void sendMessage(@NonNull WebRtcServiceState state) {
EventBus.getDefault().postSticky(new WebRtcViewModel(state.getCallInfoState().getCallState(),
state.getCallInfoState().getGroupCallState(),
state.getCallInfoState().getCallRecipient(),
state.getLocalDeviceState().getCameraState(),
state.getVideoState().getLocalSink(),
@ -612,6 +648,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
listenableFutureTask.addListener(new SendCallMessageListener<>(remotePeer));
}
public void sendOpaqueCallMessage(@NonNull UUID uuid, @NonNull SignalServiceCallMessage opaqueMessage) {
sendMessage(new RemotePeer(RecipientId.from(uuid, null)), opaqueMessage);
}
@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);
@ -871,12 +911,93 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
@Override
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] bytes) {
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] opaque) {
Log.i(TAG, "onSendCallMessage:");
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(ACTION_SEND_OPAQUE_MESSAGE)
.putExtra(EXTRA_UUID, uuid.toString())
.putExtra(EXTRA_OPAQUE_MESSAGE, opaque);
startService(intent);
}
@Override
public void onSendHttpRequest(long l, @NonNull String s, @NonNull CallManager.HttpMethod httpMethod, @Nullable List<HttpHeader> list, @Nullable byte[] bytes) {
Log.i(TAG, "onSendHttpRequest:");
public void onSendHttpRequest(long requestId, @NonNull String url, @NonNull CallManager.HttpMethod httpMethod, @Nullable List<HttpHeader> headers, @Nullable byte[] body) {
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 = messageSender.makeCallingRequest(requestId, url, httpMethod.name(), headerPairs, body);
Intent intent = new Intent(this, WebRtcCallService.class);
if (response instanceof CallingResponse.Success) {
CallingResponse.Success success = (CallingResponse.Success) response;
intent.setAction(ACTION_HTTP_SUCCESS)
.putExtra(EXTRA_HTTP_REQUEST_ID, success.getRequestId())
.putExtra(EXTRA_HTTP_RESPONSE_STATUS, success.getResponseStatus())
.putExtra(EXTRA_HTTP_RESPONSE_BODY, success.getResponseBody());
} else {
intent.setAction(ACTION_HTTP_FAILURE)
.putExtra(EXTRA_HTTP_REQUEST_ID, response.getRequestId());
}
startService(intent);
});
}
@Override
public void requestMembershipProof(@NonNull GroupCall groupCall) {
Log.i(TAG, "requestMembershipProof():");
networkExecutor.execute(() -> {
try {
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(this, serviceState.getCallInfoState().getCallRecipient().getGroupId().get().requireV2());
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF)
.putExtra(EXTRA_GROUP_EXTERNAL_TOKEN, credential.getTokenBytes().toByteArray());
startService(intent);
} catch (IOException | VerificationFailedException e) {
Log.w(TAG, "Unable to fetch group membership proof", e);
}
});
}
@Override
public void requestGroupMembers(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_REQUEST_UPDATE_MEMBERS));
}
@Override
public void onLocalDeviceStateChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED));
}
@Override
public void onRemoteDeviceStatesChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED));
}
@Override
public void onJoinedMembersChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED));
}
@Override
public void onEnded(@NonNull GroupCall groupCall, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
Log.i(TAG, "onEnded: " + groupCallEndReason);
}
}

Wyświetl plik

@ -116,7 +116,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
Log.i(tag, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId());
CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
CallParticipant newParticipant = oldParticipant.withVideoEnabled(enable);
return currentState.builder()

Wyświetl plik

@ -0,0 +1,214 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.util.LongSparseArray;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.VideoTrack;
import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Base group call action processor that handles general callbacks around call members
* and call specific setup information that is the same for any group call state.
*/
public class GroupActionProcessor extends DeviceAwareActionProcessor {
public GroupActionProcessor(@NonNull WebRtcInteractor webRtcInteractor, @NonNull String tag) {
super(webRtcInteractor, tag);
}
@Override
protected @NonNull WebRtcServiceState handleGroupRemoteDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRemoteDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
WebRtcServiceStateBuilder.CallInfoStateBuilder builder = currentState.builder()
.changeCallInfoState()
.clearParticipantMap();
LongSparseArray<GroupCall.RemoteDeviceState> remoteDevices = groupCall.getRemoteDeviceStates();
for(int i = 0; i < remoteDevices.size(); i++) {
GroupCall.RemoteDeviceState device = remoteDevices.get(remoteDevices.keyAt(i));
Recipient recipient = Recipient.externalPush(context, device.getUserId(), null, false);
CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId());
CallParticipant callParticipant = participants.get(callParticipantId);
BroadcastVideoSink videoSink;
VideoTrack videoTrack = device.getVideoTrack();
if (videoTrack != null) {
videoSink = (callParticipant != null && callParticipant.getVideoSink().getEglBase() != null) ? callParticipant.getVideoSink()
: new BroadcastVideoSink(currentState.getVideoState().requireEglBase());
videoTrack.addSink(videoSink);
} else {
videoSink = new BroadcastVideoSink(null);
}
builder.putParticipant(callParticipantId,
CallParticipant.createRemote(recipient,
null,
videoSink,
true));
}
return builder.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
Log.i(tag, "handleGroupRequestMembershipProof():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.setMembershipProof(groupMembershipToken);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set group membership proof", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupRequestUpdateMembers(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRequestUpdateMembers():");
Recipient group = currentState.getCallInfoState().getCallRecipient();
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(context, group.requireGroupId().requireV2()))
.map(e -> new GroupCall.GroupMemberInfo(e.getKey(), e.getValue().serialize()))
.toList();
try {
groupCall.setGroupMembers(new ArrayList<>(members));
} catch (CallException e) {
return groupCallFailure(currentState, "Unable set group members", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
ArrayList<GroupCall.RenderedResolution> renderedResolutions = new ArrayList<>(participants.size());
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
BroadcastVideoSink.RequestedSize maxSize = entry.getValue().getVideoSink().getMaxRequestingSize();
renderedResolutions.add(new GroupCall.RenderedResolution(entry.getKey().getDemuxId(), maxSize.getWidth(), maxSize.getHeight(), null));
}
try {
currentState.getCallInfoState().requireGroupCall().setRenderedResolutions(renderedResolutions);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set rendered resolutions", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
try {
webRtcInteractor.getCallManager().receivedHttpResponse(httpData.getRequestId(), httpData.getStatus(), httpData.getBody() != null ? httpData.getBody() : new byte[0]);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
try {
webRtcInteractor.getCallManager().httpRequestFailed(httpData.getRequestId());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSendOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleSendOpaqueMessage():");
OpaqueMessage opaqueMessage = new OpaqueMessage(opaqueMessageMetadata.getOpaque());
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOpaque(opaqueMessage, true, null);
webRtcInteractor.sendOpaqueCallMessage(opaqueMessageMetadata.getUuid(), callMessage);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():");
try {
webRtcInteractor.getCallManager().receivedCallMessage(opaqueMessageMetadata.getUuid(),
opaqueMessageMetadata.getRemoteDeviceId(),
1,
opaqueMessageMetadata.getOpaque(),
opaqueMessageMetadata.getMessageAgeSeconds());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to receive opaque message", e);
}
return currentState;
}
public @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
try {
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
if (groupCall != null) {
groupCall.disconnect();
}
webRtcInteractor.getCallManager().reset();
} catch (CallException e) {
Log.w(tag, "Unable to reset call manager: ", e);
}
return terminateGroupCall(currentState);
}
public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.stopForegroundService();
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
WebRtcVideoUtil.deinitializeVideo(currentState);
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
}

Wyświetl plik

@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
/**
* Process actions for when the call has at least once been connected and joined.
*/
public class GroupConnectedActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupConnectedActionProcessor.class);
public GroupConnectedActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(TAG, "handleSetEnableVideo():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
Camera camera = currentState.getVideoState().requireCamera();
try {
groupCall.setOutgoingVideoMuted(!enable);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable set video muted", e);
}
camera.setEnabled(enable);
currentState = currentState.builder()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor.getWebRtcCallService(), currentState.getCallSetupState().isEnableVideoOnCreate());
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
try {
currentState.getCallInfoState().requireGroupCall().setOutgoingAudioMuted(muted);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set audio muted", e);
}
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "handleLocalHangup():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
return terminateGroupCall(currentState);
}
}

Wyświetl plik

@ -0,0 +1,149 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
/**
* Process actions to go from lobby to a joined call.
*/
public class GroupJoiningActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupJoiningActionProcessor.class);
private final CallSetupActionProcessorDelegate callSetupDelegate;
public GroupJoiningActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
callSetupDelegate = new CallSetupActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
GroupCall.LocalDeviceState device = groupCall.getLocalDeviceState();
Log.i(tag, "local device changed: " + device.getConnectionState() + " " + device.getJoinState());
WebRtcServiceStateBuilder builder = currentState.builder();
switch (device.getConnectionState()) {
case NOT_CONNECTED:
case RECONNECTING:
builder.changeCallInfoState()
.groupCallState(WebRtcUtil.groupCallStateForConnection(device.getConnectionState()))
.commit();
break;
case CONNECTING:
case CONNECTED:
if (device.getJoinState() == GroupCall.JoinState.JOINED) {
webRtcInteractor.startAudioCommunication(true);
webRtcInteractor.setWantsBluetoothConnection(true);
if (currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
} else {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, currentState.getCallInfoState().getCallRecipient());
try {
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
} catch (CallException e) {
Log.e(tag, e);
throw new RuntimeException(e);
}
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_CONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED)
.callConnectedTime(System.currentTimeMillis())
.commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor))
.build();
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
builder.changeCallInfoState()
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.commit();
} else {
builder.changeCallInfoState()
.groupCallState(WebRtcUtil.groupCallStateForConnection(device.getConnectionState()))
.commit();
}
break;
}
return builder.build();
}
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleLocalHangup():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
return terminateGroupCall(currentState);
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
Camera camera = currentState.getVideoState().requireCamera();
try {
groupCall.setOutgoingVideoMuted(!enable);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set video muted", e);
}
camera.setEnabled(enable);
currentState = currentState.builder()
.changeLocalDeviceState()
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor.getWebRtcCallService(), currentState.getCallSetupState().isEnableVideoOnCreate());
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
try {
currentState.getCallInfoState().requireGroupCall().setOutgoingAudioMuted(muted);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set audio muted", e);
}
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
}

Wyświetl plik

@ -0,0 +1,175 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
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.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.List;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_RINGING;
/**
* Process actions while the user is in the pre-join lobby for the call.
*/
public class GroupPreJoinActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupPreJoinActionProcessor.class);
public GroupPreJoinActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handlePreJoinCall():");
byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId();
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
currentState.getVideoState().requireEglBase(),
webRtcInteractor.getGroupCallObserver());
try {
groupCall.setOutgoingAudioMuted(true);
groupCall.setOutgoingVideoMuted(true);
Log.i(TAG, "Connecting to group call: " + currentState.getCallInfoState().getCallRecipient().getId());
groupCall.connect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to connect to group call", e);
}
return currentState.builder()
.changeCallInfoState()
.groupCall(groupCall)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleCancelPreJoinCall(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "handleCancelPreJoinCall():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
WebRtcVideoUtil.deinitializeVideo(currentState);
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
@Override
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
GroupCall.LocalDeviceState device = groupCall.getLocalDeviceState();
Log.i(tag, "local device changed: " + device.getConnectionState() + " " + device.getJoinState());
return currentState.builder()
.changeCallInfoState()
.groupCallState(WebRtcUtil.groupCallStateForConnection(device.getConnectionState()))
.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
List<Recipient> callParticipants = Stream.of(groupCall.getJoinedGroupMembers())
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
.toList();
WebRtcServiceStateBuilder.CallInfoStateBuilder builder = currentState.builder()
.changeCallInfoState();
for (Recipient recipient : callParticipants) {
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), false));
}
return builder.build();
}
@Override
protected @NonNull WebRtcServiceState handleOutgoingCall(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull OfferMessage.Type offerType)
{
Log.i(TAG, "handleOutgoingCall():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
currentState = WebRtcVideoUtil.reinitializeCamera(context, webRtcInteractor.getCameraEventListener(), currentState);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.setWantsBluetoothConnection(true);
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, currentState.getCallInfoState().getCallRecipient());
try {
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
groupCall.setBandwidthMode(GroupCall.BandwidthMode.NORMAL);
groupCall.join();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to join group call", e);
}
return currentState.builder()
.actionProcessor(new GroupJoiningActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_OUTGOING)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(TAG, "handleSetEnableVideo(): Changing for pre-join group call. enable: " + enable);
currentState.getVideoState().requireCamera().setEnabled(enable);
return currentState.builder()
.changeCallSetupState()
.enableVideoOnCreate(enable)
.commit()
.changeLocalDeviceState()
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
Log.i(TAG, "handleSetMuteAudio(): Changing for pre-join group call. muted: " + muted);
return currentState.builder()
.changeLocalDeviceState()
.isMicrophoneEnabled(!muted)
.build();
}
}

Wyświetl plik

@ -12,8 +12,6 @@ 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
@ -52,14 +50,21 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handlePreJoinCall():");
WebRtcServiceState newState = initializeVanityCamera(WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState));
boolean isGroupCall = remotePeer.getRecipient().isPushV2Group();
WebRtcActionProcessor processor = isGroupCall ? new GroupPreJoinActionProcessor(webRtcInteractor)
: new PreJoinActionProcessor(webRtcInteractor);
return newState.builder()
.actionProcessor(new PreJoinActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_PRE_JOIN)
.callRecipient(remotePeer.getRecipient())
.build();
currentState = initializeVanityCamera(WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState));
currentState = currentState.builder()
.actionProcessor(processor)
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_PRE_JOIN)
.callRecipient(remotePeer.getRecipient())
.build();
return isGroupCall ? currentState.getActionProcessor().handlePreJoinCall(currentState, remotePeer)
: currentState;
}
private @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) {

Wyświetl plik

@ -80,7 +80,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn;
VideoState videoState = currentState.getVideoState();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
try {
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),

Wyświetl plik

@ -107,7 +107,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
try {
VideoState videoState = currentState.getVideoState();
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context,

Wyświetl plik

@ -19,6 +19,7 @@ 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.HttpData;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
@ -57,6 +58,14 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_
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_GROUP_JOINED_MEMBERSHIP_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REQUEST_UPDATE_MEMBERS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_HTTP_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_HTTP_SUCCESS;
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;
@ -71,6 +80,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_RECEIV
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_RECEIVE_OPAQUE_MESSAGE;
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;
@ -79,6 +89,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SEND_B
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_SEND_OPAQUE_MESSAGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SETUP_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_AUDIO_BLUETOOTH;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_SET_AUDIO_SPEAKER;
@ -90,20 +101,22 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_TURN_S
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_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.OpaqueMessageMetadata;
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.getCameraState;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getEnable;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorCallState;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorIdentityKey;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupMembershipToken;
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;
@ -189,7 +202,7 @@ public abstract class WebRtcActionProcessor {
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));
case ACTION_CAMERA_SWITCH_COMPLETED: return handleCameraSwitchCompleted(currentState, getCameraState(intent));
// End Call Actions
case ACTION_ENDED_REMOTE_HANGUP:
@ -208,6 +221,20 @@ public abstract class WebRtcActionProcessor {
// Local Call Failure Actions
case ACTION_SETUP_FAILURE: return handleSetupFailure(currentState, getCallId(intent));
// Group Calling
case ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED: return handleGroupLocalDeviceStateChanged(currentState);
case ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED: return handleGroupRemoteDeviceStateChanged(currentState);
case ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED: return handleGroupJoinedMembershipChanged(currentState);
case ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF: return handleGroupRequestMembershipProof(currentState, getGroupMembershipToken(intent));
case ACTION_GROUP_REQUEST_UPDATE_MEMBERS: return handleGroupRequestUpdateMembers(currentState);
case ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS: return handleUpdateRenderedResolutions(currentState);
case ACTION_HTTP_SUCCESS: return handleHttpSuccess(currentState, HttpData.fromIntent(intent));
case ACTION_HTTP_FAILURE: return handleHttpFailure(currentState, HttpData.fromIntent(intent));
case ACTION_SEND_OPAQUE_MESSAGE: return handleSendOpaqueMessage(currentState, OpaqueMessageMetadata.fromIntent(intent));
case ACTION_RECEIVE_OPAQUE_MESSAGE: return handleReceivedOpaqueMessage(currentState, OpaqueMessageMetadata.fromIntent(intent));
}
return currentState;
@ -275,8 +302,8 @@ public abstract class WebRtcActionProcessor {
//region Incoming call
protected @NonNull WebRtcServiceState handleReceivedOffer(@NonNull WebRtcServiceState currentState,
@NonNull WebRtcData.CallMetadata callMetadata,
@NonNull WebRtcData.OfferMetadata offerMetadata,
@NonNull CallMetadata callMetadata,
@NonNull OfferMetadata offerMetadata,
@NonNull ReceivedOfferMetadata receivedOfferMetadata)
{
Log.i(tag, "handleReceivedOffer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
@ -386,7 +413,7 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleSendBusy(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.CallMetadata callMetadata, boolean broadcast) {
protected @NonNull WebRtcServiceState handleSendBusy(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, boolean broadcast) {
Log.i(tag, "handleSendBusy(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
BusyMessage busyMessage = new BusyMessage(callMetadata.getCallId().longValue());
@ -473,7 +500,7 @@ public abstract class WebRtcActionProcessor {
WebRtcServiceStateBuilder builder = currentState.builder();
if (errorCallState == WebRtcViewModel.State.UNTRUSTED_IDENTITY) {
CallParticipant participant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteParticipant(activePeer.getRecipient()));
CallParticipant participant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
CallParticipant untrusted = participant.withIdentityKey(identityKey.get());
builder.changeCallInfoState()
@ -648,4 +675,68 @@ public abstract class WebRtcActionProcessor {
}
//endregion
//region Group Calling
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRemoteDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRemoteDeviceStateChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
Log.i(tag, "handleGroupRequestMembershipProof not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRequestUpdateMembers(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupRequestUpdateMembers not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleUpdateRenderedResolutions not processed");
return currentState;
}
//endregion
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) {
try {
webRtcInteractor.getCallManager().receivedHttpResponse(httpData.getRequestId(), httpData.getStatus(), httpData.getBody() != null ? httpData.getBody() : new byte[0]);
} catch (CallException e) {
return callFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) {
try {
webRtcInteractor.getCallManager().httpRequestFailed(httpData.getRequestId());
} catch (CallException e) {
return callFailure(currentState, "Unable to process received http response", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleSendOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleSendOpaqueMessage not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage not processed");
return currentState;
}
}

Wyświetl plik

@ -11,11 +11,18 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.UUID;
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_HTTP_REQUEST_ID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_RESPONSE_BODY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_RESPONSE_STATUS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MESSAGE_AGE_SECONDS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemoteDevice;
/**
* Collection of classes to ease parsing data from intents and passing said data
@ -224,4 +231,77 @@ public class WebRtcData {
return deviceId;
}
}
/**
* Http response data.
*/
static class HttpData {
private final long requestId;
private final int status;
private final byte[] body;
static @NonNull HttpData fromIntent(@NonNull Intent intent) {
return new HttpData(intent.getLongExtra(EXTRA_HTTP_REQUEST_ID, -1),
intent.getIntExtra(EXTRA_HTTP_RESPONSE_STATUS, -1),
intent.getByteArrayExtra(EXTRA_HTTP_RESPONSE_BODY));
}
HttpData(long requestId, int status, @Nullable byte[] body) {
this.requestId = requestId;
this.status = status;
this.body = body;
}
long getRequestId() {
return requestId;
}
int getStatus() {
return status;
}
@Nullable byte[] getBody() {
return body;
}
}
/**
* An opaque calling message.
*/
static class OpaqueMessageMetadata {
private final UUID uuid;
private final byte[] opaque;
private final int remoteDeviceId;
private final long messageAgeSeconds;
static @NonNull OpaqueMessageMetadata fromIntent(@NonNull Intent intent) {
return new OpaqueMessageMetadata(WebRtcIntentParser.getUuid(intent),
WebRtcIntentParser.getOpaque(intent),
getRemoteDevice(intent),
intent.getLongExtra(EXTRA_MESSAGE_AGE_SECONDS, 0));
}
OpaqueMessageMetadata(@NonNull UUID uuid, @NonNull byte[] opaque, int remoteDeviceId, long messageAgeSeconds) {
this.uuid = uuid;
this.opaque = opaque;
this.remoteDeviceId = remoteDeviceId;
this.messageAgeSeconds = messageAgeSeconds;
}
@NonNull UUID getUuid() {
return uuid;
}
@NonNull byte[] getOpaque() {
return opaque;
}
int getRemoteDeviceId() {
return remoteDeviceId;
}
long getMessageAgeSeconds() {
return messageAgeSeconds;
}
}
}

Wyświetl plik

@ -9,6 +9,7 @@ import org.signal.ringrtc.CallId;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.ringrtc.TurnServerInfoParcel;
@ -18,29 +19,35 @@ import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
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_CAMERA_STATE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ENABLE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_CALL_STATE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_IDENTITY_KEY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_EXTERNAL_TOKEN;
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_OPAQUE_MESSAGE;
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;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_UUID;
/**
* Helper to parse the various attributes out of intents passed to the service.
@ -111,6 +118,14 @@ public final class WebRtcIntentParser {
return intent.getByteArrayExtra(EXTRA_OFFER_OPAQUE);
}
public static @NonNull byte[] getOpaque(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getByteArrayExtra(EXTRA_OPAQUE_MESSAGE));
}
public static @NonNull UUID getUuid(@NonNull Intent intent) {
return UuidUtil.parseOrThrow(intent.getStringExtra(EXTRA_UUID));
}
public static boolean getBroadcastFlag(@NonNull Intent intent) {
return intent.getBooleanExtra(EXTRA_BROADCAST, false);
}
@ -149,10 +164,17 @@ public final class WebRtcIntentParser {
return intent.getBooleanExtra(EXTRA_ENABLE, false);
}
public static @NonNull byte[] getGroupMembershipToken(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getByteArrayExtra(EXTRA_GROUP_EXTERNAL_TOKEN));
}
public static @NonNull CameraState getCameraState(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getParcelableExtra(EXTRA_CAMERA_STATE));
}
public static @NonNull WebRtcViewModel.State getErrorCallState(@NonNull Intent intent) {
return (WebRtcViewModel.State) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_ERROR_CALL_STATE));
}
public static @NonNull Optional<IdentityKey> getErrorIdentityKey(@NonNull Intent intent) {
IdentityKeyParcelable identityKeyParcelable = (IdentityKeyParcelable) intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY);
if (identityKeyParcelable != null) {

Wyświetl plik

@ -6,6 +6,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
@ -16,6 +19,8 @@ import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.UUID;
/**
* 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
@ -23,19 +28,21 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
*/
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;
@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;
@NonNull private final GroupCall.Observer groupCallObserver;
public WebRtcInteractor(@NonNull WebRtcCallService webRtcCallService,
@NonNull CallManager callManager,
@NonNull LockManager lockManager,
@NonNull SignalAudioManager audioManager,
@NonNull BluetoothStateManager bluetoothStateManager,
@NonNull CameraEventListener cameraEventListener)
@NonNull CameraEventListener cameraEventListener,
@NonNull GroupCall.Observer groupCallObserver)
{
this.webRtcCallService = webRtcCallService;
this.callManager = callManager;
@ -43,6 +50,7 @@ public class WebRtcInteractor {
this.audioManager = audioManager;
this.bluetoothStateManager = bluetoothStateManager;
this.cameraEventListener = cameraEventListener;
this.groupCallObserver = groupCallObserver;
}
@NonNull CameraEventListener getCameraEventListener() {
@ -57,6 +65,10 @@ public class WebRtcInteractor {
return webRtcCallService;
}
@NonNull GroupCall.Observer getGroupCallObserver() {
return groupCallObserver;
}
void setWantsBluetoothConnection(boolean enabled) {
bluetoothStateManager.setWantsConnection(enabled);
}
@ -73,8 +85,16 @@ public class WebRtcInteractor {
webRtcCallService.sendCallMessage(remotePeer, callMessage);
}
void sendOpaqueCallMessage(@NonNull UUID uuid, @NonNull SignalServiceCallMessage callMessage) {
webRtcCallService.sendOpaqueCallMessage(uuid, callMessage);
}
void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
webRtcCallService.setCallInProgressNotification(type, remotePeer);
webRtcCallService.setCallInProgressNotification(type, remotePeer.getRecipient());
}
void setCallInProgressNotification(int type, @NonNull Recipient recipient) {
webRtcCallService.setCallInProgressNotification(type, recipient);
}
void retrieveTurnServers(@NonNull RemotePeer remotePeer) {

Wyświetl plik

@ -6,6 +6,8 @@ import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.InvalidKeyException;
@ -56,4 +58,17 @@ public final class WebRtcUtil {
androidAudioManager.setSpeakerphoneOn(true);
}
}
public static @NonNull WebRtcViewModel.GroupCallState groupCallStateForConnection(@NonNull GroupCall.ConnectionState connectionState) {
switch (connectionState) {
case CONNECTING:
return WebRtcViewModel.GroupCallState.CONNECTING;
case CONNECTED:
return WebRtcViewModel.GroupCallState.CONNECTED;
case RECONNECTING:
return WebRtcViewModel.GroupCallState.RECONNECTING;
default:
return WebRtcViewModel.GroupCallState.DISCONNECTED;
}
}
}

Wyświetl plik

@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
@ -12,6 +14,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -20,27 +23,31 @@ import java.util.Objects;
*/
public class CallInfoState {
WebRtcViewModel.State callState;
Recipient callRecipient;
long callConnectedTime;
Map<Recipient, CallParticipant> remoteParticipants;
Map<Integer, RemotePeer> peerMap;
RemotePeer activePeer;
WebRtcViewModel.State callState;
Recipient callRecipient;
long callConnectedTime;
Map<CallParticipantId, CallParticipant> remoteParticipants;
Map<Integer, RemotePeer> peerMap;
RemotePeer activePeer;
GroupCall groupCall;
WebRtcViewModel.GroupCallState groupState;
public CallInfoState() {
this(WebRtcViewModel.State.IDLE, Recipient.UNKNOWN, -1, Collections.emptyMap(), Collections.emptyMap(), null);
this(WebRtcViewModel.State.IDLE, Recipient.UNKNOWN, -1, Collections.emptyMap(), Collections.emptyMap(), null, null, WebRtcViewModel.GroupCallState.IDLE);
}
public CallInfoState(@NonNull CallInfoState toCopy) {
this(toCopy.callState, toCopy.callRecipient, toCopy.callConnectedTime, toCopy.remoteParticipants, toCopy.peerMap, toCopy.activePeer);
this(toCopy.callState, toCopy.callRecipient, toCopy.callConnectedTime, toCopy.remoteParticipants, toCopy.peerMap, toCopy.activePeer, toCopy.groupCall, toCopy.groupState);
}
public CallInfoState(@NonNull WebRtcViewModel.State callState,
@NonNull Recipient callRecipient,
long callConnectedTime,
@NonNull Map<Recipient, CallParticipant> remoteParticipants,
@NonNull Map<CallParticipantId, CallParticipant> remoteParticipants,
@NonNull Map<Integer, RemotePeer> peerMap,
@Nullable RemotePeer activePeer)
@Nullable RemotePeer activePeer,
@Nullable GroupCall groupCall,
@NonNull WebRtcViewModel.GroupCallState groupState)
{
this.callState = callState;
this.callRecipient = callRecipient;
@ -48,6 +55,8 @@ public class CallInfoState {
this.remoteParticipants = new LinkedHashMap<>(remoteParticipants);
this.peerMap = new HashMap<>(peerMap);
this.activePeer = activePeer;
this.groupCall = groupCall;
this.groupState = groupState;
}
public @NonNull Recipient getCallRecipient() {
@ -58,11 +67,19 @@ public class CallInfoState {
return callConnectedTime;
}
public @Nullable CallParticipant getRemoteParticipant(@NonNull Recipient recipient) {
return remoteParticipants.get(recipient);
public @NonNull Map<CallParticipantId, CallParticipant> getRemoteCallParticipantsMap() {
return new LinkedHashMap<>(remoteParticipants);
}
public @NonNull ArrayList<CallParticipant> getRemoteCallParticipants() {
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull Recipient recipient) {
return getRemoteCallParticipant(new CallParticipantId(recipient));
}
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull CallParticipantId callParticipantId) {
return remoteParticipants.get(callParticipantId);
}
public @NonNull List<CallParticipant> getRemoteCallParticipants() {
return new ArrayList<>(remoteParticipants.values());
}
@ -81,4 +98,16 @@ public class CallInfoState {
public @NonNull RemotePeer requireActivePeer() {
return Objects.requireNonNull(activePeer);
}
public @Nullable GroupCall getGroupCall() {
return groupCall;
}
public @NonNull GroupCall requireGroupCall() {
return Objects.requireNonNull(groupCall);
}
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
return groupState;
}
}

Wyświetl plik

@ -3,8 +3,10 @@ package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.Camera;
@ -192,8 +194,18 @@ public class WebRtcServiceStateBuilder {
return this;
}
public @NonNull CallInfoStateBuilder putParticipant(@NonNull CallParticipantId callParticipantId, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(callParticipantId, callParticipant);
return this;
}
public @NonNull CallInfoStateBuilder putParticipant(@NonNull Recipient recipient, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(recipient, callParticipant);
toBuild.remoteParticipants.put(new CallParticipantId(recipient), callParticipant);
return this;
}
public @NonNull CallInfoStateBuilder clearParticipantMap() {
toBuild.remoteParticipants.clear();
return this;
}
@ -216,5 +228,15 @@ public class WebRtcServiceStateBuilder {
toBuild.activePeer = activePeer;
return this;
}
public @NonNull CallInfoStateBuilder groupCall(@Nullable GroupCall groupCall) {
toBuild.groupCall = groupCall;
return this;
}
public @NonNull CallInfoStateBuilder groupCallState(@Nullable WebRtcViewModel.GroupCallState groupState) {
toBuild.groupState = groupState;
return this;
}
}
}

Wyświetl plik

@ -64,6 +64,7 @@ public final class FeatureFlags {
private static final String MAX_ENVELOPE_SIZE = "android.maxEnvelopeSize";
private static final String GV1_AUTO_MIGRATE_VERSION = "android.groupsv2.autoMigrateVersion";
private static final String GV1_MANUAL_MIGRATE_VERSION = "android.groupsv2.manualMigrateVersion";
private static final String GROUP_CALLING_VERSION = "android.groupsv2.callingVersion";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -84,7 +85,8 @@ public final class FeatureFlags {
VIEWED_RECEIPTS,
MAX_ENVELOPE_SIZE,
GV1_AUTO_MIGRATE_VERSION,
GV1_MANUAL_MIGRATE_VERSION
GV1_MANUAL_MIGRATE_VERSION,
GROUP_CALLING_VERSION
);
/**
@ -274,6 +276,10 @@ public final class FeatureFlags {
return groupsV1AutoMigration() && getVersionFlag(GV1_MANUAL_MIGRATE_VERSION) == VersionFlag.ON;
}
public static boolean groupCalling() {
return getVersionFlag(GROUP_CALLING_VERSION) == VersionFlag.ON;
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

Wyświetl plik

@ -36,8 +36,11 @@ import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.view.ViewCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.lifecycle.Lifecycle;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
@ -276,4 +279,20 @@ public final class ViewUtil {
}
}
}
public static @Nullable Lifecycle getActivityLifecycle(@NonNull View view) {
return getActivityLifecycle(view.getContext());
}
private static @Nullable Lifecycle getActivityLifecycle(@Nullable Context context) {
if (context instanceof ContextThemeWrapper) {
return getActivityLifecycle(((ContextThemeWrapper) context).getBaseContext());
}
if (context instanceof AppCompatActivity) {
return ((AppCompatActivity) context).getLifecycle();
}
return null;
}
}

Wyświetl plik

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/core_white" android:state_enabled="true" />
<item android:color="@color/transparent_white_40" />
</selector>

Wyświetl plik

@ -9,14 +9,12 @@
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/call_participant_item_avatar"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="112dp"
android:layout_height="112dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5"
tools:srcCompat="@tools:sample/avatars" />
<ImageView

Wyświetl plik

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black"
tools:layout_height="match_parent"
tools:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/group_call_participant_card_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
android:clipChildren="true"
app:cardCornerRadius="10dp">
<include
android:id="@+id/group_call_participant"
layout="@layout/call_participant_item"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
</FrameLayout>

Wyświetl plik

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Space xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="72dp"
android:layout_width="40dp"
android:layout_height="72dp"
tools:background="@color/red"
tools:visibility="visible" />

Wyświetl plik

@ -4,6 +4,9 @@
android:id="@+id/call_screen_call_participants"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexDirection="row"
app:flexWrap="wrap" />
app:flexWrap="wrap"
app:justifyContent="flex_start" />

Wyświetl plik

@ -4,12 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="org.thoughtcrime.securesms.components.webrtc.WebRtcCallView">
<View
android:id="@+id/call_screen_blur_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent_black_40" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen_participants_parent"
android:layout_width="match_parent"
@ -316,7 +310,7 @@
android:layout_weight="1"
android:text="@string/WebRtcCallView__start_call"
android:textAllCaps="false"
android:textColor="@color/core_white"
android:textColor="@color/core_green_text_button"
app:backgroundTint="@color/core_green" />
</LinearLayout>

Wyświetl plik

@ -3,11 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStart="0dp">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
@ -22,7 +21,9 @@
android:id="@+id/call_screen_recipient_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
android:textColor="@color/core_white"
app:layout_constraintBottom_toTopOf="@id/action_bar_guideline"
@ -36,7 +37,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/core_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

Wyświetl plik

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation_callable_secure__menu_video"
android:id="@+id/menu_video_secure"
android:icon="@drawable/ic_video_solid_24"
app:showAsAction="always" />
</menu>

Wyświetl plik

@ -1332,9 +1332,23 @@
<string name="WebRtcCallView__signal_voice_call">Signal voice call…</string>
<string name="WebRtcCallView__signal_video_call">Signal video call…</string>
<string name="WebRtcCallView__start_call">Start Call</string>
<string name="WebRtcCallView__group_call">Group Call</string>
<string name="WebRtcCallView__join_call">Join Call</string>
<string name="WebRtcCallView__s_group_call">\"%1$s\" Group Call</string>
<string name="WebRtcCallView__view_participants_list">View participants</string>
<string name="WebRtcCallView__your_video_is_off">Your video is off</string>
<string name="WebRtcCallView__connecting">Connecting…</string>
<string name="WebRtcCallView__reconnecting">Reconnecting…</string>
<string name="WebRtcCallView__joining">Joining…</string>
<string name="WebRtcCallView__disconnected">Disconnected</string>
<string name="WebRtcCallView__no_one_else_is_here">No one else is here</string>
<string name="WebRtcCallView__s_is_in_this_call">%1$s is in this call</string>
<string name="WebRtcCallView__s_and_s_are_in_this_call">%1$s and %2$s are in this call</string>
<plurals name="WebRtcCallView__s_s_and_d_others_are_in_this_call">
<item quantity="one">%1$s, %2$s, and %3$d other are in this call</item>
<item quantity="other">%1$s, %2$s, and %3$d others are in this call</item>
</plurals>
<!-- CallParticipantsListDialog -->
<plurals name="CallParticipantsListDialog_in_this_call_d_people">

Wyświetl plik

@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
@ -89,7 +90,6 @@ import org.whispersystems.util.Base64;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.sql.Time;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@ -239,6 +239,20 @@ public class SignalServiceMessageSender {
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null);
}
/**
* Send an http request on behalf of the calling infrastructure.
*
* @param requestId Request identifier
* @param url Fully qualified URL to request
* @param httpMethod Http method to use (e.g., "GET", "POST")
* @param headers Optional list of headers to send with request
* @param body Optional body to send with request
* @return
*/
public CallingResponse makeCallingRequest(long requestId, String url, String httpMethod, List<Pair<String, String>> headers, byte[] body) {
return socket.makeCallingRequest(requestId, url, httpMethod, headers, body);
}
/**
* Send a message to a single recipient.
*
@ -773,6 +787,8 @@ public class SignalServiceMessageSender {
}
} else if (callMessage.getBusyMessage().isPresent()) {
builder.setBusy(CallMessage.Busy.newBuilder().setId(callMessage.getBusyMessage().get().getId()));
} else if (callMessage.getOpaqueMessage().isPresent()) {
builder.setOpaque(CallMessage.Opaque.newBuilder().setData(ByteString.copyFrom(callMessage.getOpaqueMessage().get().getOpaque())));
}
builder.setMultiRing(callMessage.isMultiRing());

Wyświetl plik

@ -7,6 +7,7 @@ import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
@ -166,6 +167,12 @@ public final class GroupsV2Api {
return socket.patchGroupsV2Group(groupChange, authorization.toString(), groupLinkPassword);
}
public GroupExternalCredential getGroupExternalCredential(GroupsV2AuthorizationString authorization)
throws IOException
{
return socket.getGroupExternalCredential(authorization);
}
private static HashMap<Integer, AuthCredentialResponse> parseCredentialResponse(CredentialResponse credentialResponse)
throws IOException
{

Wyświetl plik

@ -23,6 +23,7 @@ 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.OpaqueMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
@ -616,6 +617,9 @@ public final class SignalServiceContent {
} else if (content.hasBusy()) {
SignalServiceProtos.CallMessage.Busy busy = content.getBusy();
return SignalServiceCallMessage.forBusy(new BusyMessage(busy.getId()), isMultiRing, destinationDeviceId);
} else if (content.hasOpaque()) {
SignalServiceProtos.CallMessage.Opaque opaque = content.getOpaque();
return SignalServiceCallMessage.forOpaque(new OpaqueMessage(opaque.getData().toByteArray()), isMultiRing, destinationDeviceId);
}
return SignalServiceCallMessage.empty();

Wyświetl plik

@ -0,0 +1,48 @@
package org.whispersystems.signalservice.api.messages.calls;
/**
* Encapsulate the response to an http request on behalf of ringrtc.
*/
public abstract class CallingResponse {
private final long requestId;
CallingResponse(long requestId) {
this.requestId = requestId;
}
public long getRequestId() {
return requestId;
}
public static class Success extends CallingResponse {
private final int responseStatus;
private final byte[] responseBody;
public Success(long requestId, int responseStatus, byte[] responseBody) {
super(requestId);
this.responseStatus = responseStatus;
this.responseBody = responseBody;
}
public int getResponseStatus() {
return responseStatus;
}
public byte[] getResponseBody() {
return responseBody;
}
}
public static class Error extends CallingResponse {
private final Throwable throwable;
public Error(long requestId, Throwable throwable) {
super(requestId);
this.throwable = throwable;
}
public Throwable getThrowable() {
return throwable;
}
}
}

Wyświetl plik

@ -0,0 +1,14 @@
package org.whispersystems.signalservice.api.messages.calls;
public class OpaqueMessage {
private final byte[] opaque;
public OpaqueMessage(byte[] opaque) {
this.opaque = opaque;
}
public byte[] getOpaque() {
return opaque;
}
}

Wyświetl plik

@ -12,6 +12,7 @@ public class SignalServiceCallMessage {
private final Optional<HangupMessage> hangupMessage;
private final Optional<BusyMessage> busyMessage;
private final Optional<List<IceUpdateMessage>> iceUpdateMessages;
private final Optional<OpaqueMessage> opaqueMessage;
private final Optional<Integer> destinationDeviceId;
private final boolean isMultiRing;
@ -20,6 +21,7 @@ public class SignalServiceCallMessage {
Optional<List<IceUpdateMessage>> iceUpdateMessages,
Optional<HangupMessage> hangupMessage,
Optional<BusyMessage> busyMessage,
Optional<OpaqueMessage> opaqueMessage,
boolean isMultiRing,
Optional<Integer> destinationDeviceId)
{
@ -28,6 +30,7 @@ public class SignalServiceCallMessage {
this.iceUpdateMessages = iceUpdateMessages;
this.hangupMessage = hangupMessage;
this.busyMessage = busyMessage;
this.opaqueMessage = opaqueMessage;
this.isMultiRing = isMultiRing;
this.destinationDeviceId = destinationDeviceId;
}
@ -38,6 +41,7 @@ public class SignalServiceCallMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
@ -48,6 +52,7 @@ public class SignalServiceCallMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
@ -58,6 +63,7 @@ public class SignalServiceCallMessage {
Optional.of(iceUpdateMessages),
Optional.absent(),
Optional.absent(),
Optional.absent(),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
@ -71,6 +77,7 @@ public class SignalServiceCallMessage {
Optional.of(iceUpdateMessages),
Optional.absent(),
Optional.absent(),
Optional.absent(),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
@ -81,6 +88,7 @@ public class SignalServiceCallMessage {
Optional.absent(),
Optional.of(hangupMessage),
Optional.absent(),
Optional.absent(),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
@ -91,6 +99,18 @@ public class SignalServiceCallMessage {
Optional.absent(),
Optional.absent(),
Optional.of(busyMessage),
Optional.absent(),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
public static SignalServiceCallMessage forOpaque(OpaqueMessage opaqueMessage, boolean isMultiRing, Integer destinationDeviceId) {
return new SignalServiceCallMessage(Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(opaqueMessage),
isMultiRing,
Optional.fromNullable(destinationDeviceId));
}
@ -102,7 +122,7 @@ public class SignalServiceCallMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
false,
Optional.absent(), false,
Optional.absent());
}
@ -126,6 +146,10 @@ public class SignalServiceCallMessage {
return busyMessage;
}
public Optional<OpaqueMessage> getOpaqueMessage() {
return opaqueMessage;
}
public boolean isMultiRing() {
return isMultiRing;
}

Wyświetl plik

@ -15,6 +15,7 @@ import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
@ -38,6 +39,7 @@ import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
@ -204,6 +206,7 @@ public class PushServiceSocket {
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s";
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
private static final String GROUPSV2_TOKEN = "/v1/groups/token";
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
@ -1665,6 +1668,42 @@ public class PushServiceSocket {
throw new NonSuccessfulResponseCodeException("Response: " + response);
}
public CallingResponse makeCallingRequest(long requestId, String url, String httpMethod, List<Pair<String, String>> headers, byte[] body) {
ConnectionHolder connectionHolder = getRandom(serviceClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.followRedirects(true)
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
RequestBody requestBody = body != null ? RequestBody.create(null, body) : null;
Request.Builder builder = new Request.Builder()
.url(url)
.method(httpMethod, requestBody);
if (headers != null) {
for (Pair<String, String> header : headers) {
builder.addHeader(header.first(), header.second());
}
}
Call call = okHttpClient.newCall(builder.build());
try {
Response response = call.execute();
int responseStatus = response.code();
byte[] responseBody = response.body() != null ? response.body().bytes() : new byte[0];
return new CallingResponse.Success(requestId, responseStatus, responseBody);
} catch (IOException e) {
Log.w(TAG, "Exception during ringrtc http call.", e);
return new CallingResponse.Error(requestId, e);
}
}
private ServiceConnectionHolder[] createServiceConnectionHolders(SignalUrl[] urls,
List<Interceptor> interceptors,
Optional<Dns> dns)
@ -2037,6 +2076,18 @@ public class PushServiceSocket {
return GroupJoinInfo.parseFrom(readBodyBytes(response));
}
public GroupExternalCredential getGroupExternalCredential(GroupsV2AuthorizationString authorization)
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
{
ResponseBody response = makeStorageRequest(authorization.toString(),
GROUPSV2_TOKEN,
"GET",
null,
NO_HANDLER);
return GroupExternalCredential.parseFrom(readBodyBytes(response));
}
public static final class GroupHistory {
private final GroupChanges groupChanges;
private final Optional<ContentRange> contentRange;

Wyświetl plik

@ -210,3 +210,7 @@ message GroupJoinInfo {
uint32 revision = 6;
bool pendingAdminApproval = 7;
}
message GroupExternalCredential {
string token = 1;
}

Wyświetl plik

@ -92,6 +92,9 @@ message CallMessage {
optional uint32 deviceId = 3;
}
message Opaque {
optional bytes data = 1;
}
optional Offer offer = 1;
optional Answer answer = 2;
@ -102,6 +105,7 @@ message CallMessage {
optional Hangup hangup = 7;
optional bool multiRing = 8;
optional uint32 destinationDeviceId = 9;
optional Opaque opaque = 10;
}
message DataMessage {