diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 48f333e37..e9854fc04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ShortcutManager; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; @@ -82,6 +83,7 @@ import com.annimon.stream.Stream; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; +import com.google.android.material.button.MaterialButton; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -135,6 +137,7 @@ import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; +import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel; import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel; import org.thoughtcrime.securesms.conversationlist.model.MessageResult; import org.thoughtcrime.securesms.crypto.SecurityEvent; @@ -159,6 +162,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.events.GroupCallPeekEvent; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.groups.GroupChangeException; @@ -369,6 +373,7 @@ public class ConversationActivity extends PassphraseRequiredActivity private View requestingMemberBanner; private View cancelJoinRequest; private Stub mentionsSuggestions; + private MaterialButton joinGroupCallButton; private LinkPreviewViewModel linkPreviewViewModel; private ConversationSearchViewModel searchViewModel; @@ -377,6 +382,7 @@ public class ConversationActivity extends PassphraseRequiredActivity private InviteReminderModel inviteReminderModel; private ConversationGroupViewModel groupViewModel; private MentionsPickerViewModel mentionsViewModel; + private GroupCallViewModel groupCallViewModel; private LiveRecipient recipient; private long threadId; @@ -426,6 +432,7 @@ public class ConversationActivity extends PassphraseRequiredActivity initializeViewModel(args); initializeGroupViewModel(); initializeMentionsViewModel(); + initializeGroupCallViewModel(); initializeEnabledCheck(); initializePendingRequestsBanner(); initializeGroupV1MigrationsBanners(); @@ -532,6 +539,10 @@ public class ConversationActivity extends PassphraseRequiredActivity .enqueue(); } + if (groupCallViewModel != null) { + groupCallViewModel.peekGroupCall(this); + } + setVisibleThread(threadId); ConversationUtil.refreshRecipientShortcuts(); } @@ -816,6 +827,9 @@ public class ConversationActivity extends PassphraseRequiredActivity } else if (isGroupConversation()) { if (isActiveV2Group && FeatureFlags.groupCalling()) { inflater.inflate(R.menu.conversation_callable_groupv2, menu); + if (groupCallViewModel != null && Boolean.TRUE.equals(groupCallViewModel.hasActiveGroupCall().getValue())) { + hideMenuItem(menu, R.id.menu_video_secure); + } } inflater.inflate(R.menu.conversation_group_options, menu); @@ -1895,6 +1909,7 @@ public class ConversationActivity extends PassphraseRequiredActivity noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner); requestingMemberBanner = findViewById(R.id.conversation_requesting_banner); cancelJoinRequest = findViewById(R.id.conversation_cancel_request); + joinGroupCallButton = findViewById(R.id.conversation_group_cal_join); container.addOnKeyboardShownListener(this); inputPanel.setListener(this); @@ -1949,6 +1964,8 @@ public class ConversationActivity extends PassphraseRequiredActivity inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment()); reactionOverlay.setOnReactionSelectedListener(this); + + joinGroupCallButton.setOnClickListener(v -> handleVideo(getRecipient())); } protected void initializeActionBar() { @@ -2105,6 +2122,21 @@ public class ConversationActivity extends PassphraseRequiredActivity }); } + public void initializeGroupCallViewModel() { + groupCallViewModel = ViewModelProviders.of(this, new GroupCallViewModel.Factory()).get(GroupCallViewModel.class); + + recipient.observe(this, r -> { + groupCallViewModel.onRecipientChange(this, r); + }); + + groupCallViewModel.hasActiveGroupCall().observe(this, hasActiveCall -> { + invalidateOptionsMenu(); + joinGroupCallButton.setVisibility(hasActiveCall ? View.VISIBLE : View.GONE); + }); + + groupCallViewModel.canJoinGroupCall().observe(this, canJoin -> joinGroupCallButton.setText(canJoin ? R.string.ConversationActivity_join : R.string.ConversationActivity_full)); + } + private void showStickerIntroductionTooltip() { TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER); inputPanel.setMediaKeyboardToggleMode(true); @@ -2218,6 +2250,10 @@ public class ConversationActivity extends PassphraseRequiredActivity if (mentionsViewModel != null) { mentionsViewModel.onRecipientChange(recipient); } + + if (groupCallViewModel != null) { + groupCallViewModel.onRecipientChange(this, recipient); + } } @Subscribe(threadMode = ThreadMode.MAIN) @@ -2239,6 +2275,13 @@ public class ConversationActivity extends PassphraseRequiredActivity } } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent event) { + if (groupCallViewModel != null) { + groupCallViewModel.onGroupCallPeekEvent(event); + } + } + private void initializeReceivers() { securityUpdateReceiver = new BroadcastReceiver() { @Override @@ -2396,8 +2439,13 @@ public class ConversationActivity extends PassphraseRequiredActivity private void setActionBarColor(MaterialColor color) { ActionBar supportActionBar = getSupportActionBar(); if (supportActionBar == null) throw new AssertionError(); - supportActionBar.setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this))); + int actionBarColor = color.toActionBarColor(this); + supportActionBar.setBackgroundDrawable(new ColorDrawable(actionBarColor)); WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this)); + + joinGroupCallButton.setTextColor(actionBarColor); + joinGroupCallButton.setIconTint(ColorStateList.valueOf(actionBarColor)); + joinGroupCallButton.setRippleColor(ColorStateList.valueOf(actionBarColor)); } private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 272a6dedd..0caa60fbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.VerifyIdentityActivity; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; +import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil; import org.thoughtcrime.securesms.database.model.LiveUpdateMessage; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.UpdateDescription; @@ -182,13 +183,16 @@ public final class ConversationUpdateItem extends LinearLayout } }); } else if (conversationMessage.getMessageRecord().isGroupCall()) { - UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true); Collection 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 (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) { + text = R.string.ConversationUpdateItem_call_is_full; + } else { + text = uuids.contains(TextSecurePreferences.getLocalUuid(getContext())) ? R.string.ConversationUpdateItem_return_to_call : R.string.ConversationUpdateItem_join_call; + } } if (text != 0) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/groupcall/GroupCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/groupcall/GroupCallViewModel.java new file mode 100644 index 000000000..073f9f5a7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/groupcall/GroupCallViewModel.java @@ -0,0 +1,90 @@ +package org.thoughtcrime.securesms.conversation.ui.groupcall; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.thoughtcrime.securesms.events.GroupCallPeekEvent; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.ringrtc.RemotePeer; +import org.thoughtcrime.securesms.service.WebRtcCallService; +import org.thoughtcrime.securesms.util.FeatureFlags; + +import java.util.Objects; + +public class GroupCallViewModel extends ViewModel { + + private static final String TAG = Log.tag(GroupCallViewModel.class); + + private final MutableLiveData activeGroupCall; + private final MutableLiveData canJoin; + + private @Nullable Recipient currentRecipient; + + GroupCallViewModel() { + this.activeGroupCall = new MutableLiveData<>(false); + this.canJoin = new MutableLiveData<>(false); + } + + public @NonNull LiveData hasActiveGroupCall() { + return activeGroupCall; + } + + public @NonNull LiveData canJoinGroupCall() { + return canJoin; + } + + public void onRecipientChange(@NonNull Context context, @Nullable Recipient recipient) { + if (Objects.equals(currentRecipient, recipient)) { + return; + } + + activeGroupCall.postValue(false); + canJoin.postValue(false); + + currentRecipient = recipient; + + peekGroupCall(context); + } + + public void peekGroupCall(@NonNull Context context) { + if (isGroupCallCapable(currentRecipient)) { + Log.i(TAG, "peek call for " + currentRecipient.getId()); + Intent intent = new Intent(context, WebRtcCallService.class); + intent.setAction(WebRtcCallService.ACTION_GROUP_CALL_PEEK) + .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(currentRecipient.getId())); + + context.startService(intent); + } + } + + public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent groupCallPeekEvent) { + if (isGroupCallCapable(currentRecipient) && groupCallPeekEvent.getGroupRecipientId().equals(currentRecipient.getId())) { + Log.i(TAG, "update UI with call event: active call: " + groupCallPeekEvent.hasActiveCall() + " canJoin: " + groupCallPeekEvent.canJoinCall()); + + activeGroupCall.postValue(groupCallPeekEvent.hasActiveCall()); + canJoin.postValue(groupCallPeekEvent.canJoinCall()); + } else { + Log.i(TAG, "Ignore call event for different recipient."); + } + } + + private static boolean isGroupCallCapable(@Nullable Recipient recipient) { + return recipient != null && recipient.isActiveGroup() && recipient.isPushV2Group() && FeatureFlags.groupCalling(); + } + + public static final class Factory implements ViewModelProvider.Factory { + @Override + public @NonNull T create(@NonNull Class modelClass) { + //noinspection ConstantConditions + return modelClass.cast(new GroupCallViewModel()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 10432d62c..f40e27196 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -133,12 +133,14 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract @NonNull Pair insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer); public abstract @NonNull Pair insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer); public abstract @NonNull Pair 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 peekJoinedUuids); + public abstract void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, + @NonNull RecipientId sender, + long timestamp, + @Nullable String messageGroupCallEraId, + @Nullable String peekGroupCallEraId, + @NonNull Collection peekJoinedUuids, + boolean isCallFull); + public abstract boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull); public abstract Optional insertMessageInbox(IncomingTextMessage message, long type); public abstract Optional insertMessageInbox(IncomingTextMessage message); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 3fbb0d71a..a97605dcb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -464,16 +464,22 @@ public class MmsDatabase extends MessageDatabase { } @Override - public @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String messageGroupCallEraId, - @Nullable String peekGroupCallEraId, - @NonNull Collection peekJoinedUuids) + public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, + @NonNull RecipientId sender, + long timestamp, + @Nullable String messageGroupCallEraId, + @Nullable String peekGroupCallEraId, + @NonNull Collection peekJoinedUuids, + boolean isCallFull) { throw new UnsupportedOperationException(); } + @Override + public boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull) { + throw new UnsupportedOperationException(); + } + @Override public Optional insertMessageInbox(IncomingTextMessage message, long type) { throw new UnsupportedOperationException(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index c0656302d..4f02701aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -691,7 +691,8 @@ public class SmsDatabase extends MessageDatabase { long timestamp, @Nullable String messageGroupCallEraId, @Nullable String peekGroupCallEraId, - @NonNull Collection peekJoinedUuids) + @NonNull Collection peekJoinedUuids, + boolean isCallFull) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); @@ -701,7 +702,7 @@ public class SmsDatabase extends MessageDatabase { Recipient recipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); - boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids); + boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull); if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) { Recipient self = Recipient.self(); @@ -712,6 +713,7 @@ public class SmsDatabase extends MessageDatabase { .setStartedCallUuid(Recipient.resolved(sender).requireUuid().toString()) .setStartedCallTimestamp(timestamp) .addAllInCallUuids(Stream.of(peekJoinedUuids).map(UUID::toString).toList()) + .setIsCallFull(isCallFull) .build() .toByteArray(); @@ -743,7 +745,8 @@ public class SmsDatabase extends MessageDatabase { } } - private boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids) { + @Override + public boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); String where = TYPE + " = ? AND " + THREAD_ID + " = ?"; String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId); @@ -761,7 +764,7 @@ public class SmsDatabase extends MessageDatabase { List inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList() : Collections.emptyList(); - String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids); + String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull); ContentValues contentValues = new ContentValues(); contentValues.put(BODY, body); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java index 908f6e361..f248e0185 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java @@ -34,8 +34,9 @@ public final class GroupCallUpdateDetailsUtil { return groupCallUpdateDetails; } - public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List inCallUuids) { + public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List inCallUuids, boolean isCallFull) { GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.toBuilder() + .setIsCallFull(isCallFull) .clearInCallUuids(); if (Util.hasItems(inCallUuids)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallPeekEvent.java b/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallPeekEvent.java new file mode 100644 index 000000000..79e0f02f6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallPeekEvent.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.events; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.recipients.RecipientId; + +public final class GroupCallPeekEvent { + private final RecipientId groupRecipientId; + private final String eraId; + private final long deviceCount; + private final long deviceLimit; + + public GroupCallPeekEvent(@NonNull RecipientId groupRecipientId, @Nullable String eraId, @Nullable Long deviceCount, @Nullable Long deviceLimit) { + this.groupRecipientId = groupRecipientId; + this.eraId = eraId; + this.deviceCount = deviceCount != null ? deviceCount : 0; + this.deviceLimit = deviceLimit != null ? deviceLimit : 0; + } + + public @NonNull RecipientId getGroupRecipientId() { + return groupRecipientId; + } + + public boolean hasActiveCall() { + return eraId != null && deviceCount > 0; + } + + public boolean canJoinCall() { + return hasActiveCall() && deviceCount < deviceLimit; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index b6b9d5b99..72110b93e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -35,6 +35,7 @@ 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.GroupCallPeekEvent; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupManager; @@ -52,6 +53,7 @@ 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.WebRtcUtil; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.FutureTaskListener; import org.thoughtcrime.securesms.util.ListenableFutureTask; @@ -200,6 +202,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, 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 String ACTION_GROUP_CALL_PEEK = "GROUP_CALL_PEEK"; public static final int BUSY_TONE_LENGTH = 2000; @@ -686,25 +689,59 @@ public class WebRtcCallService extends Service implements CallManager.Observer, groupCallUpdateMetadata.getServerReceivedTimestamp(), groupCallUpdateMetadata.getGroupCallEraId(), peekInfo.getEraId(), - peekInfo.getJoinedMembers()); + peekInfo.getJoinedMembers(), + WebRtcUtil.isCallFull(peekInfo)); long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(group); ApplicationDependencies.getMessageNotifier().updateNotification(this, threadId, true); + + EventBus.getDefault().postSticky(new GroupCallPeekEvent(group.getId(), peekInfo.getEraId(), peekInfo.getDeviceCount(), peekInfo.getMaxDevices())); }); } catch (IOException | VerificationFailedException | CallException e) { - Log.e(TAG, "error peeking", e); + Log.e(TAG, "error peeking from message", e); } }); } - public void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection joinedMembers) { + public void peekGroupCall(@NonNull RecipientId id) { + networkExecutor.execute(() -> { + try { + Recipient group = Recipient.resolved(id); + GroupId.V2 groupId = group.requireGroupId().requireV2(); + GroupExternalCredential credential = GroupManager.getGroupExternalCredential(this, groupId); + + List 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 -> { + long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(group); + + DatabaseFactory.getSmsDatabase(this).updatePreviousGroupCall(threadId, + peekInfo.getEraId(), + peekInfo.getJoinedMembers(), + WebRtcUtil.isCallFull(peekInfo)); + + ApplicationDependencies.getMessageNotifier().updateNotification(this, threadId, true); + + EventBus.getDefault().postSticky(new GroupCallPeekEvent(id, peekInfo.getEraId(), peekInfo.getDeviceCount(), peekInfo.getMaxDevices())); + }); + + } catch (IOException | VerificationFailedException | CallException e) { + Log.e(TAG, "error peeking from active conversation", e); + } + }); + } + + public void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection joinedMembers, boolean isCallFull) { DatabaseFactory.getSmsDatabase(this).insertOrUpdateGroupCall(groupId, Recipient.self().getId(), System.currentTimeMillis(), null, groupCallEraId, - joinedMembers); + joinedMembers, + isCallFull); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java index 856fefe96..aa81b3a8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -125,7 +125,7 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { if (!members.contains(Recipient.self().requireUuid())) { members.add(Recipient.self().requireUuid()); } - webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members); + webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members, WebRtcUtil.isCallFull(peekInfo)); return currentState.builder() .changeCallSetupState() @@ -149,7 +149,7 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId); List members = Stream.of(currentState.getCallInfoState().getRemoteCallParticipants()).map(p -> p.getRecipient().requireUuid()).toList(); - webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members); + webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members, false); currentState = currentState.builder() .changeCallInfoState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 54ec61e31..31369d92f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -61,6 +61,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_ 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_PEEK; 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; @@ -238,6 +239,7 @@ public abstract class WebRtcActionProcessor { 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_GROUP_CALL_PEEK: return handleGroupCallPeek(currentState, getRemotePeer(intent)); case ACTION_HTTP_SUCCESS: return handleHttpSuccess(currentState, HttpData.fromIntent(intent)); case ACTION_HTTP_FAILURE: return handleHttpFailure(currentState, HttpData.fromIntent(intent)); @@ -727,6 +729,11 @@ public abstract class WebRtcActionProcessor { return currentState; } + protected @NonNull WebRtcServiceState handleGroupCallPeek(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { + webRtcInteractor.peekGroupCall(remotePeer.getId()); + return currentState; + } + //endregion protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java index 0764975b9..8ab6eebd2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java @@ -94,8 +94,8 @@ public class WebRtcInteractor { webRtcCallService.sendGroupCallMessage(recipient, groupCallEraId); } - void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection joinedMembers) { - webRtcCallService.updateGroupCallUpdateMessage(groupId, groupCallEraId, joinedMembers); + void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection joinedMembers, boolean isCallFull) { + webRtcCallService.updateGroupCallUpdateMessage(groupId, groupCallEraId, joinedMembers, isCallFull); } void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) { @@ -157,4 +157,8 @@ public class WebRtcInteractor { void peekGroupCall(@NonNull WebRtcData.GroupCallUpdateMetadata groupCallUpdateMetadata) { webRtcCallService.peekGroupCall(groupCallUpdateMetadata); } + + void peekGroupCall(@NonNull RecipientId recipientId) { + webRtcCallService.peekGroupCall(recipientId); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java index 5d9215f61..52d07a43b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java @@ -77,4 +77,8 @@ public final class WebRtcUtil { PeekInfo peekInfo = groupCall.getPeekInfo(); return peekInfo != null ? peekInfo.getEraId() : null; } + + public static boolean isCallFull(@Nullable PeekInfo peekInfo) { + return peekInfo != null && peekInfo.getMaxDevices() != null && peekInfo.getDeviceCount() >= peekInfo.getMaxDevices(); + } } diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 85d7d2e3c..8c191d532 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -75,4 +75,5 @@ message GroupCallUpdateDetails { string startedCallUuid = 2; int64 startedCallTimestamp = 3; repeated string inCallUuids = 4; + bool isCallFull = 5; } diff --git a/app/src/main/res/drawable/ic_video_solid_18.xml b/app/src/main/res/drawable/ic_video_solid_18.xml new file mode 100644 index 000000000..89aa5ebff --- /dev/null +++ b/app/src/main/res/drawable/ic_video_solid_18.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml index ad2b89519..9b1993bff 100644 --- a/app/src/main/res/layout/conversation_activity.xml +++ b/app/src/main/res/layout/conversation_activity.xml @@ -22,7 +22,33 @@ app:contentInsetStart="46dp" tools:background="#ff007f00"> - + + + + + + + diff --git a/app/src/main/res/layout/conversation_title_view.xml b/app/src/main/res/layout/conversation_title_view.xml index 600f198c9..3cfdf0457 100644 --- a/app/src/main/res/layout/conversation_title_view.xml +++ b/app/src/main/res/layout/conversation_title_view.xml @@ -7,7 +7,8 @@ android:layout_height="match_parent" android:layout_gravity="center_vertical" android:gravity="center_vertical" - tools:background="#007fff"> + tools:background="#007fff" + tools:layout_height="?actionBarSize"> More options now in \"Group settings\" + Join + Full + %d unread message @@ -1924,6 +1927,7 @@ Learn more Join call Return to call + Call is full Play … Pause