Add screen share receive support and improve video calling rotation.

fork-5.53.8
Cody Henthorne 2021-05-25 12:15:07 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 513e5b45c5
commit b9b2924939
41 zmienionych plików z 665 dodań i 397 usunięć

Wyświetl plik

@ -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'

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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<VideoSink, Boolean> sinks;
private final WeakHashMap<Object, Point> 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);

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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<CallParticipant> callParticipants = Collections.emptyList();
private List<CallParticipant> 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<CallParticipant> callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip) {
void update(@NonNull List<CallParticipant> 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);

Wyświetl plik

@ -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<CallParticipant> 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<CallParticipant> participants) {
List<CallParticipant> 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

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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<CallParticipant> 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<CallParticipant> 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<CallParticipant> 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);
}
}

Wyświetl plik

@ -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<WebRtcCallParticipantsPage, WebRtcCallParticipantsPagerAdapter.ViewHolder> {
@ -84,7 +86,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip());
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait());
}
}
@ -107,8 +109,14 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
CallParticipant participant = page.getCallParticipants().get(0);
callParticipantView.setCallParticipant(participant);
callParticipantView.setRenderInPip(page.isRenderInPip());
if (participant.isScreenSharing()) {
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
} else {
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
}
}
}

Wyświetl plik

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.webrtc.RendererCommon;
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
@ -61,6 +62,7 @@ class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant,
void bind(@NonNull CallParticipant callParticipant) {
callParticipantView.setCallParticipant(callParticipant);
callParticipantView.setRenderInPip(true);
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
}
}

Wyświetl plik

@ -1,9 +1,5 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
@ -12,13 +8,7 @@ import android.util.AttributeSet;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@ -43,10 +33,10 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.google.android.material.button.MaterialButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@ -67,6 +57,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallView extends FrameLayout {
private static final long TRANSITION_DURATION_MILLIS = 250;
@ -313,15 +305,15 @@ public class WebRtcCallView extends FrameLayout {
micToggle.setChecked(isMicEnabled, false);
}
public void updateCallParticipants(@NonNull CallParticipantsState state) {
public void updateCallParticipants(@NonNull CallParticipantsState state, boolean isPortrait) {
List<WebRtcCallParticipantsPage> 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();

Wyświetl plik

@ -61,19 +61,15 @@ public class WebRtcCallViewModel extends ViewModel {
private boolean answerWithVideoAvailable = false;
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callStarting = false;
private List<CallParticipant> 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<Orientation> 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 {

Wyświetl plik

@ -51,10 +51,6 @@ public final class WebRtcControls {
this.participantLimit = participantLimit;
}
boolean canRotateControls() {
return !isGroupCall();
}
boolean displayErrorControls() {
return isError();
}

Wyświetl plik

@ -10,14 +10,16 @@ import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
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<CallParticipa
videoMuted.setVisibility(model.getVideoMutedVisibility());
audioMuted.setVisibility(model.getAudioMutedVisibility());
screenSharing.setVisibility(model.getScreenSharingVisibility());
}
}

Wyświetl plik

@ -36,6 +36,10 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
return callParticipant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE;
}
public int getScreenSharingVisibility() {
return callParticipant.isScreenSharing() ? View.VISIBLE : View.GONE;
}
@Override
public boolean areItemsTheSame(@NonNull CallParticipantViewState newItem) {
return callParticipant.getCallParticipantId().equals(newItem.callParticipant.getCallParticipantId());

Wyświetl plik

@ -1,216 +0,0 @@
package org.thoughtcrime.securesms.events;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
import java.util.Objects;
public final class CallParticipant {
public static final CallParticipant EMPTY = createRemote(new CallParticipantId(Recipient.UNKNOWN), Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false, 0, true, 0, DeviceOrdinal.PRIMARY);
private final @NonNull CallParticipantId callParticipantId;
private final @NonNull CameraState cameraState;
private final @NonNull Recipient recipient;
private final @Nullable IdentityKey identityKey;
private final @NonNull BroadcastVideoSink videoSink;
private final boolean videoEnabled;
private final boolean microphoneEnabled;
private final long lastSpoke;
private final boolean mediaKeysReceived;
private final long addedToCallTime;
private final @NonNull DeviceOrdinal deviceOrdinal;
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
@NonNull BroadcastVideoSink renderer,
boolean microphoneEnabled)
{
return new CallParticipant(new CallParticipantId(Recipient.self()),
Recipient.self(),
null,
renderer,
cameraState,
cameraState.isEnabled() && cameraState.getCameraCount() > 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
}
}

Wyświetl plik

@ -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
)
}
}
}

