From 38fa58c0a3f7c91715b62392a511fed081b8d112 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 6 Oct 2020 11:21:56 -0300 Subject: [PATCH] Write previous group state to the database for advanced change messages. --- .../model/GroupsV2UpdateMessageProducer.java | 69 ++++++++++++++++--- .../database/model/MessageRecord.java | 2 +- .../securesms/groups/GroupManagerV2.java | 33 +++++---- .../securesms/groups/GroupMutation.java | 31 +++++++++ .../securesms/groups/GroupProtoUtil.java | 13 ++-- .../v2/processing/GroupsV2StateProcessor.java | 15 ++-- app/src/main/proto/Database.proto | 7 +- app/src/main/res/values/strings.xml | 6 ++ .../GroupsV2UpdateMessageProducerTest.java | 62 ++++++++++++++++- 9 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/GroupMutation.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index 3f0de33ff..faa34e65d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -3,6 +3,7 @@ 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; @@ -81,7 +82,11 @@ final class GroupsV2UpdateMessageProducer { } } - List describeChanges(@NonNull DecryptedGroupChange change) { + 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()))) { @@ -96,7 +101,7 @@ final class GroupsV2UpdateMessageProducer { describeUnknownEditorNewTimer(change, updates); describeUnknownEditorNewAttributeAccess(change, updates); describeUnknownEditorNewMembershipAccess(change, updates); - describeUnknownEditorNewGroupInviteLinkAccess(change, updates); + describeUnknownEditorNewGroupInviteLinkAccess(previousGroupState, change, updates); describeRequestingMembers(change, updates); describeUnknownEditorRequestingMembersApprovals(change, updates); describeUnknownEditorRequestingMembersDeletes(change, updates); @@ -119,7 +124,7 @@ final class GroupsV2UpdateMessageProducer { describeNewTimer(change, updates); describeNewAttributeAccess(change, updates); describeNewMembershipAccess(change, updates); - describeNewGroupInviteLinkAccess(change, updates); + describeNewGroupInviteLinkAccess(previousGroupState, change, updates); describeRequestingMembers(change, updates); describeRequestingMembersApprovals(change, updates); describeRequestingMembersDeletes(change, updates); @@ -509,7 +514,16 @@ final class GroupsV2UpdateMessageProducer { } } - private void describeNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { + 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; @@ -517,17 +531,33 @@ final class GroupsV2UpdateMessageProducer { case ANY: groupLinkEnabled = true; if (editorIsYou) { - updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off))); + 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 { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor))); + 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) { - updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on))); + 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 { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor))); + 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: @@ -548,13 +578,30 @@ final class GroupsV2UpdateMessageProducer { } } - private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { + 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: - updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off))); + 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: - updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on))); + 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))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 503d217fa..833ab70fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -177,7 +177,7 @@ public abstract class MessageRecord extends DisplayRecord { GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get()); if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) { - return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange())); + return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange())); } else { return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 965a9f00d..8e4c44470 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -193,7 +193,7 @@ final class GroupManagerV2 { .setEditor(UuidUtil.toByteString(selfUuid)) .build(); - RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, groupChange, null); + RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null); return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, @@ -505,16 +505,18 @@ final class GroupManagerV2 { private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change) throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException { - final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId); - final GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties(); - final int nextRevision = v2GroupProperties.getGroupRevision() + 1; - final GroupChange.Actions changeActions = change.setRevision(nextRevision).build(); + final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId); + final GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties(); + final int nextRevision = v2GroupProperties.getGroupRevision() + 1; + final GroupChange.Actions changeActions = change.setRevision(nextRevision).build(); final DecryptedGroupChange decryptedChange; final DecryptedGroup decryptedGroupState; + final DecryptedGroup previousGroupState; try { + previousGroupState = v2GroupProperties.getDecryptedGroup(); decryptedChange = groupOperations.decryptChange(changeActions, selfUuid); - decryptedGroupState = DecryptedGroupUtil.apply(v2GroupProperties.getDecryptedGroup(), decryptedChange); + decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { Log.w(TAG, e); throw new IOException(e); @@ -523,7 +525,8 @@ final class GroupManagerV2 { GroupChange signedGroupChange = commitToServer(changeActions); groupDatabase.update(groupId, decryptedGroupState); - RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange, signedGroupChange); + GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState); + RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange); int newMembersCount = decryptedChange.getNewMembersCount(); List newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList()); @@ -681,7 +684,7 @@ final class GroupManagerV2 { } else if (requestToJoin) { Log.i(TAG, "Requested to join, cannot send update"); - RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroup, decryptedChange, signedGroupChange); + RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange); return new GroupManager.GroupActionResult(groupRecipient, recipientAndThread.threadId, @@ -706,7 +709,7 @@ final class GroupManagerV2 { System.currentTimeMillis(), decryptedChange); - RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroup, decryptedChange, signedGroupChange); + RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange); return new GroupManager.GroupActionResult(groupRecipient, recipientAndThread.threadId, @@ -905,7 +908,7 @@ final class GroupManagerV2 { groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.getRevision())); - sendGroupUpdate(groupMasterKey, decryptedGroup, decryptedChange, signedGroupChange); + sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { throw new GroupChangeFailedException(e); } @@ -959,13 +962,12 @@ final class GroupManagerV2 { } private @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey, - @NonNull DecryptedGroup decryptedGroup, - @Nullable DecryptedGroupChange plainGroupChange, + @NonNull GroupMutation groupMutation, @Nullable GroupChange signedGroupChange) { GroupId.V2 groupId = GroupId.v2(masterKey); Recipient groupRecipient = Recipient.externalGroup(context, groupId); - DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange, signedGroupChange); + DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange); OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, decryptedGroupV2Context, null, @@ -977,8 +979,11 @@ final class GroupManagerV2 { Collections.emptyList(), Collections.emptyList()); + + DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange(); + if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) { - ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, decryptedGroup, outgoingMessage)); + ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, groupMutation.getNewGroupState(), outgoingMessage)); return new RecipientAndThread(groupRecipient, -1); } else { long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMutation.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMutation.java new file mode 100644 index 000000000..9108d3dbe --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMutation.java @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.groups; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; + +public final class GroupMutation { + @Nullable private final DecryptedGroup previousGroupState; + @Nullable private final DecryptedGroupChange groupChange; + @NonNull private final DecryptedGroup newGroupState; + + public GroupMutation(@Nullable DecryptedGroup previousGroupState, @Nullable DecryptedGroupChange groupChange, @NonNull DecryptedGroup newGroupState) { + this.previousGroupState = previousGroupState; + this.groupChange = groupChange; + this.newGroupState = newGroupState; + } + + public @Nullable DecryptedGroup getPreviousGroupState() { + return previousGroupState; + } + + public @Nullable DecryptedGroupChange getGroupChange() { + return groupChange; + } + + public @NonNull DecryptedGroup getNewGroupState() { + return newGroupState; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java index 43bab80e3..1a0d290a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java @@ -49,12 +49,13 @@ public final class GroupProtoUtil { } public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey, - @NonNull DecryptedGroup decryptedGroup, - @Nullable DecryptedGroupChange plainGroupChange, + @NonNull GroupMutation groupMutation, @Nullable GroupChange signedServerChange) { - int revision = plainGroupChange != null ? plainGroupChange.getRevision() : decryptedGroup.getRevision(); - SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder() + DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange(); + DecryptedGroup decryptedGroup = groupMutation.getNewGroupState(); + int revision = plainGroupChange != null ? plainGroupChange.getRevision() : decryptedGroup.getRevision(); + SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder() .setMasterKey(ByteString.copyFrom(masterKey.serialize())) .setRevision(revision); @@ -66,6 +67,10 @@ public final class GroupProtoUtil { .setContext(contextBuilder.build()) .setGroupState(decryptedGroup); + if (groupMutation.getPreviousGroupState() != null) { + builder.setPreviousGroupState(groupMutation.getPreviousGroupState()); + } + if (plainGroupChange != null) { builder.setChange(plainGroupChange); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 38276b4cc..37ccde7e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.groups.GroupMutation; import org.thoughtcrime.securesms.groups.GroupNotAMemberException; import org.thoughtcrime.securesms.groups.GroupProtoUtil; import org.thoughtcrime.securesms.groups.GroupsV2Authorization; @@ -226,9 +227,9 @@ public final class GroupsV2StateProcessor { determineProfileSharing(inputGroupState, newLocalState); if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) { Log.i(TAG, "Inserting single update message for restore placeholder"); - insertUpdateMessages(timestamp, Collections.singleton(new LocalGroupLogEntry(newLocalState, null))); + insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null))); } else { - insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries()); + insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries()); } persistLearnedProfileKeys(inputGroupState); @@ -260,7 +261,7 @@ public final class GroupsV2StateProcessor { .addDeleteMembers(UuidUtil.toByteString(selfUuid)) .build(); - DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange, null); + DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(decryptedGroup, simulatedGroupChange, simulatedGroupState), null); OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient, decryptedGroupV2Context, null, @@ -362,13 +363,17 @@ public final class GroupsV2StateProcessor { } } - private void insertUpdateMessages(long timestamp, Collection processedLogEntries) { + private void insertUpdateMessages(long timestamp, + @Nullable DecryptedGroup previousGroupState, + Collection processedLogEntries) + { for (LocalGroupLogEntry entry : processedLogEntries) { if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange()) && !DecryptedGroupUtil.changeIsEmpty(entry.getChange())) { Log.d(TAG, "Skipping profile key changes only update message"); } else { - storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp); + storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(previousGroupState, entry.getChange(), entry.getGroup()), null), timestamp); } + previousGroupState = entry.getGroup(); } } diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index df6edc9d5..ad1937d18 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -28,9 +28,10 @@ import "SignalService.proto"; import "DecryptedGroups.proto"; message DecryptedGroupV2Context { - signalservice.GroupContextV2 context = 1; - DecryptedGroupChange change = 2; - DecryptedGroup groupState = 3; + signalservice.GroupContextV2 context = 1; + DecryptedGroupChange change = 2; + DecryptedGroup groupState = 3; + DecryptedGroup previousGroupState = 4; } message TemporalAuthCredentialResponse { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04c27b6a3..bd23ad9c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1013,6 +1013,12 @@ The group link has been turned on with admin approval off. The group link has been turned on with admin approval on. The group link has been turned off. + You turned off admin approval for the group link. + %1$s turned off admin approval for the group link. + The admin approval for the group link has been turned off. + You turned on admin approval for the group link. + %1$s turned on admin approval for the group link. + The admin approval for the group link has been turned on. You reset the group link. diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java index 25efd4611..1dd53f438 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model; import android.app.Application; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import com.annimon.stream.Stream; @@ -35,6 +36,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -921,6 +923,56 @@ public final class GroupsV2UpdateMessageProducerTest { assertThat(describeChange(change), is(singletonList("The group link has been turned off."))); } + // Group link with known previous group state + + @Test + public void group_link_access_from_unknown_to_administrator() { + assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR)); + assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR)); + assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR)); + } + + @Test + public void group_link_access_from_administrator_to_unsatisfiable() { + assertEquals("You turned off the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE)); + assertEquals("Bob turned off the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE)); + assertEquals("The group link has been turned off.", describeGroupLinkChange(null, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE)); + } + + @Test + public void group_link_access_from_unsatisfiable_to_administrator() { + assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR)); + assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR)); + assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR)); + } + + @Test + public void group_link_access_from_administrator_to_any() { + assertEquals("You turned off admin approval for the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY)); + assertEquals("Bob turned off admin approval for the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY)); + assertEquals("The admin approval for the group link has been turned off.", describeGroupLinkChange(null, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY)); + } + + @Test + public void group_link_access_from_any_to_administrator() { + assertEquals("You turned on admin approval for the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR)); + assertEquals("Bob turned on admin approval for the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR)); + assertEquals("The admin approval for the group link has been turned on.", describeGroupLinkChange(null, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR)); + } + + private String describeGroupLinkChange(@Nullable UUID editor, @NonNull AccessControl.AccessRequired fromAccess, AccessControl.AccessRequired toAccess){ + DecryptedGroup previousGroupState = DecryptedGroup.newBuilder() + .setAccessControl(AccessControl.newBuilder() + .setAddFromInviteLink(fromAccess)) + .build(); + DecryptedGroupChange change = (editor != null ? changeBy(editor) : changeByUnknown()).inviteLinkAccess(toAccess) + .build(); + + List strings = describeChange(previousGroupState, change); + assertEquals(1, strings.size()); + return strings.get(0); + } + // Group link reset @Test @@ -1271,8 +1323,14 @@ public final class GroupsV2UpdateMessageProducerTest { } private @NonNull List describeChange(@NonNull DecryptedGroupChange change) { + return describeChange(null, change); + } + + private @NonNull List describeChange(@Nullable DecryptedGroup previousGroupState, + @NonNull DecryptedGroupChange change) + { MainThreadUtil.setMainThread(false); - return Stream.of(producer.describeChanges(change)) + return Stream.of(producer.describeChanges(previousGroupState, change)) .map(UpdateDescription::getString) .toList(); } @@ -1291,7 +1349,7 @@ public final class GroupsV2UpdateMessageProducerTest { } private void assertSingleChangeMentioning(DecryptedGroupChange change, List expectedMentions) { - List changes = producer.describeChanges(change); + List changes = producer.describeChanges(null, change); assertThat(changes.size(), is(1));