diff --git a/app/build.gradle b/app/build.gradle index b8bdfbaa10..3402afc1e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -384,7 +384,7 @@ dependencies { implementation 'org.signal:argon2:13.1@aar' - implementation 'org.signal:ringrtc-android:2.9.6' + implementation 'org.signal:ringrtc-android:2.10.1.1' implementation "me.leolin:ShortcutBadger:1.1.22" implementation 'se.emilsjolander:stickylistheaders:2.7.0' diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 41d7a6d29f..0d0e7f7b00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState; +import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow; import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil; import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView; @@ -62,11 +63,15 @@ import org.thoughtcrime.securesms.util.EllapsedTimeFormatter; import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import java.util.List; +import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; + public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback { private static final String TAG = Log.tag(WebRtcCallActivity.class); @@ -253,7 +258,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls); viewModel.getEvents().observe(this, this::handleViewModelEvent); viewModel.getCallTime().observe(this, this::handleCallTime); - viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants); + LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), viewModel.getOrientation(), (s, o) -> new Pair<>(s, o == PORTRAIT_BOTTOM_EDGE)) + .observe(this, p -> callScreen.updateCallParticipants(p.first(), p.second())); viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate); viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall()); @@ -291,6 +297,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) { SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords()); return; + } else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) { + callScreen.switchToSpeakerView(); + return; + } else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) { + CallToastPopupWindow.show(callScreen); + return; } if (isInPipMode()) { 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 d75bb7ffb1..e68351d62a 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 @@ -11,18 +11,43 @@ import org.webrtc.VideoSink; import java.util.WeakHashMap; +/** + * Video sink implementation that handles broadcasting a single source video track to + * multiple {@link VideoSink} consumers. + * + * Also has logic to manage rotating frames before forwarding to prevent each renderer + * from having to copy the frame for rotation. + */ public class BroadcastVideoSink implements VideoSink { private final EglBase eglBase; private final WeakHashMap sinks; private final WeakHashMap requestingSizes; private boolean dirtySizes; + private int deviceOrientationDegrees; + private boolean rotateToRightSide; + private boolean forceRotate; + private boolean rotateWithDevice; - public BroadcastVideoSink(@Nullable EglBase eglBase) { - this.eglBase = eglBase; - this.sinks = new WeakHashMap<>(); - this.requestingSizes = new WeakHashMap<>(); - this.dirtySizes = true; + public BroadcastVideoSink() { + this(null, false, true, 0); + } + + /** + * @param eglBase Rendering context + * @param forceRotate Always rotate video frames regardless of frame dimension + * @param rotateWithDevice Rotate video frame to match device orientation + * @param deviceOrientationDegrees Device orientation in degrees + */ + public BroadcastVideoSink(@Nullable EglBase eglBase, boolean forceRotate, boolean rotateWithDevice, int deviceOrientationDegrees) { + this.eglBase = eglBase; + this.sinks = new WeakHashMap<>(); + this.requestingSizes = new WeakHashMap<>(); + this.dirtySizes = true; + this.deviceOrientationDegrees = deviceOrientationDegrees; + this.rotateToRightSide = false; + this.forceRotate = forceRotate; + this.rotateWithDevice = rotateWithDevice; } public @Nullable EglBase getEglBase() { @@ -37,13 +62,58 @@ public class BroadcastVideoSink implements VideoSink { sinks.remove(sink); } + public void setForceRotate(boolean forceRotate) { + this.forceRotate = forceRotate; + } + + public void setRotateWithDevice(boolean rotateWithDevice) { + this.rotateWithDevice = rotateWithDevice; + } + + /** + * Set the specific rotation desired when not rotating with device. + * + * Really only needed for properly rotating self camera views. + */ + public void setRotateToRightSide(boolean rotateToRightSide) { + this.rotateToRightSide = rotateToRightSide; + } + + public void setDeviceOrientationDegrees(int deviceOrientationDegrees) { + this.deviceOrientationDegrees = deviceOrientationDegrees; + } + @Override public synchronized void onFrame(@NonNull VideoFrame videoFrame) { + if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth() || forceRotate) { + int rotation = calculateRotation(); + if (rotation > 0) { + rotation += rotateWithDevice ? videoFrame.getRotation() : 0; + videoFrame = new VideoFrame(videoFrame.getBuffer(), rotation % 360, videoFrame.getTimestampNs()); + } + } + for (VideoSink sink : sinks.keySet()) { sink.onFrame(videoFrame); } } + private int calculateRotation() { + if (forceRotate && (deviceOrientationDegrees == 0 || deviceOrientationDegrees == 180)) { + return 0; + } + + if (rotateWithDevice) { + if (forceRotate) { + return deviceOrientationDegrees; + } else { + return deviceOrientationDegrees != 0 && deviceOrientationDegrees != 180 ? deviceOrientationDegrees : 270; + } + } + + return rotateToRightSide ? 90 : 270; + } + void putRequestingSize(@NonNull Object object, @NonNull Point size) { synchronized (requestingSizes) { requestingSizes.put(object, size); 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 993ff207df..f68cddec39 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 @@ -54,6 +54,7 @@ public class CallParticipantView extends ConstraintLayout { private AppCompatImageView backgroundAvatar; private AvatarImageView avatar; + private View rendererFrame; private TextureViewRenderer renderer; private ImageView pipAvatar; private ContactPhoto contactPhoto; @@ -83,6 +84,7 @@ public class CallParticipantView extends ConstraintLayout { backgroundAvatar = findViewById(R.id.call_participant_background_avatar); avatar = findViewById(R.id.call_participant_item_avatar); pipAvatar = findViewById(R.id.call_participant_item_pip_avatar); + rendererFrame = findViewById(R.id.call_participant_renderer_frame); renderer = findViewById(R.id.call_participant_renderer); audioMuted = findViewById(R.id.call_participant_mic_muted); infoOverlay = findViewById(R.id.call_participant_info_overlay); @@ -108,6 +110,7 @@ public class CallParticipantView extends ConstraintLayout { infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant); if (infoMode) { + rendererFrame.setVisibility(View.GONE); renderer.setVisibility(View.GONE); renderer.attachBroadcastVideoSink(null); audioMuted.setVisibility(View.GONE); @@ -130,7 +133,10 @@ public class CallParticipantView extends ConstraintLayout { } else { infoOverlay.setVisibility(View.GONE); - renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE); + boolean hasContentToRender = participant.isVideoEnabled() || participant.isScreenSharing(); + + rendererFrame.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE); + renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE); if (participant.isVideoEnabled()) { if (participant.getVideoSink().getEglBase() != null) { 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 997f4fd7f6..459e5dcf17 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 @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; +import org.webrtc.RendererCommon; import java.util.Collections; import java.util.List; @@ -29,9 +30,10 @@ 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 List callParticipants = Collections.emptyList(); private CallParticipant focusedParticipant = null; private boolean shouldRenderInPip; + private boolean isPortrait; public CallParticipantsLayout(@NonNull Context context) { super(context); @@ -45,10 +47,11 @@ public class CallParticipantsLayout extends FlexboxLayout { super(context, attrs, defStyleAttr); } - void update(@NonNull List callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip) { + void update(@NonNull List callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip, boolean isPortrait) { this.callParticipants = callParticipants; this.focusedParticipant = focusedParticipant; this.shouldRenderInPip = shouldRenderInPip; + this.isPortrait = isPortrait; updateLayout(); } @@ -104,6 +107,11 @@ public class CallParticipantsLayout extends FlexboxLayout { callParticipantView.setCallParticipant(participant); callParticipantView.setRenderInPip(shouldRenderInPip); + if (participant.isScreenSharing()) { + callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); + } else { + callParticipantView.setScalingType(isPortrait || count < 3 ? RendererCommon.ScalingType.SCALE_ASPECT_FILL : RendererCommon.ScalingType.SCALE_ASPECT_BALANCED); + } if (count > 1) { view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING); 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 0842fe59af..c9741864f0 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 @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.webrtc; import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.annimon.stream.ComparatorCompat; import com.annimon.stream.OptionalLong; @@ -28,16 +27,16 @@ public final class CallParticipantsState { private static final int SMALL_GROUP_MAX = 6; - public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED, - WebRtcViewModel.GroupCallState.IDLE, - new ParticipantCollection(SMALL_GROUP_MAX), - CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false), - null, - WebRtcLocalRenderState.GONE, - false, - false, - false, - OptionalLong.empty()); + public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED, + WebRtcViewModel.GroupCallState.IDLE, + new ParticipantCollection(SMALL_GROUP_MAX), + CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(), false), + CallParticipant.EMPTY, + WebRtcLocalRenderState.GONE, + false, + false, + false, + OptionalLong.empty()); private final WebRtcViewModel.State callState; private final WebRtcViewModel.GroupCallState groupCallState; @@ -54,7 +53,7 @@ public final class CallParticipantsState { @NonNull WebRtcViewModel.GroupCallState groupCallState, @NonNull ParticipantCollection remoteParticipants, @NonNull CallParticipant localParticipant, - @Nullable CallParticipant focusedParticipant, + @NonNull CallParticipant focusedParticipant, @NonNull WebRtcLocalRenderState localRenderState, boolean isInPipMode, boolean showVideoForOutgoing, @@ -105,23 +104,38 @@ public final class CallParticipantsState { switch (remoteParticipants.size()) { case 0: return context.getString(R.string.WebRtcCallView__no_one_else_is_here); - case 1: + 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).getShortRecipientDisplayName(context)); } else { - return remoteParticipants.get(0).getRecipientDisplayName(context); + if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) { + return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context)); + } else { + return remoteParticipants.get(0).getRecipientDisplayName(context); + } } - case 2: - return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call, - remoteParticipants.get(0).getShortRecipientDisplayName(context), - remoteParticipants.get(1).getShortRecipientDisplayName(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).getShortRecipientDisplayName(context), - remoteParticipants.get(1).getShortRecipientDisplayName(context), - others); + } + case 2: { + if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) { + return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context)); + } else { + return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call, + remoteParticipants.get(0).getShortRecipientDisplayName(context), + remoteParticipants.get(1).getShortRecipientDisplayName(context)); + } + } + default: { + if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) { + return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context)); + } else { + 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).getShortRecipientDisplayName(context), + remoteParticipants.get(1).getShortRecipientDisplayName(context), + others); + } + } } } @@ -133,7 +147,7 @@ public final class CallParticipantsState { return localParticipant; } - public @Nullable CallParticipant getFocusedParticipant() { + public @NonNull CallParticipant getFocusedParticipant() { return focusedParticipant; } @@ -149,8 +163,16 @@ public final class CallParticipantsState { return isInPipMode; } + public boolean isViewingFocusedParticipant() { + return isViewingFocusedParticipant; + } + public boolean needsNewRequestSizes() { - return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize()); + if (groupCallState.isNotIdle()) { + return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize()); + } else { + return false; + } } public @NonNull OptionalLong getRemoteDevicesCount() { @@ -184,16 +206,11 @@ public final class CallParticipantsState { oldState.isViewingFocusedParticipant, oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED); - List participantsByLastSpoke = new ArrayList<>(webRtcViewModel.getRemoteParticipants()); - Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke()))); - - CallParticipant focused = participantsByLastSpoke.isEmpty() ? null : participantsByLastSpoke.get(0); - return new CallParticipantsState(webRtcViewModel.getState(), webRtcViewModel.getGroupState(), oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()), webRtcViewModel.getLocalParticipant(), - focused, + getFocusedParticipant(webRtcViewModel.getRemoteParticipants()), localRenderState, oldState.isInPipMode, newShowVideoForOutgoing, @@ -211,13 +228,11 @@ public final class CallParticipantsState { oldState.isViewingFocusedParticipant, oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED); - CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0); - return new CallParticipantsState(oldState.callState, oldState.groupCallState, oldState.remoteParticipants, oldState.localParticipant, - focused, + oldState.focusedParticipant, localRenderState, isInPip, oldState.showVideoForOutgoing, @@ -248,8 +263,6 @@ public final class CallParticipantsState { } public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) { - CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0); - WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant, oldState.isInPipMode, oldState.showVideoForOutgoing, @@ -263,7 +276,7 @@ public final class CallParticipantsState { oldState.groupCallState, oldState.remoteParticipants, oldState.localParticipant, - focused, + oldState.focusedParticipant, localRenderState, oldState.isInPipMode, oldState.showVideoForOutgoing, @@ -304,6 +317,16 @@ public final class CallParticipantsState { return localRenderState; } + private static @NonNull CallParticipant getFocusedParticipant(@NonNull List participants) { + List participantsByLastSpoke = new ArrayList<>(participants); + Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke()))); + + return participantsByLastSpoke.isEmpty() ? CallParticipant.EMPTY + : participantsByLastSpoke.stream() + .filter(CallParticipant::isScreenSharing) + .findAny().orElse(participantsByLastSpoke.get(0)); + } + public enum SelectedPage { GRID, FOCUSED diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java new file mode 100644 index 0000000000..d982670f06 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.components.webrtc; + +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupWindow; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.ViewUtil; + +import java.util.concurrent.TimeUnit; + +/** + * Top screen toast to be shown to the user for 3 seconds. + * + * Currently hard coded to show specific text, but could be easily expanded to be customizable + * if desired. Based on {@link CallParticipantsListUpdatePopupWindow}. + */ +public class CallToastPopupWindow extends PopupWindow { + + private static final long DURATION = TimeUnit.SECONDS.toMillis(3); + + private final ViewGroup parent; + + public static void show(@NonNull ViewGroup viewGroup) { + CallToastPopupWindow toast = new CallToastPopupWindow(viewGroup); + toast.show(); + } + + private CallToastPopupWindow(@NonNull ViewGroup parent) { + super(LayoutInflater.from(parent.getContext()).inflate(R.layout.call_toast_popup_window, parent, false), + ViewGroup.LayoutParams.MATCH_PARENT, + ViewUtil.dpToPx(94)); + + this.parent = parent; + + setAnimationStyle(R.style.PopupAnimation); + } + + public void show() { + showAtLocation(parent, Gravity.TOP | Gravity.START, 0, 0); + measureChild(); + update(); + getContentView().postDelayed(this::dismiss, DURATION); + } + + private void measureChild() { + getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/OrientationAwareVideoSink.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/OrientationAwareVideoSink.java deleted file mode 100644 index 2ba8086434..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/OrientationAwareVideoSink.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import androidx.annotation.NonNull; - -import org.webrtc.VideoFrame; -import org.webrtc.VideoSink; - -public final class OrientationAwareVideoSink implements VideoSink { - - private final VideoSink delegate; - - public OrientationAwareVideoSink(@NonNull VideoSink delegate) { - this.delegate = delegate; - } - - @Override - public void onFrame(VideoFrame videoFrame) { - if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth()) { - delegate.onFrame(new VideoFrame(videoFrame.getBuffer(), 270, videoFrame.getTimestampNs())); - } else { - delegate.onFrame(videoFrame); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java index 89889289b4..6b040e0e18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java @@ -14,29 +14,34 @@ class WebRtcCallParticipantsPage { private final CallParticipant focusedParticipant; private final boolean isSpeaker; private final boolean isRenderInPip; - + private final boolean isPortrait; + static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List callParticipants, @NonNull CallParticipant focusedParticipant, - boolean isRenderInPip) + boolean isRenderInPip, + boolean isPortrait) { - return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip); + return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait); } - + static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant, - boolean isRenderInPip) + boolean isRenderInPip, + boolean isPortrait) { - return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip); + return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip, isPortrait); } private WebRtcCallParticipantsPage(@NonNull List callParticipants, - @NonNull CallParticipant focusedParticipant, + @NonNull CallParticipant focusedParticipant, boolean isSpeaker, - boolean isRenderInPip) + boolean isRenderInPip, + boolean isPortrait) { this.callParticipants = callParticipants; this.focusedParticipant = focusedParticipant; this.isSpeaker = isSpeaker; this.isRenderInPip = isRenderInPip; + this.isPortrait = isPortrait; } public @NonNull List getCallParticipants() { @@ -55,19 +60,24 @@ class WebRtcCallParticipantsPage { return isSpeaker; } + public boolean isPortrait() { + return isPortrait; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o; return isSpeaker == that.isSpeaker && - isRenderInPip == that.isRenderInPip && - focusedParticipant.equals(that.focusedParticipant) && - callParticipants.equals(that.callParticipants); + isRenderInPip == that.isRenderInPip && + focusedParticipant.equals(that.focusedParticipant) && + callParticipants.equals(that.callParticipants) && + isPortrait == that.isPortrait; } @Override public int hashCode() { - return Objects.hash(callParticipants, isSpeaker, focusedParticipant, isRenderInPip); + return Objects.hash(callParticipants, isSpeaker, focusedParticipant, isRenderInPip, isPortrait); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java index 06c3afd622..cb8e1843f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java @@ -10,6 +10,8 @@ import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.events.CallParticipant; +import org.webrtc.RendererCommon; class WebRtcCallParticipantsPagerAdapter extends ListAdapter { @@ -84,7 +86,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter { @@ -61,6 +62,7 @@ class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter pages = new ArrayList<>(2); if (!state.getGridParticipants().isEmpty()) { - pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode())); + pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait)); } - if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) { - pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode())); + if (state.getFocusedParticipant() != CallParticipant.EMPTY && state.getAllRemoteParticipants().size() > 1) { + pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait)); } if ((state.getGroupCallState().isNotIdle() && state.getRemoteDevicesCount().orElse(0) > 0) || state.getGroupCallState().isConnected()) { @@ -839,6 +831,12 @@ public class WebRtcCallView extends FrameLayout { return true; } + public void switchToSpeakerView() { + if (pagerAdapter.getItemCount() > 0) { + callParticipantsPager.setCurrentItem(pagerAdapter.getItemCount() - 1, false); + } + } + public interface ControlsListener { void onStartCall(boolean isVideoCall); void onCancelStartCall(); 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 2dac451ba0..d3f63babe3 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 @@ -61,19 +61,15 @@ public class WebRtcCallViewModel extends ViewModel { private boolean answerWithVideoAvailable = false; private Runnable elapsedTimeRunnable = this::handleTick; private boolean canEnterPipMode = false; - private List previousParticipantsList = Collections.emptyList(); - private boolean callStarting = false; + private List previousParticipantsList = Collections.emptyList(); + private boolean callStarting = false; + private boolean switchOnFirstScreenShare = true; + private boolean showScreenShareTip = true; private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication()); private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) { - orientation = LiveDataUtil.combineLatest(deviceOrientationMonitor.getOrientation(), webRtcControls, (deviceOrientation, controls) -> { - if (controls.canRotateControls()) { - return deviceOrientation; - } else { - return Orientation.PORTRAIT_BOTTOM_EDGE; - } - }); + orientation = deviceOrientationMonitor.getOrientation(); } public LiveData getOrientation() { @@ -150,7 +146,16 @@ public class WebRtcCallViewModel extends ViewModel { SignalStore.tooltips().markGroupCallSpeakerViewSeen(); } - //noinspection ConstantConditions + CallParticipantsState state = participantsState.getValue(); + if (state != null && + showScreenShareTip && + state.getFocusedParticipant().isScreenSharing() && + state.isViewingFocusedParticipant() && + page == CallParticipantsState.SelectedPage.GRID) { + showScreenShareTip = false; + events.setValue(new Event.ShowSwipeToSpeakerHint()); + } + participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page)); } @@ -179,8 +184,16 @@ public class WebRtcCallViewModel extends ViewModel { microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled()); - //noinspection ConstantConditions - participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo)); + CallParticipantsState state = participantsState.getValue(); + if (state != null) { + boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing(); + CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo); + participantsState.setValue(newState); + if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) { + switchOnFirstScreenShare = false; + events.setValue(new Event.SwitchToSpeaker()); + } + } if (webRtcViewModel.getGroupState().isConnected()) { if (!containsPlaceholders(previousParticipantsList)) { @@ -394,6 +407,12 @@ public class WebRtcCallViewModel extends ViewModel { return identityRecords; } } + + public static class SwitchToSpeaker extends Event { + } + + public static class ShowSwipeToSpeakerHint extends Event { + } } public static class SafetyNumberChangeEvent { 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 7ef3732dd9..79478ac8fd 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 @@ -51,10 +51,6 @@ public final class WebRtcControls { this.participantLimit = participantLimit; } - boolean canRotateControls() { - return !isGroupCall(); - } - boolean displayErrorControls() { return isError(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java index 5d37f4422a..dc6a18836b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantViewHolder.java @@ -10,14 +10,16 @@ import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder; public class CallParticipantViewHolder extends RecipientViewHolder { - private final ImageView videoMuted; - private final ImageView audioMuted; + private final View videoMuted; + private final View audioMuted; + private final View screenSharing; public CallParticipantViewHolder(@NonNull View itemView) { super(itemView, null); - videoMuted = itemView.findViewById(R.id.call_participant_video_muted); - audioMuted = itemView.findViewById(R.id.call_participant_audio_muted); + videoMuted = findViewById(R.id.call_participant_video_muted); + audioMuted = findViewById(R.id.call_participant_audio_muted); + screenSharing = findViewById(R.id.call_participant_screen_sharing); } @Override @@ -26,5 +28,6 @@ public class CallParticipantViewHolder extends RecipientViewHolder 0, - microphoneEnabled, - 0, - true, - 0, - DeviceOrdinal.PRIMARY); - } - - public static @NonNull CallParticipant createRemote(@NonNull CallParticipantId callParticipantId, - @NonNull Recipient recipient, - @Nullable IdentityKey identityKey, - @NonNull BroadcastVideoSink renderer, - boolean audioEnabled, - boolean videoEnabled, - long lastSpoke, - boolean mediaKeysReceived, - long addedToCallTime, - @NonNull DeviceOrdinal deviceOrdinal) - { - return new CallParticipant(callParticipantId, recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal); - } - - private CallParticipant(@NonNull CallParticipantId callParticipantId, - @NonNull Recipient recipient, - @Nullable IdentityKey identityKey, - @NonNull BroadcastVideoSink videoSink, - @NonNull CameraState cameraState, - boolean videoEnabled, - boolean microphoneEnabled, - long lastSpoke, - boolean mediaKeysReceived, - long addedToCallTime, - @NonNull DeviceOrdinal deviceOrdinal) - { - this.callParticipantId = callParticipantId; - this.recipient = recipient; - this.identityKey = identityKey; - this.videoSink = videoSink; - this.cameraState = cameraState; - this.videoEnabled = videoEnabled; - this.microphoneEnabled = microphoneEnabled; - this.lastSpoke = lastSpoke; - this.mediaKeysReceived = mediaKeysReceived; - this.addedToCallTime = addedToCallTime; - this.deviceOrdinal = deviceOrdinal; - } - - public @NonNull CallParticipant withIdentityKey(@Nullable IdentityKey identityKey) { - return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal); - } - - public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) { - return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal); - } - - public @NonNull CallParticipantId getCallParticipantId() { - return callParticipantId; - } - - public @NonNull Recipient getRecipient() { - return recipient; - } - - public @NonNull String getRecipientDisplayName(@NonNull Context context) { - if (recipient.isSelf() && isPrimary()) { - return context.getString(R.string.CallParticipant__you); - } else if (recipient.isSelf()) { - return context.getString(R.string.CallParticipant__you_on_another_device); - } else if (isPrimary()) { - return recipient.getDisplayName(context); - } else { - return context.getString(R.string.CallParticipant__s_on_another_device, recipient.getDisplayName(context)); - } - } - - public @NonNull String getShortRecipientDisplayName(@NonNull Context context) { - if (recipient.isSelf() && isPrimary()) { - return context.getString(R.string.CallParticipant__you); - } else if (recipient.isSelf()) { - return context.getString(R.string.CallParticipant__you_on_another_device); - } else if (isPrimary()) { - return recipient.getShortDisplayName(context); - } else { - return context.getString(R.string.CallParticipant__s_on_another_device, recipient.getShortDisplayName(context)); - } - } - - public @Nullable IdentityKey getIdentityKey() { - return identityKey; - } - - public @NonNull BroadcastVideoSink getVideoSink() { - return videoSink; - } - - public @NonNull CameraState getCameraState() { - return cameraState; - } - - public boolean isVideoEnabled() { - return videoEnabled; - } - - public boolean isMicrophoneEnabled() { - return microphoneEnabled; - } - - public @NonNull CameraState.Direction getCameraDirection() { - if (cameraState.getActiveDirection() == CameraState.Direction.BACK) { - return cameraState.getActiveDirection(); - } - return CameraState.Direction.FRONT; - } - - public boolean isMoreThanOneCameraAvailable() { - return cameraState.getCameraCount() > 1; - } - - public long getLastSpoke() { - return lastSpoke; - } - - public boolean isMediaKeysReceived() { - return mediaKeysReceived; - } - - public long getAddedToCallTime() { - return addedToCallTime; - } - - public boolean isPrimary() { - return deviceOrdinal == DeviceOrdinal.PRIMARY; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CallParticipant that = (CallParticipant) o; - return callParticipantId.equals(that.callParticipantId) && - videoEnabled == that.videoEnabled && - microphoneEnabled == that.microphoneEnabled && - lastSpoke == that.lastSpoke && - mediaKeysReceived == that.mediaKeysReceived && - addedToCallTime == that.addedToCallTime && - cameraState.equals(that.cameraState) && - recipient.equals(that.recipient) && - Objects.equals(identityKey, that.identityKey) && - Objects.equals(videoSink, that.videoSink); - } - - @Override - public int hashCode() { - return Objects.hash(callParticipantId, cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime); - } - - @Override - public @NonNull String toString() { - return "CallParticipant{" + - "cameraState=" + cameraState + - ", recipient=" + recipient.getId() + - ", identityKey=" + (identityKey == null ? "absent" : "present") + - ", videoSink=" + (videoSink.getEglBase() == null ? "not initialized" : "initialized") + - ", videoEnabled=" + videoEnabled + - ", microphoneEnabled=" + microphoneEnabled + - ", lastSpoke=" + lastSpoke + - ", mediaKeysReceived=" + mediaKeysReceived + - ", addedToCallTime=" + addedToCallTime + - '}'; - } - - public enum DeviceOrdinal { - PRIMARY, - SECONDARY - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt new file mode 100644 index 0000000000..71c0624035 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt @@ -0,0 +1,122 @@ +package org.thoughtcrime.securesms.events + +import android.content.Context +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.ringrtc.CameraState +import org.whispersystems.libsignal.IdentityKey + +data class CallParticipant constructor( + val callParticipantId: CallParticipantId = CallParticipantId(Recipient.UNKNOWN), + val recipient: Recipient = Recipient.UNKNOWN, + val identityKey: IdentityKey? = null, + val videoSink: BroadcastVideoSink = BroadcastVideoSink(), + val cameraState: CameraState = CameraState.UNKNOWN, + val isVideoEnabled: Boolean = false, + val isMicrophoneEnabled: Boolean = false, + val lastSpoke: Long = 0, + val isMediaKeysReceived: Boolean = true, + val addedToCallTime: Long = 0, + val isScreenSharing: Boolean = false, + private val deviceOrdinal: DeviceOrdinal = DeviceOrdinal.PRIMARY +) { + val cameraDirection: CameraState.Direction + get() = if (cameraState.activeDirection == CameraState.Direction.BACK) cameraState.activeDirection else CameraState.Direction.FRONT + + val isMoreThanOneCameraAvailable: Boolean + get() = cameraState.cameraCount > 1 + + val isPrimary: Boolean + get() = deviceOrdinal == DeviceOrdinal.PRIMARY + + fun getRecipientDisplayName(context: Context): String { + return if (recipient.isSelf && isPrimary) { + context.getString(R.string.CallParticipant__you) + } else if (recipient.isSelf) { + context.getString(R.string.CallParticipant__you_on_another_device) + } else if (isPrimary) { + recipient.getDisplayName(context) + } else { + context.getString(R.string.CallParticipant__s_on_another_device, recipient.getDisplayName(context)) + } + } + + fun getShortRecipientDisplayName(context: Context): String { + return if (recipient.isSelf && isPrimary) { + context.getString(R.string.CallParticipant__you) + } else if (recipient.isSelf) { + context.getString(R.string.CallParticipant__you_on_another_device) + } else if (isPrimary) { + recipient.getShortDisplayName(context) + } else { + context.getString(R.string.CallParticipant__s_on_another_device, recipient.getShortDisplayName(context)) + } + } + + fun withIdentityKey(identityKey: IdentityKey?): CallParticipant { + return copy(identityKey = identityKey) + } + + fun withVideoEnabled(videoEnabled: Boolean): CallParticipant { + return copy(isVideoEnabled = videoEnabled) + } + + fun withScreenSharingEnabled(enable: Boolean): CallParticipant { + return copy(isScreenSharing = enable) + } + + enum class DeviceOrdinal { + PRIMARY, SECONDARY + } + + companion object { + @JvmField + val EMPTY: CallParticipant = CallParticipant() + + @JvmStatic + fun createLocal( + cameraState: CameraState, + renderer: BroadcastVideoSink, + microphoneEnabled: Boolean + ): CallParticipant { + return CallParticipant( + callParticipantId = CallParticipantId(Recipient.self()), + recipient = Recipient.self(), + videoSink = renderer, + cameraState = cameraState, + isVideoEnabled = cameraState.isEnabled && cameraState.cameraCount > 0, + isMicrophoneEnabled = microphoneEnabled + ) + } + + @JvmStatic + fun createRemote( + callParticipantId: CallParticipantId, + recipient: Recipient, + identityKey: IdentityKey?, + renderer: BroadcastVideoSink, + audioEnabled: Boolean, + videoEnabled: Boolean, + lastSpoke: Long, + mediaKeysReceived: Boolean, + addedToCallTime: Long, + isScreenSharing: Boolean, + deviceOrdinal: DeviceOrdinal + ): CallParticipant { + return CallParticipant( + callParticipantId = callParticipantId, + recipient = recipient, + identityKey = identityKey, + videoSink = renderer, + isVideoEnabled = videoEnabled, + isMicrophoneEnabled = audioEnabled, + lastSpoke = lastSpoke, + isMediaKeysReceived = mediaKeysReceived, + addedToCallTime = addedToCallTime, + isScreenSharing = isScreenSharing, + deviceOrdinal = deviceOrdinal + ) + } + } +} 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 00d8f35f1a..8d09c534fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java @@ -119,7 +119,7 @@ public class WebRtcViewModel { this.participantLimit = state.getCallInfoState().getParticipantLimit(); this.localParticipant = CallParticipant.createLocal(state.getLocalDeviceState().getCameraState(), state.getVideoState().getLocalSink() != null ? state.getVideoState().getLocalSink() - : new BroadcastVideoSink(null), + : new BroadcastVideoSink(), state.getLocalDeviceState().isMicrophoneEnabled()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java index da077e6c51..0387aee92b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java @@ -22,6 +22,7 @@ import org.webrtc.CameraVideoCapturer; import org.webrtc.CapturerObserver; import org.webrtc.EglBase; import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoFrame; import java.util.LinkedList; import java.util.List; @@ -81,7 +82,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa if (capturer != null) { capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()), context, - observer); + new CameraCapturerWrapper(observer)); capturer.setOrientation(orientation); isInitialized = true; } @@ -297,4 +298,30 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context)); } } + + private class CameraCapturerWrapper implements CapturerObserver { + private final CapturerObserver observer; + + public CameraCapturerWrapper(@NonNull CapturerObserver observer) { + this.observer = observer; + } + + @Override + public void onCapturerStarted(boolean success) { + observer.onCapturerStarted(success); + if (success) { + cameraEventListener.onFullyInitialized(); + } + } + + @Override + public void onCapturerStopped() { + observer.onCapturerStopped(); + } + + @Override + public void onFrameCaptured(VideoFrame videoFrame) { + observer.onFrameCaptured(videoFrame); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java index 03d4d583cd..8a0a140a58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java @@ -3,5 +3,6 @@ package org.thoughtcrime.securesms.ringrtc; import androidx.annotation.NonNull; public interface CameraEventListener { + void onFullyInitialized(); void onCameraSwitchCompleted(@NonNull CameraState newCameraState); } 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 31174caa98..464f591609 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 @@ -95,6 +95,21 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor { .build(); } + @Override + protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) { + RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer(); + + Log.i(tag, "handleScreenSharingEnable(): call_id: " + activePeer.getCallId() + " enable: " + enable); + + CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient())); + CallParticipant newParticipant = oldParticipant.withScreenSharingEnabled(enable); + + return currentState.builder() + .changeCallInfoState() + .putParticipant(activePeer.getRecipient(), newParticipant) + .build(); + } + @Override protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) { RemotePeer remotePeer = currentState.getCallInfoState().getActivePeer(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java index 90a719d862..d87c2bc708 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java @@ -34,6 +34,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { @NonNull OfferMessage.Type offerType) { remotePeer.setCallStartTimestamp(System.currentTimeMillis()); + currentState = currentState.builder() .actionProcessor(new OutgoingCallActionProcessor(webRtcInteractor)) .changeCallInfoState() @@ -41,17 +42,20 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { .callState(WebRtcViewModel.State.CALL_OUTGOING) .putRemotePeer(remotePeer) .putParticipant(remotePeer.getRecipient(), - CallParticipant.createRemote( - new CallParticipantId(remotePeer.getRecipient()), - remotePeer.getRecipient(), - null, - new BroadcastVideoSink(currentState.getVideoState().getEglBase()), - true, - false, - 0, - true, - 0, - CallParticipant.DeviceOrdinal.PRIMARY + CallParticipant.createRemote(new CallParticipantId(remotePeer.getRecipient()), + remotePeer.getRecipient(), + null, + new BroadcastVideoSink(currentState.getVideoState().getEglBase(), + false, + true, + currentState.getLocalDeviceState().getOrientation().getDegrees()), + true, + false, + 0, + true, + 0, + false, + CallParticipant.DeviceOrdinal.PRIMARY )) .build(); @@ -85,17 +89,20 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { .activePeer(remotePeer) .callState(WebRtcViewModel.State.CALL_INCOMING) .putParticipant(remotePeer.getRecipient(), - CallParticipant.createRemote( - new CallParticipantId(remotePeer.getRecipient()), - remotePeer.getRecipient(), - null, - new BroadcastVideoSink(currentState.getVideoState().getEglBase()), - true, - false, - 0, - true, - 0, - CallParticipant.DeviceOrdinal.PRIMARY + CallParticipant.createRemote(new CallParticipantId(remotePeer.getRecipient()), + remotePeer.getRecipient(), + null, + new BroadcastVideoSink(currentState.getVideoState().getEglBase(), + false, + true, + currentState.getLocalDeviceState().getOrientation().getDegrees()), + true, + false, + 0, + true, + 0, + false, + CallParticipant.DeviceOrdinal.PRIMARY )) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java index 786ccc8ade..5e3797be68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java @@ -40,7 +40,7 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor { try { webRtcInteractor.getCallManager().setVideoEnable(enable); - } catch (CallException e) { + } catch (CallException e) { return callFailure(currentState, "setVideoEnable() failed: ", e); } @@ -77,10 +77,15 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor { } @Override - protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) { + protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) { return activeCallDelegate.handleRemoteVideoEnable(currentState, enable); } + @Override + protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) { + return activeCallDelegate.handleScreenSharingEnable(currentState, enable); + } + @Override protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.CallMetadata callMetadata, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java index 7ccd9bcb30..ce527aefa9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java @@ -5,6 +5,8 @@ import android.media.AudioManager; import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; +import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -109,6 +111,11 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor { public @NonNull WebRtcServiceState handleCameraSwitchCompleted(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) { Log.i(tag, "handleCameraSwitchCompleted():"); + BroadcastVideoSink localSink = currentState.getVideoState().getLocalSink(); + if (localSink != null) { + localSink.setRotateToRightSide(newCameraState.getActiveDirection() == CameraState.Direction.BACK); + } + return currentState.builder() .changeLocalDeviceState() .cameraState(newCameraState) 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 index c4c980610f..4206076dee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -93,10 +93,13 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { VideoTrack videoTrack = device.getVideoTrack(); if (videoTrack != null) { videoSink = (callParticipant != null && callParticipant.getVideoSink().getEglBase() != null) ? callParticipant.getVideoSink() - : new BroadcastVideoSink(currentState.getVideoState().requireEglBase()); + : new BroadcastVideoSink(currentState.getVideoState().requireEglBase(), + true, + true, + currentState.getLocalDeviceState().getOrientation().getDegrees()); videoTrack.addSink(videoSink); } else { - videoSink = new BroadcastVideoSink(null); + videoSink = new BroadcastVideoSink(); } builder.putParticipant(callParticipantId, @@ -109,6 +112,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { device.getSpeakerTime(), device.getMediaKeysReceived(), device.getAddedTime(), + Boolean.TRUE.equals(device.getPresenting()), seen.contains(recipient) ? CallParticipant.DeviceOrdinal.SECONDARY : CallParticipant.DeviceOrdinal.PRIMARY)); @@ -318,11 +322,6 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { return terminateGroupCall(currentState); } - @Override - protected @NonNull WebRtcServiceState handleOrientationChanged(@NonNull WebRtcServiceState currentState, int orientationDegrees) { - return currentState; - } - public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) { return terminateGroupCall(currentState, true); } 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 index 2a68ba2ec3..1639204b03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java @@ -8,7 +8,6 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallException; -import org.signal.ringrtc.CallManager; import org.signal.ringrtc.GroupCall; import org.signal.ringrtc.PeekInfo; import org.thoughtcrime.securesms.BuildConfig; @@ -123,7 +122,17 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor { .clearParticipantMap(); for (Recipient recipient : callParticipants) { - builder.putParticipant(recipient, CallParticipant.createRemote(new CallParticipantId(recipient), recipient, null, new BroadcastVideoSink(null), true, true, 0, false, 0, CallParticipant.DeviceOrdinal.PRIMARY)); + builder.putParticipant(recipient, CallParticipant.createRemote(new CallParticipantId(recipient), + recipient, + null, + new BroadcastVideoSink(), + true, + true, + 0, + false, + 0, + false, + CallParticipant.DeviceOrdinal.PRIMARY)); } return builder.build(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java index a8c41cc61b..3f8c363478 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 @@ -10,7 +10,6 @@ import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallException; import org.signal.ringrtc.CallId; import org.signal.ringrtc.CallManager; -import org.thoughtcrime.securesms.components.webrtc.OrientationAwareVideoSink; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -24,7 +23,6 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.VideoState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.NetworkUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.webrtc.locks.LockManager; import org.webrtc.PeerConnection; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; @@ -89,8 +87,8 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { webRtcInteractor.getCallManager().proceed(activePeer.getCallId(), context, videoState.requireEglBase(), - new OrientationAwareVideoSink(videoState.requireLocalSink()), - new OrientationAwareVideoSink(callParticipant.getVideoSink()), + videoState.requireLocalSink(), + callParticipant.getVideoSink(), videoState.requireCamera(), iceServers, hideIp, @@ -198,6 +196,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { return activeCallDelegate.handleRemoteVideoEnable(currentState, enable); } + @Override + protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) { + return activeCallDelegate.handleScreenSharingEnable(currentState, enable); + } + @Override protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { return activeCallDelegate.handleReceivedOfferWhileActive(currentState, remotePeer); 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 fde99a367d..8e6e75444c 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 @@ -10,7 +10,6 @@ import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallException; import org.signal.ringrtc.CallId; import org.signal.ringrtc.CallManager; -import org.thoughtcrime.securesms.components.webrtc.OrientationAwareVideoSink; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.events.CallParticipant; @@ -120,8 +119,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor { webRtcInteractor.getCallManager().proceed(activePeer.getCallId(), context, videoState.requireEglBase(), - new OrientationAwareVideoSink(videoState.requireLocalSink()), - new OrientationAwareVideoSink(callParticipant.getVideoSink()), + videoState.requireLocalSink(), + callParticipant.getVideoSink(), videoState.requireCamera(), iceServers, isAlwaysTurn, @@ -198,6 +197,11 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor { return activeCallDelegate.handleRemoteVideoEnable(currentState, enable); } + @Override + protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) { + return activeCallDelegate.handleScreenSharingEnable(currentState, enable); + } + @Override protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) { return activeCallDelegate.handleLocalHangup(currentState); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 0fa75fab56..fa8711eb5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -394,6 +394,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. return p.handleRemoteVideoEnable(s, true); case REMOTE_VIDEO_DISABLE: return p.handleRemoteVideoEnable(s, false); + case REMOTE_SHARING_SCREEN_ENABLE: + return p.handleScreenSharingEnable(s, true); + case REMOTE_SHARING_SCREEN_DISABLE: + return p.handleScreenSharingEnable(s, false); case ENDED_REMOTE_HANGUP: case ENDED_REMOTE_HANGUP_NEED_PERMISSION: case ENDED_REMOTE_HANGUP_ACCEPTED: @@ -641,6 +645,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleGroupCallEnded(s, groupCall.hashCode(), groupCallEndReason)); } + @Override + public void onFullyInitialized() { + process((s, p) -> p.handleOrientationChanged(s, s.getLocalDeviceState().getOrientation().getDegrees())); + } + @Override public void onCameraSwitchCompleted(@NonNull final CameraState newCameraState) { process((s, p) -> p.handleCameraSwitchCompleted(s, newCameraState)); 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 b569668d7e..1fc7691096 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 @@ -12,6 +12,7 @@ import org.signal.ringrtc.CallId; import org.signal.ringrtc.CallManager; import org.signal.ringrtc.GroupCall; import org.thoughtcrime.securesms.components.sensors.Orientation; +import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.CallParticipant; @@ -276,6 +277,11 @@ public abstract class WebRtcActionProcessor { return currentState; } + protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) { + Log.i(tag, "handleScreenSharingEnable not processed"); + return currentState; + } + protected @NonNull WebRtcServiceState handleReceivedHangup(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, @NonNull HangupMetadata hangupMetadata) @@ -454,6 +460,15 @@ public abstract class WebRtcActionProcessor { camera.setOrientation(orientationDegrees); } + BroadcastVideoSink sink = currentState.getVideoState().getLocalSink(); + if (sink != null) { + sink.setDeviceOrientationDegrees(orientationDegrees); + } + + for (CallParticipant callParticipant : currentState.getCallInfoState().getRemoteCallParticipants()) { + callParticipant.getVideoSink().setDeviceOrientationDegrees(orientationDegrees); + } + return currentState.builder() .changeLocalDeviceState() .setOrientation(Orientation.fromDegrees(orientationDegrees)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java index 28b5f50f67..12cd7a6ac5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java @@ -6,7 +6,6 @@ import androidx.annotation.NonNull; import org.signal.core.util.ThreadUtil; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; -import org.thoughtcrime.securesms.components.webrtc.OrientationAwareVideoSink; import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.CameraEventListener; import org.thoughtcrime.securesms.ringrtc.CameraState; @@ -33,7 +32,10 @@ public final class WebRtcVideoUtil { ThreadUtil.runOnMainSync(() -> { EglBase eglBase = EglBase.create(); - BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase); + BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase, + true, + false, + currentState.getLocalDeviceState().getOrientation().getDegrees()); Camera camera = new Camera(context, cameraEventListener, eglBase, CameraState.Direction.FRONT); camera.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees()); @@ -104,7 +106,7 @@ public final class WebRtcVideoUtil { public static @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) { Camera camera = currentState.getVideoState().requireCamera(); - VideoSink sink = new OrientationAwareVideoSink(currentState.getVideoState().requireLocalSink()); + VideoSink sink = currentState.getVideoState().requireLocalSink(); if (camera.hasCapturer()) { camera.initCapturer(new CapturerObserver() { diff --git a/app/src/main/res/drawable/ic_share_screen_20.xml b/app/src/main/res/drawable/ic_share_screen_20.xml new file mode 100644 index 0000000000..25f564366a --- /dev/null +++ b/app/src/main/res/drawable/ic_share_screen_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/call_participant_item.xml b/app/src/main/res/layout/call_participant_item.xml index 792d9e47bc..e689fed1a9 100644 --- a/app/src/main/res/layout/call_participant_item.xml +++ b/app/src/main/res/layout/call_participant_item.xml @@ -39,15 +39,25 @@ app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + diff --git a/app/src/main/res/layout/call_toast_popup_window.xml b/app/src/main/res/layout/call_toast_popup_window.xml new file mode 100644 index 0000000000..e40fd4e993 --- /dev/null +++ b/app/src/main/res/layout/call_toast_popup_window.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ 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 e2456c7c33..eb695c360f 100644 --- a/app/src/main/res/layout/webrtc_call_view.xml +++ b/app/src/main/res/layout/webrtc_call_view.xml @@ -32,17 +32,20 @@ android:layout_height="match_parent" android:layout="@layout/group_call_call_full" /> - - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2739231e5..79bc9b0df4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1407,6 +1407,7 @@ No one else is here %1$s is in this call %1$s and %2$s are in this call + %1$s is presenting %1$s, %2$s, and %3$d other are in this call @@ -1427,6 +1428,9 @@ Can\'t receive audio and video from %1$s This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you. + + Swipe to view screen share + Proxy server Proxy address diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java b/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java index f87ebde06c..dfa3859985 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java @@ -178,7 +178,7 @@ public class CallParticipantListUpdateTest { private static CallParticipant createParticipant(long recipientId, long deMuxId, @NonNull CallParticipant.DeviceOrdinal deviceOrdinal) { Recipient recipient = new Recipient(RecipientId.from(recipientId), mock(RecipientDetails.class), true); - return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(null), false, false, -1, false, 0, deviceOrdinal); + return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(), false, false, -1, false, 0, false, deviceOrdinal); } } \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java index bd805cd357..b9c18de5da 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java @@ -239,12 +239,13 @@ public class ParticipantCollectionTest { new CallParticipantId(serializedId, RecipientId.from(serializedId)), Recipient.UNKNOWN, null, - new BroadcastVideoSink(null), + new BroadcastVideoSink(), false, false, lastSpoke, false, added, + false, CallParticipant.DeviceOrdinal.PRIMARY); } } \ No newline at end of file diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index f51c946f53..ed2deffc5c 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -495,8 +495,8 @@ dependencyVerification { ['org.signal:argon2:13.1', '0f686ccff0d4842bfcc74d92e8dc780a5f159b9376e37a1189fabbcdac458bef'], - ['org.signal:ringrtc-android:2.9.6', - 'daa06a2a31fb2c6001319ba30d3f412d8ed8de5eae8b49214a73c507f2d0eee9'], + ['org.signal:ringrtc-android:2.10.1.1', + 'a246d87001d485a76c88b6ba83451773e96ef7f19f1949a21e19de6d80f67b2d'], ['org.signal:zkgroup-android:0.7.0', '52b172565bd01526e93ebf1796b834bdc449d4fe3422c1b827e49cb8d4f13fbd'],