Wyświetl plik

@ -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());
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -3,5 +3,6 @@ package org.thoughtcrime.securesms.ringrtc;
import androidx.annotation.NonNull;
public interface CameraEventListener {
void onFullyInitialized();
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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)

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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));

Wyświetl plik

@ -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))

Wyświetl plik

@ -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() {

Wyświetl plik

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#FF000000"
android:pathData="M17,3L3,3A2,2 0,0 0,1 5L1,15a2,2 0,0 0,2 2L17,17a2,2 0,0 0,2 -2L19,5A2,2 0,0 0,17 3ZM12.47,10.53 L11.37,9.43 10.75,8.5L10.75,14L9.25,14L9.25,8.5l-0.62,0.93 -1.1,1.1L6.47,9.47 10,5.94l3.53,3.53Z"/>
</vector>

Wyświetl plik

@ -39,15 +39,25 @@
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_participant_renderer"
<FrameLayout
android:id="@+id/call_participant_renderer_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/core_grey_80"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_participant_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/call_participant_mic_muted"

Wyświetl plik

@ -47,4 +47,12 @@
app:srcCompat="@drawable/ic_mic_off_solid_18"
app:tint="@color/core_white"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/call_participant_screen_sharing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
app:srcCompat="@drawable/ic_share_screen_20"
app:tint="@color/core_white"/>
</LinearLayout>

Wyświetl plik

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="94dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="12dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="12dp"
android:background="@drawable/call_participant_update_window_background"
android:minHeight="44dp"
android:padding="12dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="8dp"
android:gravity="center"
android:text="@string/CallToastPopupWindow__swipe_to_view_screen_share"
android:textAppearance="@style/TextAppearance.Signal.Body2"
app:drawableStartCompat="@drawable/ic_arrow_down"
app:drawableTint="@color/signal_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
</FrameLayout>

Wyświetl plik

@ -32,17 +32,20 @@
android:layout_height="match_parent"
android:layout="@layout/group_call_call_full" />
<View
android:id="@+id/call_screen_header_gradient"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="@drawable/webrtc_call_screen_header_gradient" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/call_screen_header_gradient"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="@drawable/webrtc_call_screen_header_gradient"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/call_screen_status_bar_guideline"
android:layout_width="wrap_content"
@ -93,7 +96,7 @@
android:id="@+id/call_screen_pip_area"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/call_screen_navigation_bar_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

Wyświetl plik

@ -1407,6 +1407,7 @@
<string name="WebRtcCallView__no_one_else_is_here">No one else is here</string>
<string name="WebRtcCallView__s_is_in_this_call">%1$s is in this call</string>
<string name="WebRtcCallView__s_and_s_are_in_this_call">%1$s and %2$s are in this call</string>
<string name="WebRtcCallView__s_is_presenting">%1$s is presenting</string>
<plurals name="WebRtcCallView__s_s_and_d_others_are_in_this_call">
<item quantity="one">%1$s, %2$s, and %3$d other are in this call</item>
@ -1427,6 +1428,9 @@
<string name="CallParticipantView__cant_receive_audio_and_video_from_s">Can\'t receive audio and video from %1$s</string>
<string name="CallParticipantView__this_may_be_Because_they_have_not_verified_your_safety_number_change">This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you.</string>
<!-- CallToastPopupWindow -->
<string name="CallToastPopupWindow__swipe_to_view_screen_share">Swipe to view screen share</string>
<!-- ProxyBottomSheetFragment -->
<string name="ProxyBottomSheetFragment_proxy_server">Proxy server</string>
<string name="ProxyBottomSheetFragment_proxy_address">Proxy address</string>

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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'],