diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index b775b0d897..04e4f15941 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -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); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java index c029934ee7..dac44919e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/BroadcastVideoSink.java @@ -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 sinks; + private final WeakHashMap 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; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java index 06aed83cb8..213a9cac26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java index 51563751cb..2b1be7dd50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java @@ -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 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java index 1b4186143a..bf1abd5c98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java @@ -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 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 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 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 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 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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java index 199a81b82e..f1dbe9cfb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/TextureViewRenderer.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index 56472b8945..f6337a8383 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -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 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 controlsListenerConsumer) { - if (controlsListener != null) { - controlsListenerConsumer.accept(controlsListener); + private static void runIfNonNull(@Nullable T listener, @NonNull Consumer 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(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index 5acc4dbdd0..7fb99d6146 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -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)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index 7f3c252a64..bfa12f2d07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -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 + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java index 60fa4494d8..6518e99a68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcLocalRenderState.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc; public enum WebRtcLocalRenderState { GONE, SMALL_RECTANGLE, - SMALL_SQUARE, + SMALLER_RECTANGLE, LARGE, LARGE_NO_VIDEO } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java index 0d6a7b96a6..c3156d5255 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java @@ -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> 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)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 62b70d7e1a..de37cee3be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipantId.java b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipantId.java new file mode 100644 index 0000000000..b7011cb0f9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipantId.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java index 0dad339641..8f98de178c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java @@ -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 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 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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index fce73d69f5..f5e29aef03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -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 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index b1f624b64c..23a25267e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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 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 recipients = Recipient.resolvedList(groupRecord.getMembers()); + + Map 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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 349e42a99e..4657ef65d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -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 smsMessageId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index 48fe6efbfe..2ae968a783 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -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 list, @Nullable byte[] bytes) { - Log.i(TAG, "onSendHttpRequest:"); + public void onSendHttpRequest(long requestId, @NonNull String url, @NonNull CallManager.HttpMethod httpMethod, @Nullable List headers, @Nullable byte[] body) { + Log.i(TAG, "onSendHttpRequest(): request_id: " + requestId); + networkExecutor.execute(() -> { + List> headerPairs; + if (headers != null) { + headerPairs = Stream.of(headers) + .map(header -> new Pair<>(header.getName(), header.getValue())) + .toList(); + } else { + headerPairs = Collections.emptyList(); + } + + CallingResponse response = 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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java index d4176d334d..e6092eafa9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java new file mode 100644 index 0000000000..900718bb59 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -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 participants = currentState.getCallInfoState().getRemoteCallParticipantsMap(); + + WebRtcServiceStateBuilder.CallInfoStateBuilder builder = currentState.builder() + .changeCallInfoState() + .clearParticipantMap(); + + LongSparseArray 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 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 participants = currentState.getCallInfoState().getRemoteCallParticipantsMap(); + + ArrayList renderedResolutions = new ArrayList<>(participants.size()); + for (Map.Entry 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)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java new file mode 100644 index 0000000000..5a153e0264 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java new file mode 100644 index 0000000000..14dbb9bb00 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java new file mode 100644 index 0000000000..ac68b8da29 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java @@ -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 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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java index 875c514907..fe45353c49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java index 8d664cca27..4fbe9665ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java index 530427b949..a039fb3794 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index ac24541a2f..81b29c9efe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -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; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java index 16fdd9cab9..10e48fdcee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcData.java @@ -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; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java index f094fe1b6e..f70473a45e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcIntentParser.java @@ -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 getErrorIdentityKey(@NonNull Intent intent) { IdentityKeyParcelable identityKeyParcelable = (IdentityKeyParcelable) intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY); if (identityKeyParcelable != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java index d6864949ad..97e2fc6a97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java index 51072a0757..d4cf753c65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java @@ -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; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java index 1dce4ee10c..08be439ef4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java @@ -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 remoteParticipants; - Map peerMap; - RemotePeer activePeer; + WebRtcViewModel.State callState; + Recipient callRecipient; + long callConnectedTime; + Map remoteParticipants; + Map 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 remoteParticipants, + @NonNull Map remoteParticipants, @NonNull Map 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 getRemoteCallParticipantsMap() { + return new LinkedHashMap<>(remoteParticipants); } - public @NonNull ArrayList 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 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; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index 175b6bb454..89c5b20bdd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -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; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 259ba3c62c..c54c94addb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -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 getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java index 97466dbb24..09faeb9fdb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java @@ -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; + } } diff --git a/app/src/main/res/color/core_green_text_button.xml b/app/src/main/res/color/core_green_text_button.xml new file mode 100644 index 0000000000..45111a235e --- /dev/null +++ b/app/src/main/res/color/core_green_text_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_participant_item.xml b/app/src/main/res/layout/call_participant_item.xml index 74268ab7e4..38bd5682c6 100644 --- a/app/src/main/res/layout/call_participant_item.xml +++ b/app/src/main/res/layout/call_participant_item.xml @@ -9,14 +9,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml b/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml index 2985687855..ba2db29b72 100644 --- a/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml +++ b/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participants_layout.xml b/app/src/main/res/layout/webrtc_call_participants_layout.xml index cf2184691c..e6e14cf3b0 100644 --- a/app/src/main/res/layout/webrtc_call_participants_layout.xml +++ b/app/src/main/res/layout/webrtc_call_participants_layout.xml @@ -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" /> \ No newline at end of file + app:flexWrap="wrap" + app:justifyContent="flex_start" /> \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_view.xml b/app/src/main/res/layout/webrtc_call_view.xml index e11044fd95..311cd7188c 100644 --- a/app/src/main/res/layout/webrtc_call_view.xml +++ b/app/src/main/res/layout/webrtc_call_view.xml @@ -4,12 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" tools:parentTag="org.thoughtcrime.securesms.components.webrtc.WebRtcCallView"> - - diff --git a/app/src/main/res/layout/webrtc_call_view_toolbar.xml b/app/src/main/res/layout/webrtc_call_view_toolbar.xml index 17ea9de92d..cb1d1d543d 100644 --- a/app/src/main/res/layout/webrtc_call_view_toolbar.xml +++ b/app/src/main/res/layout/webrtc_call_view_toolbar.xml @@ -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"> @@ -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" diff --git a/app/src/main/res/menu/conversation_callable_groupv2.xml b/app/src/main/res/menu/conversation_callable_groupv2.xml new file mode 100644 index 0000000000..4c8675ffca --- /dev/null +++ b/app/src/main/res/menu/conversation_callable_groupv2.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c23b1feed2..3d09830edc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1332,9 +1332,23 @@ Signal voice call… Signal video call… Start Call - Group Call + Join Call + \"%1$s\" Group Call View participants Your video is off + Connecting… + Reconnecting… + Joining… + Disconnected + + No one else is here + %1$s is in this call + %1$s and %2$s are in this call + + + %1$s, %2$s, and %3$d other are in this call + %1$s, %2$s, and %3$d others are in this call + diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 06d23aeb12..be8e3a35c5 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -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> 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()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java index 2fa821bdd2..5d75a50e47 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java @@ -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 parseCredentialResponse(CredentialResponse credentialResponse) throws IOException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 507e0e8c90..df2993021d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -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(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/CallingResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/CallingResponse.java new file mode 100644 index 0000000000..55a5c872e2 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/CallingResponse.java @@ -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; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/OpaqueMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/OpaqueMessage.java new file mode 100644 index 0000000000..dd108f10a1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/OpaqueMessage.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/SignalServiceCallMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/SignalServiceCallMessage.java index aa4beb6eb7..a15e3f984a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/SignalServiceCallMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/calls/SignalServiceCallMessage.java @@ -12,6 +12,7 @@ public class SignalServiceCallMessage { private final Optional hangupMessage; private final Optional busyMessage; private final Optional> iceUpdateMessages; + private final Optional opaqueMessage; private final Optional destinationDeviceId; private final boolean isMultiRing; @@ -20,6 +21,7 @@ public class SignalServiceCallMessage { Optional> iceUpdateMessages, Optional hangupMessage, Optional busyMessage, + Optional opaqueMessage, boolean isMultiRing, Optional 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 getOpaqueMessage() { + return opaqueMessage; + } + public boolean isMultiRing() { return isMultiRing; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 900e029377..ec72be29e3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -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> 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 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 interceptors, Optional 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; diff --git a/libsignal/service/src/main/proto/Groups.proto b/libsignal/service/src/main/proto/Groups.proto index 8ba519f939..19e1a905c0 100644 --- a/libsignal/service/src/main/proto/Groups.proto +++ b/libsignal/service/src/main/proto/Groups.proto @@ -210,3 +210,7 @@ message GroupJoinInfo { uint32 revision = 6; bool pendingAdminApproval = 7; } + +message GroupExternalCredential { + string token = 1; +} diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index a52a727008..1455de85e1 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -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 {