Add additional Group Calling features.

fork-5.53.8
Cody Henthorne 2020-11-20 15:42:46 -05:00 zatwierdzone przez GitHub
rodzic 8c1737e597
commit b90a74d26a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
61 zmienionych plików z 1193 dodań i 134 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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)}.
*/

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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" />

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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