kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add additional Group Calling features.
rodzic
8c1737e597
commit
b90a74d26a
|
@ -147,6 +147,7 @@ android {
|
|||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
|
@ -368,7 +369,7 @@ dependencies {
|
|||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.8.0'
|
||||
implementation 'org.signal:ringrtc-android:2.8.3'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
|
|
@ -60,6 +60,7 @@ public interface BindableConversationItem extends Unbindable {
|
|||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients);
|
||||
void onJoinGroupCallClicked();
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
|
|
@ -207,7 +207,6 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
private void initializeResources() {
|
||||
callScreen = findViewById(R.id.callScreen);
|
||||
callScreen.setControlsListener(new ControlsListener());
|
||||
callScreen.setEventListener(new EventListener());
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
|
@ -218,6 +217,17 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||
|
||||
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null) {
|
||||
if (state.needsNewRequestSizes()) {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||
|
@ -619,13 +629,4 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
viewModel.setIsViewingFocusedParticipant(page);
|
||||
}
|
||||
}
|
||||
|
||||
private class EventListener implements WebRtcCallView.EventListener {
|
||||
@Override
|
||||
public void onPotentialLayoutChange() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
@ -132,9 +133,23 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
setAvatar(GlideApp.with(this), recipient, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows self as the profile avatar.
|
||||
*/
|
||||
public void setAvatarUsingProfile(@Nullable Recipient recipient) {
|
||||
setAvatar(GlideApp.with(this), recipient, false, true);
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
|
||||
setAvatar(requestManager, recipient, quickContactEnabled, false);
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
|
||||
if (recipient != null) {
|
||||
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
|
||||
RecipientContactPhoto photo = (recipient.isSelf() && useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
|
||||
new ProfileContactPhoto(Recipient.self(),
|
||||
Recipient.self().getProfileAvatar()))
|
||||
: new RecipientContactPhoto(recipient);
|
||||
|
||||
if (!photo.equals(recipientContactPhoto)) {
|
||||
requestManager.clear(this);
|
||||
|
@ -218,9 +233,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
private final boolean ready;
|
||||
|
||||
RecipientContactPhoto(@NonNull Recipient recipient) {
|
||||
this(recipient, recipient.getContactPhoto());
|
||||
}
|
||||
|
||||
RecipientContactPhoto(@NonNull Recipient recipient, @Nullable ContactPhoto contactPhoto) {
|
||||
this.recipient = recipient;
|
||||
this.ready = !recipient.isResolving();
|
||||
this.contactPhoto = recipient.getContactPhoto();
|
||||
this.contactPhoto = contactPhoto;
|
||||
}
|
||||
|
||||
public boolean equals(@Nullable RecipientContactPhoto other) {
|
||||
|
|
|
@ -17,11 +17,13 @@ public class BroadcastVideoSink implements VideoSink {
|
|||
private final EglBase eglBase;
|
||||
private final WeakHashMap<VideoSink, Boolean> sinks;
|
||||
private final WeakHashMap<Object, Point> requestingSizes;
|
||||
private boolean dirtySizes;
|
||||
|
||||
public BroadcastVideoSink(@Nullable EglBase eglBase) {
|
||||
this.eglBase = eglBase;
|
||||
this.sinks = new WeakHashMap<>();
|
||||
this.requestingSizes = new WeakHashMap<>();
|
||||
this.dirtySizes = true;
|
||||
}
|
||||
|
||||
public @Nullable EglBase getEglBase() {
|
||||
|
@ -46,12 +48,14 @@ public class BroadcastVideoSink implements VideoSink {
|
|||
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
|
||||
synchronized (requestingSizes) {
|
||||
requestingSizes.put(object, size);
|
||||
dirtySizes = true;
|
||||
}
|
||||
}
|
||||
|
||||
void removeRequestingSize(@NonNull Object object) {
|
||||
synchronized (requestingSizes) {
|
||||
requestingSizes.remove(object);
|
||||
dirtySizes = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,6 +75,14 @@ public class BroadcastVideoSink implements VideoSink {
|
|||
return new RequestedSize(width, height);
|
||||
}
|
||||
|
||||
public void newSizeRequested() {
|
||||
dirtySizes = false;
|
||||
}
|
||||
|
||||
public boolean needsNewRequestingSize() {
|
||||
return dirtySizes;
|
||||
}
|
||||
|
||||
public static class RequestedSize {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
@ -42,6 +43,7 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
private TextureViewRenderer renderer;
|
||||
private ImageView pipAvatar;
|
||||
private ContactPhoto contactPhoto;
|
||||
private View audioMuted;
|
||||
|
||||
public CallParticipantView(@NonNull Context context) {
|
||||
super(context);
|
||||
|
@ -59,9 +61,10 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
avatar = findViewById(R.id.call_participant_item_avatar);
|
||||
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
|
||||
renderer = findViewById(R.id.call_participant_renderer);
|
||||
avatar = findViewById(R.id.call_participant_item_avatar);
|
||||
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
|
||||
renderer = findViewById(R.id.call_participant_renderer);
|
||||
audioMuted = findViewById(R.id.call_participant_mic_muted);
|
||||
|
||||
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
|
||||
useLargeAvatar();
|
||||
|
@ -83,11 +86,13 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
}
|
||||
|
||||
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
|
||||
avatar.setAvatar(participant.getRecipient());
|
||||
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
|
||||
avatar.setAvatarUsingProfile(participant.getRecipient());
|
||||
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this, true);
|
||||
setPipAvatar(participant.getRecipient());
|
||||
contactPhoto = participant.getRecipient().getContactPhoto();
|
||||
}
|
||||
|
||||
audioMuted.setVisibility(participant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
void setRenderInPip(boolean shouldRenderInPip) {
|
||||
|
@ -103,6 +108,10 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
changeAvatarParams(SMALL_AVATAR);
|
||||
}
|
||||
|
||||
void releaseRenderer() {
|
||||
renderer.release();
|
||||
}
|
||||
|
||||
private void changeAvatarParams(int dimension) {
|
||||
ViewGroup.LayoutParams params = avatar.getLayoutParams();
|
||||
if (params.height != dimension) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.google.android.flexbox.FlexboxLayout;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
@ -88,6 +89,8 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
|||
}
|
||||
} else if (childCount > count) {
|
||||
for (int i = count; i < childCount; i++) {
|
||||
CallParticipantView callParticipantView = getChildAt(count).findViewById(R.id.group_call_participant);
|
||||
callParticipantView.releaseRenderer();
|
||||
removeViewAt(count);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,12 @@ import android.content.Context;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -146,6 +149,10 @@ public final class CallParticipantsState {
|
|||
return isInPipMode;
|
||||
}
|
||||
|
||||
public boolean needsNewRequestSizes() {
|
||||
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
|
||||
@NonNull WebRtcViewModel webRtcViewModel,
|
||||
boolean enableVideo)
|
||||
|
@ -161,7 +168,7 @@ public final class CallParticipantsState {
|
|||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
webRtcViewModel.getState(),
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
webRtcViewModel.getRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant);
|
||||
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
@ -233,8 +240,10 @@ public final class CallParticipantsState {
|
|||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
|
||||
} else {
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
} else {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE;
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE;
|
||||
|
|
|
@ -89,6 +89,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
|||
return;
|
||||
}
|
||||
|
||||
eglRenderer.clearImage();
|
||||
|
||||
if (attachedVideoSink != null) {
|
||||
attachedVideoSink.removeSink(this);
|
||||
attachedVideoSink.removeRequestingSize(this);
|
||||
|
@ -265,7 +267,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
|||
|
||||
@Override
|
||||
public void onFrame(VideoFrame videoFrame) {
|
||||
eglRenderer.onFrame(videoFrame);
|
||||
if (isAttachedToWindow()) {
|
||||
eglRenderer.onFrame(videoFrame);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -92,9 +92,9 @@ public class WebRtcCallView extends FrameLayout {
|
|||
private RecyclerView callParticipantsRecycler;
|
||||
private Toolbar toolbar;
|
||||
private MaterialButton startCall;
|
||||
private TextView participantCount;
|
||||
private int pagerBottomMarginDp;
|
||||
private boolean controlsVisible = true;
|
||||
private EventListener eventListener;
|
||||
|
||||
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
|
@ -168,7 +168,6 @@ public class WebRtcCallView extends FrameLayout {
|
|||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
|
||||
runIfNonNull(eventListener, EventListener::onPotentialLayoutChange);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -239,10 +238,6 @@ public class WebRtcCallView extends FrameLayout {
|
|||
this.controlsListener = controlsListener;
|
||||
}
|
||||
|
||||
public void setEventListener(@Nullable EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void setMicEnabled(boolean isMicEnabled) {
|
||||
micToggle.setChecked(isMicEnabled, false);
|
||||
}
|
||||
|
@ -262,6 +257,12 @@ public class WebRtcCallView extends FrameLayout {
|
|||
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isNotIdle() && participantCount != null) {
|
||||
boolean includeSelf = state.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
|
||||
|
||||
participantCount.setText(String.valueOf(state.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
|
||||
}
|
||||
|
||||
pagerAdapter.submitList(pages);
|
||||
recyclerAdapter.submitList(state.getListParticipants());
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
|
||||
|
@ -271,10 +272,6 @@ public class WebRtcCallView extends FrameLayout {
|
|||
} else {
|
||||
layoutParticipantsForSmallCount();
|
||||
}
|
||||
|
||||
if (eventListener != null) {
|
||||
eventListener.onPotentialLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
|
||||
|
@ -362,7 +359,11 @@ public class WebRtcCallView extends FrameLayout {
|
|||
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, recipient.getDisplayName(getContext())));
|
||||
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
|
||||
toolbar.inflateMenu(R.menu.group_call);
|
||||
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
|
||||
|
||||
View showParticipants = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list).getActionView();
|
||||
showParticipants.setOnClickListener(unused -> showParticipantsList());
|
||||
|
||||
participantCount = showParticipants.findViewById(R.id.show_participants_menu_counter);
|
||||
}
|
||||
} else {
|
||||
recipientName.setText(recipient.getDisplayName(getContext()));
|
||||
|
@ -398,15 +399,13 @@ public class WebRtcCallView extends FrameLayout {
|
|||
case DISCONNECTED:
|
||||
status.setText(R.string.WebRtcCallView__disconnected);
|
||||
break;
|
||||
case CONNECTING:
|
||||
status.setText(R.string.WebRtcCallView__connecting);
|
||||
break;
|
||||
case RECONNECTING:
|
||||
status.setText(R.string.WebRtcCallView__reconnecting);
|
||||
break;
|
||||
case CONNECTED_AND_JOINING:
|
||||
status.setText(R.string.WebRtcCallView__joining);
|
||||
break;
|
||||
case CONNECTING:
|
||||
case CONNECTED_AND_JOINED:
|
||||
case CONNECTED:
|
||||
status.setText("");
|
||||
|
|
|
@ -117,7 +117,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
callConnectedTime = webRtcViewModel.getCallConnectedTime();
|
||||
startTimer();
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.getGroupState().isNotIdleOrConnected()) {
|
||||
cancelTimer();
|
||||
callConnectedTime = -1;
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ public final class WebRtcControls {
|
|||
}
|
||||
|
||||
boolean displayGroupMembersButton() {
|
||||
return groupCallState == GroupCallState.CONNECTED;
|
||||
return groupCallState.isAtLeast(GroupCallState.CONNECTING);
|
||||
}
|
||||
|
||||
boolean displayEndCall() {
|
||||
|
@ -156,8 +156,12 @@ public final class WebRtcControls {
|
|||
public enum GroupCallState {
|
||||
NONE,
|
||||
DISCONNECTED,
|
||||
RECONNECTING,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING
|
||||
CONNECTED;
|
||||
|
||||
boolean isAtLeast(@SuppressWarnings("SameParameterValue") @NonNull GroupCallState other) {
|
||||
return compareTo(other) >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||
|
||||
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
|
||||
|
||||
private final ImageView videoMuted;
|
||||
private final ImageView audioMuted;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull CallParticipantViewState model) {
|
||||
super.bind(model);
|
||||
|
||||
videoMuted.setVisibility(model.getVideoMutedVisibility());
|
||||
audioMuted.setVisibility(model.getAudioMutedVisibility());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
@ -18,4 +22,18 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
|
|||
public @NonNull Recipient getRecipient() {
|
||||
return callParticipant.getRecipient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getName(@NonNull Context context) {
|
||||
return callParticipant.getRecipient().isSelf() ? context.getString(R.string.GroupMembersDialog_you)
|
||||
: super.getName(context);
|
||||
}
|
||||
|
||||
public int getVideoMutedVisibility() {
|
||||
return callParticipant.isVideoEnabled() ? View.GONE : View.VISIBLE;
|
||||
}
|
||||
|
||||
public int getAudioMutedVisibility() {
|
||||
return callParticipant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1419,6 +1419,11 @@ public class ConversationFragment extends LoggingFragment {
|
|||
public void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients) {
|
||||
GroupsV1MigrationInfoBottomSheetDialogFragment.showForLearnMore(requireFragmentManager(), pendingRecipients);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onJoinGroupCallClicked() {
|
||||
CommunicationActions.startVideoCall(requireActivity(), recipient.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -27,13 +27,17 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public final class ConversationUpdateItem extends LinearLayout
|
||||
|
@ -177,6 +181,28 @@ public final class ConversationUpdateItem extends LinearLayout
|
|||
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationEventInvites());
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
|
||||
|
||||
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody());
|
||||
Collection<UUID> uuids = updateDescription.getMentioned();
|
||||
|
||||
int text = 0;
|
||||
if (Util.hasItems(uuids)) {
|
||||
text = uuids.contains(TextSecurePreferences.getLocalUuid(getContext())) ? R.string.ConversationUpdateItem_return_to_call : R.string.ConversationUpdateItem_join_call;
|
||||
}
|
||||
|
||||
if (text != 0) {
|
||||
actionButton.setText(text);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onJoinGroupCallClicked();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
}
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
|
|
|
@ -461,6 +461,8 @@ public final class ConversationListItem extends RelativeLayout
|
|||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_audio_call), defaultTint);
|
||||
} else if (SmsDatabase.Types.isMissedVideoCall(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_video_call), defaultTint);
|
||||
} else if (MmsSmsColumns.Types.isGroupCall(thread.getType())) {
|
||||
return emphasisAdded(context, MessageRecord.getGroupCallUpdateDescription(context, thread.getBody()), defaultTint);
|
||||
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context)))));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -50,6 +49,7 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class MessageDatabase extends Database implements MmsSmsColumns {
|
||||
|
||||
|
@ -130,6 +130,12 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
|||
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer);
|
||||
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer);
|
||||
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer);
|
||||
public abstract @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@NonNull RecipientId sender,
|
||||
long timestamp,
|
||||
@Nullable String messageGroupCallEraId,
|
||||
@Nullable String peekGroupCallEraId,
|
||||
@NonNull Collection<UUID> peekJoinedUuids);
|
||||
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);
|
||||
|
|
|
@ -92,6 +92,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
|
||||
|
||||
|
@ -397,6 +398,17 @@ public class MmsDatabase extends MessageDatabase {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@NonNull RecipientId sender,
|
||||
long timestamp,
|
||||
@Nullable String messageGroupCallEraId,
|
||||
@Nullable String peekGroupCallEraId,
|
||||
@NonNull Collection<UUID> peekJoinedUuids)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -44,6 +44,7 @@ public interface MmsSmsColumns {
|
|||
protected static final long GV1_MIGRATION_TYPE = 9;
|
||||
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
|
||||
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
|
||||
protected static final long GROUP_CALL_TYPE = 12;
|
||||
|
||||
protected static final long BASE_INBOX_TYPE = 20;
|
||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||
|
@ -214,7 +215,8 @@ public interface MmsSmsColumns {
|
|||
isOutgoingAudioCall(type) ||
|
||||
isOutgoingVideoCall(type) ||
|
||||
isMissedAudioCall(type) ||
|
||||
isMissedVideoCall(type);
|
||||
isMissedVideoCall(type) ||
|
||||
isGroupCall(type);
|
||||
}
|
||||
|
||||
public static boolean isExpirationTimerUpdate(long type) {
|
||||
|
@ -237,7 +239,6 @@ public interface MmsSmsColumns {
|
|||
return type == OUTGOING_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isMissedAudioCall(long type) {
|
||||
return type == MISSED_AUDIO_CALL_TYPE;
|
||||
}
|
||||
|
@ -246,6 +247,10 @@ public interface MmsSmsColumns {
|
|||
return type == MISSED_VIDEO_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupCall(long type) {
|
||||
return type == GROUP_CALL_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupUpdate(long type) {
|
||||
return (type & GROUP_UPDATE_BIT) != 0;
|
||||
}
|
||||
|
|
|
@ -35,9 +35,11 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
|||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
|
@ -57,6 +59,7 @@ import org.thoughtcrime.securesms.util.CursorUtil;
|
|||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
|
@ -69,7 +72,9 @@ import java.util.Collections;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Database for storage of SMS messages.
|
||||
|
@ -666,6 +671,89 @@ public class SmsDatabase extends MessageDatabase {
|
|||
return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@NonNull RecipientId sender,
|
||||
long timestamp,
|
||||
@Nullable String messageGroupCallEraId,
|
||||
@Nullable String peekGroupCallEraId,
|
||||
@NonNull Collection<UUID> peekJoinedUuids)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
Recipient recipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
|
||||
boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids);
|
||||
|
||||
if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
|
||||
byte[] updateDetails = GroupCallUpdateDetails.newBuilder()
|
||||
.setEraId(Util.emptyIfNull(peekGroupCallEraId))
|
||||
.setStartedCallUuid(Recipient.resolved(sender).requireUuid().toString())
|
||||
.setStartedCallTimestamp(timestamp)
|
||||
.addAllInCallUuids(Stream.of(peekJoinedUuids).map(UUID::toString).toList())
|
||||
.build()
|
||||
.toByteArray();
|
||||
|
||||
String body = Base64.encodeBytes(updateDetails);
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, sender.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, 1);
|
||||
values.put(DATE_RECEIVED, timestamp);
|
||||
values.put(DATE_SENT, timestamp);
|
||||
values.put(READ, 0);
|
||||
values.put(BODY, body);
|
||||
values.put(TYPE, Types.GROUP_CALL_TYPE);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
||||
db.insert(TABLE_NAME, null, values);
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
}
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection<UUID> peekJoinedUuids) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = TYPE + " = ? AND " + THREAD_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId);
|
||||
|
||||
try (Reader reader = new Reader(db.query(TABLE_NAME, MESSAGE_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) {
|
||||
MessageRecord record = reader.getNext();
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody());
|
||||
boolean sameEraId = groupCallUpdateDetails.getEraId().equals(peekGroupCallEraId) && !Util.isEmpty(peekGroupCallEraId);
|
||||
|
||||
List<String> inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList()
|
||||
: Collections.emptyList();
|
||||
|
||||
String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(BODY, body);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(record.getId()));
|
||||
|
||||
return sameEraId;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
|
|
|
@ -164,6 +164,10 @@ public abstract class DisplayRecord {
|
|||
return SmsDatabase.Types.isMissedVideoCall(type);
|
||||
}
|
||||
|
||||
public final boolean isGroupCall() {
|
||||
return SmsDatabase.Types.isGroupCall(type);
|
||||
}
|
||||
|
||||
public boolean isVerificationStatusChange() {
|
||||
return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public final class GroupCallUpdateDetailsUtil {
|
||||
|
||||
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
|
||||
|
||||
private GroupCallUpdateDetailsUtil() {
|
||||
}
|
||||
|
||||
public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) {
|
||||
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetails.getDefaultInstance();
|
||||
|
||||
if (body == null) {
|
||||
return groupCallUpdateDetails;
|
||||
}
|
||||
|
||||
try {
|
||||
groupCallUpdateDetails = GroupCallUpdateDetails.parseFrom(Base64.decode(body));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Group call update details could not be read", e);
|
||||
}
|
||||
|
||||
return groupCallUpdateDetails;
|
||||
}
|
||||
|
||||
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids) {
|
||||
GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.toBuilder()
|
||||
.clearInCallUuids();
|
||||
|
||||
if (Util.hasItems(inCallUuids)) {
|
||||
builder.addAllInCallUuids(inCallUuids);
|
||||
}
|
||||
|
||||
return Base64.encodeBytes(builder.build().toByteArray());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Create a group call update message based on time and joined members.
|
||||
*/
|
||||
public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFactory {
|
||||
private final Context context;
|
||||
private final List<UUID> joinedMembers;
|
||||
private final GroupCallUpdateDetails groupCallUpdateDetails;
|
||||
private final UUID selfUuid;
|
||||
|
||||
public GroupCallUpdateMessageFactory(@NonNull Context context, @NonNull List<UUID> joinedMembers, @NonNull GroupCallUpdateDetails groupCallUpdateDetails) {
|
||||
this.context = context;
|
||||
this.joinedMembers = new ArrayList<>(joinedMembers);
|
||||
this.groupCallUpdateDetails = groupCallUpdateDetails;
|
||||
this.selfUuid = TextSecurePreferences.getLocalUuid(context);
|
||||
|
||||
boolean removed = this.joinedMembers.remove(selfUuid);
|
||||
if (removed) {
|
||||
this.joinedMembers.add(selfUuid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String create() {
|
||||
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.getStartedCallTimestamp());
|
||||
|
||||
switch (joinedMembers.size()) {
|
||||
case 0:
|
||||
return context.getString(R.string.MessageRecord_group_call_s, time);
|
||||
case 1:
|
||||
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.getStartedCallUuid())) {
|
||||
return context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time);
|
||||
} else if (Objects.equals(joinedMembers.get(0), selfUuid)) {
|
||||
return context.getString(R.string.MessageRecord_you_are_in_the_group_call_s, describe(joinedMembers.get(0)), time);
|
||||
} else {
|
||||
return context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time);
|
||||
}
|
||||
case 2:
|
||||
return context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s,
|
||||
describe(joinedMembers.get(0)),
|
||||
describe(joinedMembers.get(1)),
|
||||
time);
|
||||
default:
|
||||
int others = joinedMembers.size() - 2;
|
||||
return context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s,
|
||||
others,
|
||||
describe(joinedMembers.get(0)),
|
||||
describe(joinedMembers.get(1)),
|
||||
others,
|
||||
time);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String describe(@NonNull UUID uuid) {
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
return context.getString(R.string.MessageRecord_unknown);
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.resolved(RecipientId.from(uuid, null));
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
return context.getString(R.string.MessageRecord_you);
|
||||
} else {
|
||||
return recipient.getShortDisplayName(context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
|
@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
|
|||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
|
@ -154,6 +157,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_audio_call_date, getCallDateString(context)), R.drawable.ic_update_audio_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else if (isMissedVideoCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, getCallDateString(context)), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else if (isGroupCall()) {
|
||||
return getGroupCallUpdateDescription(context, getBody());
|
||||
} else if (isJoined()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)), R.drawable.ic_update_group_add_16);
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
|
@ -281,6 +286,19 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return context.getString(R.string.MessageRecord_changed_their_profile, getIndividualRecipient().getDisplayName(context));
|
||||
}
|
||||
|
||||
public static @NonNull UpdateDescription getGroupCallUpdateDescription(@NonNull Context context, @NonNull String body) {
|
||||
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(body);
|
||||
|
||||
List<UUID> joinedMembers = Stream.of(groupCallUpdateDetails.getInCallUuidsList())
|
||||
.map(UuidUtil::parseOrNull)
|
||||
.withoutNulls()
|
||||
.toList();
|
||||
|
||||
UpdateDescription.StringFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, groupCallUpdateDetails);
|
||||
|
||||
return UpdateDescription.mentioning(joinedMembers, stringFactory, R.drawable.ic_video_16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
|
||||
*/
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.jobmanager.JobMigrator;
|
|||
import org.thoughtcrime.securesms.jobmanager.impl.FactoryJobPredicate;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.jobs.MarkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
|
@ -146,7 +147,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
|||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(context), SignalExecutors.newCachedSingleThreadExecutor("signal-fast-job-storage")))
|
||||
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushDecryptMessageJob.KEY, PushProcessMessageJob.KEY, MarkerJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushTextSendJob.KEY, PushMediaSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushTextSendJob.KEY, PushMediaSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY, GroupCallUpdateSendJob.KEY))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.util.Objects;
|
|||
|
||||
public class CallParticipant {
|
||||
|
||||
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false);
|
||||
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false);
|
||||
|
||||
private final @NonNull CameraState cameraState;
|
||||
private final @NonNull Recipient recipient;
|
||||
|
@ -36,9 +36,10 @@ public class CallParticipant {
|
|||
public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
|
||||
@Nullable IdentityKey identityKey,
|
||||
@NonNull BroadcastVideoSink renderer,
|
||||
boolean audioEnabled,
|
||||
boolean videoEnabled)
|
||||
{
|
||||
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, true);
|
||||
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled);
|
||||
}
|
||||
|
||||
private CallParticipant(@NonNull Recipient recipient,
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Send a group call update message to every one in a V2 group. Used to indicate you
|
||||
* have joined or left a call.
|
||||
*/
|
||||
public class GroupCallUpdateSendJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "GroupCallUpdateSendJob";
|
||||
|
||||
private static final String TAG = Log.tag(GroupCallUpdateSendJob.class);
|
||||
|
||||
private static final String KEY_RECIPIENT_ID = "recipient_id";
|
||||
private static final String KEY_ERA_ID = "era_id";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final String eraId;
|
||||
private final List<RecipientId> recipients;
|
||||
private final int initialRecipientCount;
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull GroupCallUpdateSendJob create(@NonNull RecipientId recipientId, @Nullable String eraId) {
|
||||
Recipient conversationRecipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (!conversationRecipient.isPushV2Group()) {
|
||||
throw new AssertionError("We have a recipient, but it's not a V2 Group");
|
||||
}
|
||||
|
||||
List<RecipientId> recipients = Stream.of(RecipientUtil.getEligibleForSending(conversationRecipient.getParticipants()))
|
||||
.filterNot(Recipient::isSelf)
|
||||
.map(Recipient::getId)
|
||||
.toList();
|
||||
|
||||
return new GroupCallUpdateSendJob(recipientId,
|
||||
eraId,
|
||||
recipients,
|
||||
recipients.size(),
|
||||
new Parameters.Builder()
|
||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||
.setLifespan(TimeUnit.MINUTES.toMillis(5))
|
||||
.setMaxAttempts(3)
|
||||
.build());
|
||||
}
|
||||
|
||||
private GroupCallUpdateSendJob(@NonNull RecipientId recipientId,
|
||||
@NonNull String eraId,
|
||||
@NonNull List<RecipientId> recipients,
|
||||
int initialRecipientCount,
|
||||
@NonNull Parameters parameters)
|
||||
{
|
||||
super(parameters);
|
||||
|
||||
this.recipientId = recipientId;
|
||||
this.eraId = eraId;
|
||||
this.recipients = recipients;
|
||||
this.initialRecipientCount = initialRecipientCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize())
|
||||
.putString(KEY_ERA_ID, eraId)
|
||||
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
|
||||
.putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
Recipient conversationRecipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (!conversationRecipient.isPushV2Group()) {
|
||||
throw new AssertionError("We have a recipient, but it's not a V2 Group");
|
||||
}
|
||||
|
||||
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
|
||||
List<Recipient> completions = deliver(conversationRecipient, destinations);
|
||||
|
||||
for (Recipient completion : completions) {
|
||||
recipients.remove(completion.getId());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size());
|
||||
|
||||
if (!recipients.isEmpty()) {
|
||||
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof IOException ||
|
||||
e instanceof RetryLaterException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
if (recipients.size() < initialRecipientCount) {
|
||||
Log.w(TAG, "Only sent a group update to " + recipients.size() + "/" + initialRecipientCount + " recipients. Still, it sent to someone, so it stays.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "Failed to send the group update to all recipients!");
|
||||
}
|
||||
|
||||
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withGroupCallUpdate(new SignalServiceDataMessage.GroupCallUpdate(eraId));
|
||||
|
||||
if (conversationRecipient.isGroup()) {
|
||||
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<GroupCallUpdateSendJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
GroupCallUpdateSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
RecipientId recipientId = RecipientId.from(data.getString(KEY_RECIPIENT_ID));
|
||||
String eraId = data.getString(KEY_ERA_ID);
|
||||
List<RecipientId> recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS));
|
||||
int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT);
|
||||
|
||||
return new GroupCallUpdateSendJob(recipientId, eraId, recipients, initialRecipientCount, parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -70,6 +70,7 @@ public final class JobManagerFactories {
|
|||
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
|
||||
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
|
||||
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
|
||||
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
|
||||
put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory());
|
||||
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
|
|
|
@ -84,6 +84,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
|
@ -373,14 +374,15 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
|
||||
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId, groupId.get().requireV1());
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId, groupId);
|
||||
else if (message.getReaction().isPresent()) handleReaction(content, message);
|
||||
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
|
||||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
|
||||
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
|
||||
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId, groupId.get().requireV1());
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId, groupId);
|
||||
else if (message.getReaction().isPresent()) handleReaction(content, message);
|
||||
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
|
||||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
|
||||
else if (FeatureFlags.groupCalling() && message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId);
|
||||
|
||||
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
||||
handleUnknownGroupMessage(content, message.getGroupContext().get());
|
||||
|
@ -649,6 +651,28 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
context.startService(intent);
|
||||
}
|
||||
|
||||
private void handleGroupCallUpdateMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
@NonNull Optional<GroupId> groupId)
|
||||
{
|
||||
if (!groupId.isPresent() || !groupId.get().isV2()) {
|
||||
Log.w(TAG, "Invalid group for group call update message");
|
||||
return;
|
||||
}
|
||||
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(groupId.get());
|
||||
|
||||
Intent intent = new Intent(context, WebRtcCallService.class);
|
||||
|
||||
intent.setAction(WebRtcCallService.ACTION_GROUP_CALL_UPDATE_MESSAGE)
|
||||
.putExtra(WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_SENDER, RecipientId.from(content.getSender()).serialize())
|
||||
.putExtra(WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_GROUP, groupRecipientId.serialize())
|
||||
.putExtra(WebRtcCallService.EXTRA_GROUP_CALL_ERA_ID, message.getGroupCallUpdate().get().getEraId())
|
||||
.putExtra(WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP, content.getServerReceivedTimestamp());
|
||||
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
private void handleEndSessionMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull Optional<Long> smsMessageId)
|
||||
{
|
||||
|
|
|
@ -29,13 +29,16 @@ import org.signal.ringrtc.Remote;
|
|||
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -47,12 +50,15 @@ import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
|
|||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.ringrtc.TurnServerInfoParcel;
|
||||
import org.thoughtcrime.securesms.service.webrtc.IdleActionProcessor;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcInteractor;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
|
||||
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
|
||||
|
@ -125,6 +131,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
public static final String EXTRA_OPAQUE_MESSAGE = "opaque";
|
||||
public static final String EXTRA_UUID = "uuid";
|
||||
public static final String EXTRA_MESSAGE_AGE_SECONDS = "message_age_seconds";
|
||||
public static final String EXTRA_GROUP_CALL_END_REASON = "group_call_end_reason";
|
||||
public static final String EXTRA_GROUP_CALL_HASH = "group_call_hash";
|
||||
public static final String EXTRA_GROUP_CALL_UPDATE_SENDER = "group_call_update_sender";
|
||||
public static final String EXTRA_GROUP_CALL_UPDATE_GROUP = "group_call_update_group";
|
||||
public static final String EXTRA_GROUP_CALL_ERA_ID = "era_id";
|
||||
|
||||
public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN";
|
||||
public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL";
|
||||
|
@ -187,6 +198,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
public static final String ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF = "GROUP_REQUEST_MEMBERSHIP_PROOF";
|
||||
public static final String ACTION_GROUP_REQUEST_UPDATE_MEMBERS = "GROUP_REQUEST_UPDATE_MEMBERS";
|
||||
public static final String ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS = "GROUP_UPDATE_RENDERED_RESOLUTIONS";
|
||||
public static final String ACTION_GROUP_CALL_ENDED = "GROUP_CALL_ENDED";
|
||||
public static final String ACTION_GROUP_CALL_UPDATE_MESSAGE = "GROUP_CALL_UPDATE_MESSAGE";
|
||||
|
||||
public static final int BUSY_TONE_LENGTH = 2000;
|
||||
|
||||
|
@ -652,6 +665,38 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
sendMessage(new RemotePeer(RecipientId.from(uuid, null)), opaqueMessage);
|
||||
}
|
||||
|
||||
public void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(GroupCallUpdateSendJob.create(recipient.getId(), groupCallEraId)));
|
||||
|
||||
peekGroupCall(new WebRtcData.GroupCallUpdateMetadata(Recipient.self().getId(), recipient.getId(), groupCallEraId, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
public void peekGroupCall(@NonNull WebRtcData.GroupCallUpdateMetadata groupCallUpdateMetadata) {
|
||||
networkExecutor.execute(() -> {
|
||||
try {
|
||||
Recipient group = Recipient.resolved(groupCallUpdateMetadata.getGroupRecipientId());
|
||||
GroupId.V2 groupId = group.requireGroupId().requireV2();
|
||||
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(this, groupId);
|
||||
|
||||
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(this, groupId))
|
||||
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
|
||||
.toList();
|
||||
|
||||
callManager.peekGroupCall(BuildConfig.SIGNAL_SFU_URL, credential.getTokenBytes().toByteArray(), members, peekInfo -> {
|
||||
DatabaseFactory.getSmsDatabase(this).insertOrUpdateGroupCall(group.getId(),
|
||||
groupCallUpdateMetadata.getSender(),
|
||||
groupCallUpdateMetadata.getServerReceivedTimestamp(),
|
||||
groupCallUpdateMetadata.getGroupCallEraId(),
|
||||
peekInfo.getEraId(),
|
||||
peekInfo.getJoinedMembers());
|
||||
});
|
||||
|
||||
} catch (IOException | VerificationFailedException | CallException e) {
|
||||
Log.e(TAG, "error peeking", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartCall(@Nullable Remote remote, @NonNull CallId callId, @NonNull Boolean isOutgoing, @Nullable CallManager.CallMediaType callMediaType) {
|
||||
Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType);
|
||||
|
@ -967,11 +1012,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
|
||||
intent.setAction(ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF)
|
||||
.putExtra(EXTRA_GROUP_EXTERNAL_TOKEN, credential.getTokenBytes().toByteArray());
|
||||
.putExtra(EXTRA_GROUP_EXTERNAL_TOKEN, credential.getTokenBytes().toByteArray())
|
||||
.putExtra(EXTRA_GROUP_CALL_HASH, groupCall.hashCode());
|
||||
|
||||
startService(intent);
|
||||
} catch (IOException | VerificationFailedException e) {
|
||||
Log.w(TAG, "Unable to fetch group membership proof", e);
|
||||
onEnded(groupCall, GroupCall.GroupCallEndReason.DEVICE_EXPLICITLY_DISCONNECTED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -992,12 +1039,18 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onJoinedMembersChanged(@NonNull GroupCall groupCall) {
|
||||
public void onPeekChanged(@NonNull GroupCall groupCall) {
|
||||
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnded(@NonNull GroupCall groupCall, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
|
||||
Log.i(TAG, "onEnded: " + groupCallEndReason);
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
|
||||
intent.setAction(ACTION_GROUP_CALL_ENDED)
|
||||
.putExtra(EXTRA_GROUP_CALL_HASH, groupCall.hashCode())
|
||||
.putExtra(EXTRA_GROUP_CALL_END_REASON, groupCallEndReason.ordinal());
|
||||
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
|
||||
true,
|
||||
false
|
||||
))
|
||||
.build();
|
||||
|
@ -82,6 +83,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
|
||||
true,
|
||||
false
|
||||
))
|
||||
.build();
|
||||
|
|
|
@ -48,14 +48,14 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
|
||||
LongSparseArray<GroupCall.RemoteDeviceState> remoteDevices = groupCall.getRemoteDeviceStates();
|
||||
|
||||
for(int i = 0; i < remoteDevices.size(); i++) {
|
||||
for (int i = 0; i < remoteDevices.size(); i++) {
|
||||
GroupCall.RemoteDeviceState device = remoteDevices.get(remoteDevices.keyAt(i));
|
||||
Recipient recipient = Recipient.externalPush(context, device.getUserId(), null, false);
|
||||
CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId());
|
||||
CallParticipant callParticipant = participants.get(callParticipantId);
|
||||
|
||||
BroadcastVideoSink videoSink;
|
||||
VideoTrack videoTrack = device.getVideoTrack();
|
||||
VideoTrack videoTrack = device.getVideoTrack();
|
||||
if (videoTrack != null) {
|
||||
videoSink = (callParticipant != null && callParticipant.getVideoSink().getEglBase() != null) ? callParticipant.getVideoSink()
|
||||
: new BroadcastVideoSink(currentState.getVideoState().requireEglBase());
|
||||
|
@ -68,17 +68,23 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
CallParticipant.createRemote(recipient,
|
||||
null,
|
||||
videoSink,
|
||||
true));
|
||||
Boolean.FALSE.equals(device.getAudioMuted()),
|
||||
Boolean.FALSE.equals(device.getVideoMuted())));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
|
||||
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull byte[] groupMembershipToken) {
|
||||
Log.i(tag, "handleGroupRequestMembershipProof():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
|
||||
|
||||
if (groupCall == null || groupCall.hashCode() != groupCallHash) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
try {
|
||||
groupCall.setMembershipProof(groupMembershipToken);
|
||||
} catch (CallException e) {
|
||||
|
@ -96,7 +102,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
|
||||
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(context, group.requireGroupId().requireV2()))
|
||||
.map(e -> new GroupCall.GroupMemberInfo(e.getKey(), e.getValue().serialize()))
|
||||
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
|
||||
.toList();
|
||||
|
||||
try {
|
||||
|
@ -109,17 +115,20 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
|
||||
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
|
||||
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
|
||||
|
||||
ArrayList<GroupCall.RenderedResolution> renderedResolutions = new ArrayList<>(participants.size());
|
||||
ArrayList<GroupCall.VideoRequest> resolutionRequests = new ArrayList<>(participants.size());
|
||||
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
|
||||
BroadcastVideoSink.RequestedSize maxSize = entry.getValue().getVideoSink().getMaxRequestingSize();
|
||||
renderedResolutions.add(new GroupCall.RenderedResolution(entry.getKey().getDemuxId(), maxSize.getWidth(), maxSize.getHeight(), null));
|
||||
BroadcastVideoSink videoSink = entry.getValue().getVideoSink();
|
||||
BroadcastVideoSink.RequestedSize maxSize = videoSink.getMaxRequestingSize();
|
||||
|
||||
resolutionRequests.add(new GroupCall.VideoRequest(entry.getKey().getDemuxId(), maxSize.getWidth(), maxSize.getHeight(), null));
|
||||
videoSink.newSizeRequested();
|
||||
}
|
||||
|
||||
try {
|
||||
currentState.getCallInfoState().requireGroupCall().setRenderedResolutions(renderedResolutions);
|
||||
currentState.getCallInfoState().requireGroupCall().requestVideo(resolutionRequests);
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to set rendered resolutions", e);
|
||||
}
|
||||
|
@ -127,7 +136,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
|
||||
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
|
||||
try {
|
||||
webRtcInteractor.getCallManager().receivedHttpResponse(httpData.getRequestId(), httpData.getStatus(), httpData.getBody() != null ? httpData.getBody() : new byte[0]);
|
||||
} catch (CallException e) {
|
||||
|
@ -136,7 +145,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
|
||||
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
|
||||
try {
|
||||
webRtcInteractor.getCallManager().httpRequestFailed(httpData.getRequestId());
|
||||
} catch (CallException e) {
|
||||
|
@ -174,9 +183,43 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleGroupCallEnded(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
|
||||
Log.i(tag, "handleGroupCallEnded(): reason: " + groupCallEndReason);
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
|
||||
|
||||
if (groupCall == null || groupCall.hashCode() != groupCallHash) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
try {
|
||||
groupCall.disconnect();
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
|
||||
}
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeCallInfoState()
|
||||
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
|
||||
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
|
||||
.build();
|
||||
|
||||
webRtcInteractor.sendMessage(currentState);
|
||||
|
||||
return terminateGroupCall(currentState);
|
||||
}
|
||||
|
||||
public @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
|
||||
Log.w(tag, "groupCallFailure(): " + message, error);
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
|
||||
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
|
||||
|
||||
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
|
||||
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
|
||||
}
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeCallInfoState()
|
||||
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
|
||||
|
@ -186,7 +229,6 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
webRtcInteractor.sendMessage(currentState);
|
||||
|
||||
try {
|
||||
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
|
||||
if (groupCall != null) {
|
||||
groupCall.disconnect();
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.signal.ringrtc.PeekInfo;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
|
@ -20,6 +21,32 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
|
|||
super(webRtcInteractor, TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleGroupLocalDeviceStateChanged():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
GroupCall.LocalDeviceState device = groupCall.getLocalDeviceState();
|
||||
GroupCall.ConnectionState connectionState = device.getConnectionState();
|
||||
GroupCall.JoinState joinState = device.getJoinState();
|
||||
|
||||
Log.i(tag, "local device changed: " + connectionState + " " + joinState);
|
||||
|
||||
WebRtcViewModel.GroupCallState groupCallState = WebRtcUtil.groupCallStateForConnection(connectionState);
|
||||
|
||||
if (connectionState == GroupCall.ConnectionState.CONNECTED || connectionState == GroupCall.ConnectionState.CONNECTING) {
|
||||
if (joinState == GroupCall.JoinState.JOINED) {
|
||||
groupCallState = WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
|
||||
} else if (joinState == GroupCall.JoinState.JOINING) {
|
||||
groupCallState = WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING;
|
||||
}
|
||||
}
|
||||
|
||||
return currentState.builder().changeCallInfoState()
|
||||
.groupCallState(groupCallState)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
Log.i(TAG, "handleSetEnableVideo():");
|
||||
|
@ -58,16 +85,43 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleGroupJoinedMembershipChanged():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
PeekInfo peekInfo = groupCall.getPeekInfo();
|
||||
|
||||
if (peekInfo == null) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (currentState.getCallSetupState().hasSentJoinedMessage()) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), WebRtcUtil.getGroupCallEraId(groupCall));
|
||||
|
||||
return currentState.builder()
|
||||
.changeCallSetupState()
|
||||
.sentJoinedMessage(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(TAG, "handleLocalHangup():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
|
||||
try {
|
||||
groupCall.disconnect();
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
|
||||
}
|
||||
|
||||
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), WebRtcUtil.getGroupCallEraId(groupCall));
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeCallInfoState()
|
||||
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
|
||||
|
|
|
@ -8,6 +8,8 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.signal.ringrtc.PeekInfo;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
|
@ -40,6 +42,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
|||
|
||||
byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId();
|
||||
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
|
||||
BuildConfig.SIGNAL_SFU_URL,
|
||||
currentState.getVideoState().requireEglBase(),
|
||||
webRtcInteractor.getGroupCallObserver());
|
||||
|
||||
|
@ -96,8 +99,14 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
|||
Log.i(tag, "handleGroupJoinedMembershipChanged():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
PeekInfo peekInfo = groupCall.getPeekInfo();
|
||||
|
||||
List<Recipient> callParticipants = Stream.of(groupCall.getJoinedGroupMembers())
|
||||
if (peekInfo == null) {
|
||||
Log.i(tag, "No peek info available");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
List<Recipient> callParticipants = Stream.of(peekInfo.getJoinedMembers())
|
||||
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
|
||||
.toList();
|
||||
|
||||
|
@ -105,7 +114,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
|||
.changeCallInfoState();
|
||||
|
||||
for (Recipient recipient : callParticipants) {
|
||||
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), false));
|
||||
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), true, true));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
|
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraState;
|
|||
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.GroupCallUpdateMetadata;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.HttpData;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata;
|
||||
|
@ -58,6 +60,8 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_
|
|||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_SIGNALING_FAILURE;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_TIMEOUT;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_FLIP_CAMERA;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_ENDED;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_UPDATE_MESSAGE;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED;
|
||||
|
@ -116,6 +120,8 @@ import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getCa
|
|||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getEnable;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorCallState;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorIdentityKey;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupCallEndReason;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupCallHash;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupMembershipToken;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getIceCandidates;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getIceServers;
|
||||
|
@ -227,9 +233,11 @@ public abstract class WebRtcActionProcessor {
|
|||
case ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED: return handleGroupLocalDeviceStateChanged(currentState);
|
||||
case ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED: return handleGroupRemoteDeviceStateChanged(currentState);
|
||||
case ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED: return handleGroupJoinedMembershipChanged(currentState);
|
||||
case ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF: return handleGroupRequestMembershipProof(currentState, getGroupMembershipToken(intent));
|
||||
case ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF: return handleGroupRequestMembershipProof(currentState, getGroupCallHash(intent), getGroupMembershipToken(intent));
|
||||
case ACTION_GROUP_REQUEST_UPDATE_MEMBERS: return handleGroupRequestUpdateMembers(currentState);
|
||||
case ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS: return handleUpdateRenderedResolutions(currentState);
|
||||
case ACTION_GROUP_CALL_ENDED: return handleGroupCallEnded(currentState, getGroupCallHash(intent), getGroupCallEndReason(intent));
|
||||
case ACTION_GROUP_CALL_UPDATE_MESSAGE: return handleGroupCallUpdateMessage(currentState, GroupCallUpdateMetadata.fromIntent(intent));
|
||||
|
||||
case ACTION_HTTP_SUCCESS: return handleHttpSuccess(currentState, HttpData.fromIntent(intent));
|
||||
case ACTION_HTTP_FAILURE: return handleHttpFailure(currentState, HttpData.fromIntent(intent));
|
||||
|
@ -694,7 +702,7 @@ public abstract class WebRtcActionProcessor {
|
|||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
|
||||
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull byte[] groupMembershipToken) {
|
||||
Log.i(tag, "handleGroupRequestMembershipProof not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
@ -709,6 +717,16 @@ public abstract class WebRtcActionProcessor {
|
|||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleGroupCallEnded(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
|
||||
Log.i(tag, "handleGroupCallEnded not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleGroupCallUpdateMessage(@NonNull WebRtcServiceState currentState, @NonNull GroupCallUpdateMetadata groupCallUpdateMetadata) {
|
||||
webRtcInteractor.peekGroupCall(groupCallUpdateMetadata);
|
||||
return currentState;
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) {
|
||||
|
|
|
@ -7,12 +7,16 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_ERA_ID;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_GROUP;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_SENDER;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_DEVICE_ID;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_IS_LEGACY;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_TYPE;
|
||||
|
@ -22,6 +26,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_RE
|
|||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MESSAGE_AGE_SECONDS;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRecipientId;
|
||||
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemoteDevice;
|
||||
|
||||
/**
|
||||
|
@ -304,4 +309,44 @@ public class WebRtcData {
|
|||
return messageAgeSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata associated with a group call update message.
|
||||
*/
|
||||
public static class GroupCallUpdateMetadata {
|
||||
private final RecipientId sender;
|
||||
private final RecipientId groupRecipientId;
|
||||
private final String groupCallEraId;
|
||||
private final long serverReceivedTimestamp;
|
||||
|
||||
static @NonNull GroupCallUpdateMetadata fromIntent(@NonNull Intent intent) {
|
||||
return new GroupCallUpdateMetadata(getRecipientId(intent, EXTRA_GROUP_CALL_UPDATE_SENDER),
|
||||
getRecipientId(intent, EXTRA_GROUP_CALL_UPDATE_GROUP),
|
||||
intent.getStringExtra(EXTRA_GROUP_CALL_ERA_ID),
|
||||
intent.getLongExtra(EXTRA_SERVER_RECEIVED_TIMESTAMP, 0));
|
||||
}
|
||||
|
||||
public GroupCallUpdateMetadata(@NonNull RecipientId sender, @NonNull RecipientId groupRecipientId, @Nullable String groupCallEraId, long serverReceivedTimestamp) {
|
||||
this.sender = sender;
|
||||
this.groupRecipientId = groupRecipientId;
|
||||
this.groupCallEraId = groupCallEraId;
|
||||
this.serverReceivedTimestamp = serverReceivedTimestamp;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getGroupRecipientId() {
|
||||
return groupRecipientId;
|
||||
}
|
||||
|
||||
public @Nullable String getGroupCallEraId() {
|
||||
return groupCallEraId;
|
||||
}
|
||||
|
||||
public long getServerReceivedTimestamp() {
|
||||
return serverReceivedTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
|
@ -36,6 +38,8 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_CAMERA_
|
|||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ENABLE;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_CALL_STATE;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_IDENTITY_KEY;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_END_REASON;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_HASH;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_EXTERNAL_TOKEN;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ICE_CANDIDATES;
|
||||
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MULTI_RING;
|
||||
|
@ -174,15 +178,34 @@ public final class WebRtcIntentParser {
|
|||
public static @NonNull CameraState getCameraState(@NonNull Intent intent) {
|
||||
return Objects.requireNonNull(intent.getParcelableExtra(EXTRA_CAMERA_STATE));
|
||||
}
|
||||
|
||||
public static @NonNull WebRtcViewModel.State getErrorCallState(@NonNull Intent intent) {
|
||||
return (WebRtcViewModel.State) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_ERROR_CALL_STATE));
|
||||
}
|
||||
|
||||
public static @NonNull Optional<IdentityKey> getErrorIdentityKey(@NonNull Intent intent) {
|
||||
IdentityKeyParcelable identityKeyParcelable = (IdentityKeyParcelable) intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY);
|
||||
IdentityKeyParcelable identityKeyParcelable = intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY);
|
||||
if (identityKeyParcelable != null) {
|
||||
return Optional.fromNullable(identityKeyParcelable.get());
|
||||
}
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static int getGroupCallHash(@NonNull Intent intent) {
|
||||
return intent.getIntExtra(EXTRA_GROUP_CALL_HASH, 0);
|
||||
}
|
||||
|
||||
public static @NonNull GroupCall.GroupCallEndReason getGroupCallEndReason(@NonNull Intent intent) {
|
||||
int ordinal = intent.getIntExtra(EXTRA_GROUP_CALL_END_REASON, -1);
|
||||
|
||||
if (ordinal >= 0 && ordinal < GroupCall.GroupCallEndReason.values().length) {
|
||||
return GroupCall.GroupCallEndReason.values()[ordinal];
|
||||
}
|
||||
|
||||
return GroupCall.GroupCallEndReason.DEVICE_EXPLICITLY_DISCONNECTED;
|
||||
}
|
||||
|
||||
public static @NonNull RecipientId getRecipientId(@NonNull Intent intent, @NonNull String name) {
|
||||
return RecipientId.from(Objects.requireNonNull(intent.getStringExtra(name)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.annotation.Nullable;
|
|||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
|
@ -89,6 +88,10 @@ public class WebRtcInteractor {
|
|||
webRtcCallService.sendOpaqueCallMessage(uuid, callMessage);
|
||||
}
|
||||
|
||||
void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
|
||||
webRtcCallService.sendGroupCallMessage(recipient, groupCallEraId);
|
||||
}
|
||||
|
||||
void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
|
||||
webRtcCallService.setCallInProgressNotification(type, remotePeer.getRecipient());
|
||||
}
|
||||
|
@ -144,4 +147,8 @@ public class WebRtcInteractor {
|
|||
void startAudioCommunication(boolean preserveSpeakerphone) {
|
||||
audioManager.startCommunication(preserveSpeakerphone);
|
||||
}
|
||||
|
||||
void peekGroupCall(@NonNull WebRtcData.GroupCallUpdateMetadata groupCallUpdateMetadata) {
|
||||
webRtcCallService.peekGroupCall(groupCallUpdateMetadata);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import android.content.Context;
|
|||
import android.media.AudioManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.signal.ringrtc.PeekInfo;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
|
||||
|
@ -66,4 +68,13 @@ public final class WebRtcUtil {
|
|||
return WebRtcViewModel.GroupCallState.DISCONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable String getGroupCallEraId(@Nullable GroupCall groupCall) {
|
||||
if (groupCall == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PeekInfo peekInfo = groupCall.getPeekInfo();
|
||||
return peekInfo != null ? peekInfo.getEraId() : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,19 +9,21 @@ public final class CallSetupState {
|
|||
boolean enableVideoOnCreate;
|
||||
boolean isRemoteVideoOffer;
|
||||
boolean acceptWithVideo;
|
||||
boolean sentJoinedMessage;
|
||||
|
||||
public CallSetupState() {
|
||||
this(false, false, false);
|
||||
this(false, false, false, false);
|
||||
}
|
||||
|
||||
public CallSetupState(@NonNull CallSetupState toCopy) {
|
||||
this(toCopy.enableVideoOnCreate, toCopy.isRemoteVideoOffer, toCopy.acceptWithVideo);
|
||||
this(toCopy.enableVideoOnCreate, toCopy.isRemoteVideoOffer, toCopy.acceptWithVideo, toCopy.sentJoinedMessage);
|
||||
}
|
||||
|
||||
public CallSetupState(boolean enableVideoOnCreate, boolean isRemoteVideoOffer, boolean acceptWithVideo) {
|
||||
public CallSetupState(boolean enableVideoOnCreate, boolean isRemoteVideoOffer, boolean acceptWithVideo, boolean sentJoinedMessage) {
|
||||
this.enableVideoOnCreate = enableVideoOnCreate;
|
||||
this.isRemoteVideoOffer = isRemoteVideoOffer;
|
||||
this.acceptWithVideo = acceptWithVideo;
|
||||
this.sentJoinedMessage = sentJoinedMessage;
|
||||
}
|
||||
|
||||
public boolean isEnableVideoOnCreate() {
|
||||
|
@ -35,4 +37,8 @@ public final class CallSetupState {
|
|||
public boolean isAcceptWithVideo() {
|
||||
return acceptWithVideo;
|
||||
}
|
||||
|
||||
public boolean hasSentJoinedMessage() {
|
||||
return sentJoinedMessage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,11 @@ public class WebRtcServiceStateBuilder {
|
|||
toBuild.acceptWithVideo = acceptWithVideo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
|
||||
toBuild.sentJoinedMessage = sentJoinedMessage;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class VideoStateBuilder {
|
||||
|
|
|
@ -44,15 +44,25 @@ public final class AvatarUtil {
|
|||
}
|
||||
|
||||
public static void loadBlurredIconIntoViewBackground(@NonNull Recipient recipient, @NonNull View target) {
|
||||
loadBlurredIconIntoViewBackground(recipient, target, false);
|
||||
}
|
||||
|
||||
public static void loadBlurredIconIntoViewBackground(@NonNull Recipient recipient, @NonNull View target, boolean useSelfProfileAvatar) {
|
||||
Context context = target.getContext();
|
||||
|
||||
if (recipient.getContactPhoto() == null) {
|
||||
ContactPhoto photo;
|
||||
|
||||
if (recipient.isSelf() && useSelfProfileAvatar) {
|
||||
photo = new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar());
|
||||
} else if (recipient.getContactPhoto() == null) {
|
||||
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
|
||||
return;
|
||||
} else {
|
||||
photo = recipient.getContactPhoto();
|
||||
}
|
||||
|
||||
GlideApp.with(target)
|
||||
.load(recipient.getContactPhoto())
|
||||
.load(photo)
|
||||
.transform(new CenterCrop(), new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS))
|
||||
.into(new CustomViewTarget<View, Drawable>(target) {
|
||||
@Override
|
||||
|
|
|
@ -69,3 +69,10 @@ message BodyRangeList {
|
|||
|
||||
repeated BodyRange ranges = 1;
|
||||
}
|
||||
|
||||
message GroupCallUpdateDetails {
|
||||
string eraId = 1;
|
||||
string startedCallUuid = 2;
|
||||
int64 startedCallTimestamp = 3;
|
||||
repeated string inCallUuids = 4;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_icon_tint_primary"
|
||||
android:pathData="M10.25,16.25m-2.5,-8.5a3.5,3.5 0,1 0,3.5 3.5A3.5,3.5 0,0 0,7.75 7.75ZM11.289,14.777a4.988,4.988 0,0 1,-7.078 0A4.735,4.735 0,0 0,1 19.25L1,20a1,1 0,0 0,1 1L13.5,21a1,1 0,0 0,1 -1v-0.75A4.735,4.735 0,0 0,11.289 14.777ZM16.25,2.75a3.5,3.5 0,1 0,3.5 3.5A3.5,3.5 0,0 0,16.25 2.75ZM19.789,9.777a4.989,4.989 0,0 1,-7.074 0.005c-0.063,0.022 -0.131,0.033 -0.193,0.057a4.675,4.675 0,0 1,-0.333 3.663A6.294,6.294 0,0 1,15.078 16L22,16a1,1 0,0 0,1 -1v-0.75A4.735,4.735 0,0 0,19.789 9.777Z"/>
|
||||
</vector>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_group_solid_24" />
|
||||
</layer-list>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,13L3,13a2,2 0,0 1,-2 -2L1,5A2,2 0,0 1,3 3L9,3a2,2 0,0 1,2 2v6A2,2 0,0 1,9 13ZM12,9.5 L13.72,11.22A0.75,0.75 0,0 0,15 10.69L15,5.31a0.75,0.75 0,0 0,-1.28 -0.53L12,6.5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_icon_tint_primary"
|
||||
android:pathData="M10.25,16.25m-2.5,-8.5a3.5,3.5 0,1 0,3.5 3.5A3.5,3.5 0,0 0,7.75 7.75ZM11.289,14.777a4.988,4.988 0,0 1,-7.078 0A4.735,4.735 0,0 0,1 19.25L1,20a1,1 0,0 0,1 1L13.5,21a1,1 0,0 0,1 -1v-0.75A4.735,4.735 0,0 0,11.289 14.777ZM16.25,2.75a3.5,3.5 0,1 0,3.5 3.5A3.5,3.5 0,0 0,16.25 2.75ZM19.789,9.777a4.989,4.989 0,0 1,-7.074 0.005c-0.063,0.022 -0.131,0.033 -0.193,0.057a4.675,4.675 0,0 1,-0.333 3.663A6.294,6.294 0,0 1,15.078 16L22,16a1,1 0,0 0,1 -1v-0.75A4.735,4.735 0,0 0,19.789 9.777Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:fillColor="@color/core_grey_75"
|
||||
android:pathData="M25.56,3.56l-22,22L2.5,24.5l4.67,-4.67A9.22,9.22 0,0 1,5.5 14.5L7,14.5a7.74,7.74 0,0 0,1.25 4.25l1.37,-1.37A4.9,4.9 0,0 1,9 15L9,7A5,5 0,0 1,19 7L19,8l5.5,-5.5ZM19,15L19,12.24l-7.22,7.22A5,5 0,0 0,19 15ZM14,22a6.62,6.62 0,0 1,-3.65 -1.11L9.28,22a8.09,8.09 0,0 0,4 1.5L13.28,28h1.5L14.78,23.46a8.82,8.82 0,0 0,7.75 -9L21,14.46A7.27,7.27 0,0 1,14 22Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.23,4.16a1.23,1.23 0,0 0,-1.36 0.27L11,6.29L11,4.75A1.76,1.76 0,0 0,9.25 3L2.75,3A1.76,1.76 0,0 0,1 4.75v6.5A1.76,1.76 0,0 0,2.75 13h6.5A1.76,1.76 0,0 0,11 11.25L11,9.71l1.87,1.86a1.23,1.23 0,0 0,0.88 0.37,1.18 1.18,0 0,0 0.48,-0.1A1.23,1.23 0,0 0,15 10.69L15,5.31A1.23,1.23 0,0 0,14.23 4.16ZM10,11.25a0.76,0.76 0,0 1,-0.75 0.75L2.75,12A0.76,0.76 0,0 1,2 11.25L2,4.75A0.76,0.76 0,0 1,2.75 4h6.5a0.76,0.76 0,0 1,0.75 0.75ZM14,10.69a0.25,0.25 0,0 1,-0.15 0.23,0.26 0.26,0 0,1 -0.28,-0.05L11,8.29L11,7.71l2.57,-2.58a0.26,0.26 0,0 1,0.28 0,0.25 0.25,0 0,1 0.15,0.23Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:fillColor="@color/core_white"
|
||||
android:pathData="M22,12l3.29,-3.29a1,1 0,0 1,1.71 0.7v9.18a1,1 0,0 1,-1.71 0.7L22,16ZM24.5,2.5L19.85,7.15A3,3 0,0 0,17.5 6L6,6A3,3 0,0 0,3 9L3,19a3,3 0,0 0,2.14 2.86L2.5,24.5l1.06,1.06 22,-22ZM9.24,22L17.5,22a3,3 0,0 0,3 -3L20.5,10.74Z"/>
|
||||
</vector>
|
|
@ -38,4 +38,15 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/call_participant_mic_muted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:srcCompat="@drawable/ic_mic_off_solid_18"
|
||||
app:tint="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
</org.thoughtcrime.securesms.components.webrtc.CallParticipantView>
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/recipient_view_name"
|
||||
style="@style/TextAppearance.Signal.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
|
@ -31,4 +32,19 @@
|
|||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/call_participant_video_muted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_video_off_solid_white_18"
|
||||
app:tint="@color/core_white"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/call_participant_audio_muted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:srcCompat="@drawable/ic_mic_off_solid_18"
|
||||
app:tint="@color/core_white"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:minWidth="48dp"
|
||||
android:orientation="horizontal"
|
||||
tools:background="@color/core_ultramarine">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_group_solid_24"
|
||||
app:tint="@color/core_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/show_participants_menu_counter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/core_white"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="0" />
|
||||
|
||||
</LinearLayout>
|
|
@ -5,9 +5,8 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/menu_group_call_participants_list"
|
||||
android:icon="@drawable/ic_group_24"
|
||||
android:title="@string/WebRtcCallView__view_participants_list"
|
||||
app:iconTint="@color/white"
|
||||
app:actionLayout="@layout/show_participants_menu_view"
|
||||
app:showAsAction="always"
|
||||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
|
|
|
@ -1199,6 +1199,20 @@
|
|||
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified">You marked your safety number with %s unverified</string>
|
||||
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device">You marked your safety number with %s unverified from another device</string>
|
||||
|
||||
<!-- Group Calling update messages -->
|
||||
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
|
||||
<string name="MessageRecord_s_is_in_the_group_call_s">%1$s is in the group call · %2$s</string>
|
||||
<string name="MessageRecord_you_are_in_the_group_call_s">You are in the group call · %2$s</string>
|
||||
<string name="MessageRecord_s_and_s_are_in_the_group_call_s">%1$s and %2$s are in the group call · %3$s</string>
|
||||
<string name="MessageRecord_s_s_and_s_are_in_the_group_call_s">%1$s, %2$s, and %3$s are in the group call · %4$s</string>
|
||||
<string name="MessageRecord_group_call_s">Group call · %1$s</string>
|
||||
<string name="MessageRecord_you">You</string>
|
||||
|
||||
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call_s">
|
||||
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call · %4$s</item>
|
||||
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item>
|
||||
</plurals>
|
||||
|
||||
<!-- MessageRequestBottomView -->
|
||||
<string name="MessageRequestBottomView_accept">Accept</string>
|
||||
<string name="MessageRequestBottomView_continue">Continue</string>
|
||||
|
@ -1884,6 +1898,8 @@
|
|||
<!-- ConversationUpdateItem -->
|
||||
<string name="ConversationUpdateItem_loading">Loading</string>
|
||||
<string name="ConversationUpdateItem_learn_more">Learn more</string>
|
||||
<string name="ConversationUpdateItem_join_call">Join call</string>
|
||||
<string name="ConversationUpdateItem_return_to_call">Return to call</string>
|
||||
|
||||
<!-- audio_view -->
|
||||
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
|
||||
|
|
|
@ -438,8 +438,8 @@ dependencyVerification {
|
|||
['org.signal:argon2:13.1',
|
||||
'0f686ccff0d4842bfcc74d92e8dc780a5f159b9376e37a1189fabbcdac458bef'],
|
||||
|
||||
['org.signal:ringrtc-android:2.8.0',
|
||||
'49965467ed1e8634969af10f318bbaf1dd97b1d55bca05300802a5dcb6ed21a3'],
|
||||
['org.signal:ringrtc-android:2.8.3',
|
||||
'1cdc73ec34b11b9eeb0a650715e1095cade226736192c091991f31367245e37a'],
|
||||
|
||||
['org.signal:zkgroup-android:0.7.0',
|
||||
'52b172565bd01526e93ebf1796b834bdc449d4fe3422c1b827e49cb8d4f13fbd'],
|
||||
|
|
|
@ -713,6 +713,10 @@ public class SignalServiceMessageSender {
|
|||
builder.setDelete(delete);
|
||||
}
|
||||
|
||||
if (message.getGroupCallUpdate().isPresent()) {
|
||||
builder.setGroupCallUpdate(DataMessage.GroupCallUpdate.newBuilder().setEraId(message.getGroupCallUpdate().get().getEraId()));
|
||||
}
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
return enforceMaxContentSize(container.setDataMessage(builder).build().toByteArray());
|
||||
|
|
|
@ -342,18 +342,19 @@ public final class SignalServiceContent {
|
|||
}
|
||||
|
||||
|
||||
List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0);
|
||||
boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
|
||||
boolean profileKeyUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0);
|
||||
boolean isGroupV2 = groupInfoV2 != null;
|
||||
SignalServiceDataMessage.Quote quote = createQuote(content, isGroupV2);
|
||||
List<SharedContact> sharedContacts = createSharedContacts(content);
|
||||
List<SignalServiceDataMessage.Preview> previews = createPreviews(content);
|
||||
List<SignalServiceDataMessage.Mention> mentions = createMentions(content.getBodyRangesList(), content.getBody(), isGroupV2);
|
||||
SignalServiceDataMessage.Sticker sticker = createSticker(content);
|
||||
SignalServiceDataMessage.Reaction reaction = createReaction(content);
|
||||
SignalServiceDataMessage.RemoteDelete remoteDelete = createRemoteDelete(content);
|
||||
List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0);
|
||||
boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
|
||||
boolean profileKeyUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0);
|
||||
boolean isGroupV2 = groupInfoV2 != null;
|
||||
SignalServiceDataMessage.Quote quote = createQuote(content, isGroupV2);
|
||||
List<SharedContact> sharedContacts = createSharedContacts(content);
|
||||
List<SignalServiceDataMessage.Preview> previews = createPreviews(content);
|
||||
List<SignalServiceDataMessage.Mention> mentions = createMentions(content.getBodyRangesList(), content.getBody(), isGroupV2);
|
||||
SignalServiceDataMessage.Sticker sticker = createSticker(content);
|
||||
SignalServiceDataMessage.Reaction reaction = createReaction(content);
|
||||
SignalServiceDataMessage.RemoteDelete remoteDelete = createRemoteDelete(content);
|
||||
SignalServiceDataMessage.GroupCallUpdate groupCallUpdate = createGroupCallUpdate(content);
|
||||
|
||||
if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE) {
|
||||
throw new UnsupportedDataMessageProtocolVersionException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE,
|
||||
|
@ -389,7 +390,8 @@ public final class SignalServiceContent {
|
|||
sticker,
|
||||
content.getIsViewOnce(),
|
||||
reaction,
|
||||
remoteDelete);
|
||||
remoteDelete,
|
||||
groupCallUpdate);
|
||||
}
|
||||
|
||||
private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata,
|
||||
|
@ -787,6 +789,16 @@ public final class SignalServiceContent {
|
|||
return new SignalServiceDataMessage.RemoteDelete(delete.getTargetSentTimestamp());
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.GroupCallUpdate createGroupCallUpdate(SignalServiceProtos.DataMessage content) {
|
||||
if (!content.hasGroupCallUpdate()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SignalServiceProtos.DataMessage.GroupCallUpdate groupCallUpdate = content.getGroupCallUpdate();
|
||||
|
||||
return new SignalServiceDataMessage.GroupCallUpdate(groupCallUpdate.getEraId());
|
||||
}
|
||||
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getContactCount() <= 0) return null;
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ public class SignalServiceDataMessage {
|
|||
private final boolean viewOnce;
|
||||
private final Optional<Reaction> reaction;
|
||||
private final Optional<RemoteDelete> remoteDelete;
|
||||
private final Optional<GroupCallUpdate> groupCallUpdate;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceDataMessage.
|
||||
|
@ -56,7 +57,8 @@ public class SignalServiceDataMessage {
|
|||
String body, boolean endSession, int expiresInSeconds,
|
||||
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
List<Mention> mentions, Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete)
|
||||
List<Mention> mentions, Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete,
|
||||
GroupCallUpdate groupCallUpdate)
|
||||
{
|
||||
try {
|
||||
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
|
||||
|
@ -64,18 +66,19 @@ public class SignalServiceDataMessage {
|
|||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
this.timestamp = timestamp;
|
||||
this.body = OptionalUtil.absentIfEmpty(body);
|
||||
this.endSession = endSession;
|
||||
this.expiresInSeconds = expiresInSeconds;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.profileKey = Optional.fromNullable(profileKey);
|
||||
this.profileKeyUpdate = profileKeyUpdate;
|
||||
this.quote = Optional.fromNullable(quote);
|
||||
this.sticker = Optional.fromNullable(sticker);
|
||||
this.viewOnce = viewOnce;
|
||||
this.reaction = Optional.fromNullable(reaction);
|
||||
this.remoteDelete = Optional.fromNullable(remoteDelete);
|
||||
this.timestamp = timestamp;
|
||||
this.body = OptionalUtil.absentIfEmpty(body);
|
||||
this.endSession = endSession;
|
||||
this.expiresInSeconds = expiresInSeconds;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.profileKey = Optional.fromNullable(profileKey);
|
||||
this.profileKeyUpdate = profileKeyUpdate;
|
||||
this.quote = Optional.fromNullable(quote);
|
||||
this.sticker = Optional.fromNullable(sticker);
|
||||
this.viewOnce = viewOnce;
|
||||
this.reaction = Optional.fromNullable(reaction);
|
||||
this.remoteDelete = Optional.fromNullable(remoteDelete);
|
||||
this.groupCallUpdate = Optional.fromNullable(groupCallUpdate);
|
||||
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
this.attachments = Optional.of(attachments);
|
||||
|
@ -220,6 +223,10 @@ public class SignalServiceDataMessage {
|
|||
return remoteDelete;
|
||||
}
|
||||
|
||||
public Optional<GroupCallUpdate> getGroupCallUpdate() {
|
||||
return groupCallUpdate;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
|
@ -241,6 +248,7 @@ public class SignalServiceDataMessage {
|
|||
private boolean viewOnce;
|
||||
private Reaction reaction;
|
||||
private RemoteDelete remoteDelete;
|
||||
private GroupCallUpdate groupCallUpdate;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
|
@ -358,12 +366,18 @@ public class SignalServiceDataMessage {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withGroupCallUpdate(GroupCallUpdate groupCallUpdate) {
|
||||
this.groupCallUpdate = groupCallUpdate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage build() {
|
||||
if (timestamp == 0) timestamp = System.currentTimeMillis();
|
||||
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
|
||||
expiresInSeconds, expirationUpdate, profileKey,
|
||||
profileKeyUpdate, quote, sharedContacts, previews,
|
||||
mentions, sticker, viewOnce, reaction, remoteDelete);
|
||||
mentions, sticker, viewOnce, reaction, remoteDelete,
|
||||
groupCallUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -564,4 +578,16 @@ public class SignalServiceDataMessage {
|
|||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupCallUpdate {
|
||||
private final String eraId;
|
||||
|
||||
public GroupCallUpdate(String eraId) {
|
||||
this.eraId = eraId;
|
||||
}
|
||||
|
||||
public String getEraId() {
|
||||
return eraId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -215,6 +215,8 @@ public class PushServiceSocket {
|
|||
|
||||
private static final long CDN2_RESUMABLE_LINK_LIFETIME_MILLIS = TimeUnit.DAYS.toMillis(7);
|
||||
|
||||
private static final int MAX_FOLLOW_UPS = 20;
|
||||
|
||||
private long soTimeoutMillis = TimeUnit.SECONDS.toMillis(30);
|
||||
private final Set<Call> connections = new HashSet<>();
|
||||
|
||||
|
@ -1672,7 +1674,7 @@ public class PushServiceSocket {
|
|||
ConnectionHolder connectionHolder = getRandom(serviceClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.followRedirects(true)
|
||||
.followRedirects(false)
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
@ -1688,22 +1690,36 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(builder.build());
|
||||
Request request = builder.build();
|
||||
|
||||
try {
|
||||
Response response = call.execute();
|
||||
int responseStatus = response.code();
|
||||
byte[] responseBody = response.body() != null ? response.body().bytes() : new byte[0];
|
||||
for (int i = 0; i < MAX_FOLLOW_UPS; i++) {
|
||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||
int responseStatus = response.code();
|
||||
|
||||
return new CallingResponse.Success(requestId, responseStatus, responseBody);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Exception during ringrtc http call.", e);
|
||||
return new CallingResponse.Error(requestId, e);
|
||||
if (responseStatus != 307) {
|
||||
return new CallingResponse.Success(requestId,
|
||||
responseStatus,
|
||||
response.body() != null ? response.body().bytes() : new byte[0]);
|
||||
}
|
||||
|
||||
String location = response.header("Location");
|
||||
HttpUrl newUrl = location != null ? request.url().resolve(location) : null;
|
||||
|
||||
if (newUrl != null) {
|
||||
request = request.newBuilder().url(newUrl).build();
|
||||
} else {
|
||||
return new CallingResponse.Error(requestId, new IOException("Received redirect without a valid Location header"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Exception during ringrtc http call.", e);
|
||||
return new CallingResponse.Error(requestId, e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "Calling request max redirects exceeded");
|
||||
return new CallingResponse.Error(requestId, new IOException("Redirect limit exceeded"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private ServiceConnectionHolder[] createServiceConnectionHolders(SignalUrl[] urls,
|
||||
List<Interceptor> interceptors,
|
||||
Optional<Dns> dns)
|
||||
|
|
|
@ -234,6 +234,10 @@ message DataMessage {
|
|||
optional uint64 targetSentTimestamp = 1;
|
||||
}
|
||||
|
||||
message GroupCallUpdate {
|
||||
optional string eraId = 1;
|
||||
}
|
||||
|
||||
enum ProtocolVersion {
|
||||
option allow_alias = true;
|
||||
|
||||
|
@ -264,6 +268,7 @@ message DataMessage {
|
|||
optional Reaction reaction = 16;
|
||||
optional Delete delete = 17;
|
||||
repeated BodyRange bodyRanges = 18;
|
||||
optional GroupCallUpdate groupCallUpdate = 19;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
|
Ładowanie…
Reference in New Issue