package org.thoughtcrime.securesms.database.model; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.google.protobuf.ByteString; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval; import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.StringUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.UUID; final class GroupsV2UpdateMessageProducer { @NonNull private final Context context; @NonNull private final DescribeMemberStrategy descriptionStrategy; @NonNull private final UUID selfUuid; @NonNull private final ByteString selfUuidBytes; /** * @param descriptionStrategy Strategy for member description. */ GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull DescribeMemberStrategy descriptionStrategy, @NonNull UUID selfUuid) { this.context = context; this.descriptionStrategy = descriptionStrategy; this.selfUuid = selfUuid; this.selfUuidBytes = UuidUtil.toByteString(selfUuid); } /** * Describes a group that is new to you, use this when there is no available change record. *

* Invitation and revision 0 groups are the most common use cases for this. *

* When invited, it's possible there's no change available. *

* When the revision of the group is 0, the change is very noisy and only the editor is useful. */ UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) { Optional selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid); if (selfPending.isPresent()) { return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy)); } ByteString foundingMemberUuid = decryptedGroupChange.getEditor(); if (!foundingMemberUuid.isEmpty()) { if (selfUuidBytes.equals(foundingMemberUuid)) { return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group)); } else { return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator)); } } if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) { return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)); } else { return updateDescription(context.getString(R.string.MessageRecord_group_updated)); } } List describeChanges(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) { if (DecryptedGroup.getDefaultInstance().equals(previousGroupState)) { previousGroupState = null; } List updates = new LinkedList<>(); if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) { describeUnknownEditorMemberAdditions(change, updates); describeUnknownEditorModifyMemberRoles(change, updates); describeUnknownEditorInvitations(change, updates); describeUnknownEditorRevokedInvitations(change, updates); describeUnknownEditorPromotePending(change, updates); describeUnknownEditorNewTitle(change, updates); describeUnknownEditorNewAvatar(change, updates); describeUnknownEditorNewTimer(change, updates); describeUnknownEditorNewAttributeAccess(change, updates); describeUnknownEditorNewMembershipAccess(change, updates); describeUnknownEditorNewGroupInviteLinkAccess(previousGroupState, change, updates); describeRequestingMembers(change, updates); describeUnknownEditorRequestingMembersApprovals(change, updates); describeUnknownEditorRequestingMembersDeletes(change, updates); describeUnknownEditorMemberRemovals(change, updates); if (updates.isEmpty()) { describeUnknownEditorUnknownChange(updates); } } else { describeMemberAdditions(change, updates); describeModifyMemberRoles(change, updates); describeInvitations(change, updates); describeRevokedInvitations(change, updates); describePromotePending(change, updates); describeNewTitle(change, updates); describeNewAvatar(change, updates); describeNewTimer(change, updates); describeNewAttributeAccess(change, updates); describeNewMembershipAccess(change, updates); describeNewGroupInviteLinkAccess(previousGroupState, change, updates); describeRequestingMembers(change, updates); describeRequestingMembersApprovals(change, updates); describeRequestingMembersDeletes(change, updates); describeMemberRemovals(change, updates); if (updates.isEmpty()) { describeUnknownChange(change, updates); } } return updates; } /** * Handles case of future protocol versions where we don't know what has changed. */ private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group))); } else { updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor))); } } private void describeUnknownEditorUnknownChange(@NonNull List updates) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated))); } private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); for (DecryptedMember member : change.getNewMembersList()) { boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes); if (editorIsYou) { if (newMemberIsYou) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link))); } else { updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added))); } } else { if (newMemberIsYou) { updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor))); } else { if (member.getUuid().equals(change.getEditor())) { updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember))); } else { updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember))); } } } } } private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedMember member : change.getNewMembersList()) { boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes); if (newMemberIsYou) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group))); } else { updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember))); } } } private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); for (ByteString member : change.getDeleteMembersList()) { boolean removedMemberIsYou = member.equals(selfUuidBytes); if (editorIsYou) { if (removedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group))); } else { updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember))); } } else { if (removedMemberIsYou) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor))); } else { if (member.equals(change.getEditor())) { updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember))); } else { updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember))); } } } } } private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (ByteString member : change.getDeleteMembersList()) { boolean removedMemberIsYou = member.equals(selfUuidBytes); if (removedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group))); } else { updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember))); } } } private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) { boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes); if (roleChange.getRole() == Member.Role.ADMINISTRATOR) { if (editorIsYou) { updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin))); } else { if (changedMemberIsYou) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor))); } else { updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin))); } } } else { if (editorIsYou) { updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin))); } else { if (changedMemberIsYou) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor))); } else { updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin))); } } } } } private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) { boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes); if (roleChange.getRole() == Member.Role.ADMINISTRATOR) { if (changedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin))); } else { updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin))); } } else { if (changedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin))); } else { updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin))); } } } } private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); int notYouInviteCount = 0; for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) { boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes); if (newMemberIsYou) { updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor))); } else { if (editorIsYou) { updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee))); } else { notYouInviteCount++; } } } if (notYouInviteCount > 0) { final int notYouInviteCountFinalCopy = notYouInviteCount; updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy))); } } private void describeUnknownEditorInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { int notYouInviteCount = 0; for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) { boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes); if (newMemberIsYou) { UUID uuid = UuidUtil.fromByteStringOrUnknown(invitee.getAddedByUuid()); if (UuidUtil.UNKNOWN_UUID.equals(uuid)) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group))); } else { updates.add(0, updateDescription(invitee.getAddedByUuid(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor))); } } else { notYouInviteCount++; } } if (notYouInviteCount > 0) { updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount))); } } private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); int notDeclineCount = 0; for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) { boolean decline = invitee.getUuid().equals(change.getEditor()); if (decline) { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group))); } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group))); } } else if (invitee.getUuid().equals(selfUuidBytes)) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, editor))); } else { notDeclineCount++; } } if (notDeclineCount > 0) { if (editorIsYou) { updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount))); } else { final int notDeclineCountFinalCopy = notDeclineCount; updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy))); } } } private void describeUnknownEditorRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { int notDeclineCount = 0; for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) { boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes); if (inviteeWasYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group))); } else { notDeclineCount++; } } if (notDeclineCount > 0) { updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount))); } } private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); for (DecryptedMember newMember : change.getPromotePendingMembersList()) { ByteString uuid = newMember.getUuid(); boolean newMemberIsYou = uuid.equals(selfUuidBytes); if (editorIsYou) { if (newMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite))); } else { updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember))); } } else { if (newMemberIsYou) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor))); } else { if (uuid.equals(change.getEditor())) { updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember))); } else { updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember))); } } } } } private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedMember newMember : change.getPromotePendingMembersList()) { ByteString uuid = newMember.getUuid(); boolean newMemberIsYou = uuid.equals(selfUuidBytes); if (newMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group))); } else { updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName))); } } } private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (change.hasNewTitle()) { String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue()); if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle))); } } } private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.hasNewTitle()) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue())))); } } private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (change.hasNewAvatar()) { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor))); } } } private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.hasNewAvatar()) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed))); } } private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (change.hasNewTimer()) { String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration()); if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time))); } } } private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.hasNewTimer()) { String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration()); updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time))); } } private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) { String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess()); if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel))); } } } private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) { String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess()); updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel))); } } private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) { String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess()); if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel))); } } } private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) { String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess()); updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel))); } } private void describeNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change, @NonNull List updates) { AccessControl.AccessRequired previousAccessControl = null; if (previousGroupState != null) { previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink(); } boolean editorIsYou = change.getEditor().equals(selfUuidBytes); boolean groupLinkEnabled = false; switch (change.getNewInviteLinkAccess()) { case ANY: groupLinkEnabled = true; if (editorIsYou) { if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_admin_approval_for_the_group_link))); } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off))); } } else { if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, editor))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor))); } } break; case ADMINISTRATOR: groupLinkEnabled = true; if (editorIsYou) { if (previousAccessControl == AccessControl.AccessRequired.ANY) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_admin_approval_for_the_group_link))); } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on))); } } else { if (previousAccessControl == AccessControl.AccessRequired.ANY) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, editor))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor))); } } break; case UNSATISFIABLE: if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor))); } break; } if (!groupLinkEnabled && change.getNewInviteLinkPassword().size() > 0) { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link))); } else { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor))); } } } private void describeUnknownEditorNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change, @NonNull List updates) { AccessControl.AccessRequired previousAccessControl = null; if (previousGroupState != null) { previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink(); } switch (change.getNewInviteLinkAccess()) { case ANY: if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_off))); } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off))); } break; case ADMINISTRATOR: if (previousAccessControl == AccessControl.AccessRequired.ANY) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_on))); } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on))); } break; case UNSATISFIABLE: updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off))); break; } if (change.getNewInviteLinkPassword().size() > 0) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset))); } } private void describeRequestingMembers(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) { boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes); if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group))); } else { updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting))); } } } private void describeRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) { boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes); if (requestingMemberIsYou) { updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor))); } else { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (editorIsYou) { updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requesting))); } else { updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting))); } } } } private void describeUnknownEditorRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) { boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes); if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved))); } else { updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting))); } } } private void describeRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); for (ByteString requestingMember : change.getDeleteRequestingMembersList()) { boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes); if (requestingMemberIsYou) { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group))); } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin))); } } else { boolean editorIsCanceledMember = change.getEditor().equals(requestingMember); if (editorIsCanceledMember) { updates.add(updateDescription(requestingMember, editorRequesting -> context.getString(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, editorRequesting))); } else { updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting))); } } } } private void describeUnknownEditorRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (ByteString requestingMember : change.getDeleteRequestingMembersList()) { boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes); if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin))); } else { updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting))); } } } interface DescribeMemberStrategy { /** * Map a UUID to a string that describes the group member. */ @NonNull @WorkerThread String describe(@NonNull UUID uuid); } private interface StringFactory1Arg { String create(String arg1); } private interface StringFactory2Args { String create(String arg1, String arg2); } private static UpdateDescription updateDescription(@NonNull String string) { return UpdateDescription.staticDescription(string); } private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull StringFactory1Arg stringFactory) { UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes); return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1))); } private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull ByteString uuid2Bytes, @NonNull StringFactory2Args stringFactory) { UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes); UUID uuid2 = UuidUtil.fromByteStringOrUnknown(uuid2Bytes); return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2))); } }