GroupsV2 state mapping.
This commit is contained in:
parent
4e0279200f
commit
7bf090fdab
6 changed files with 361 additions and 0 deletions
|
@ -0,0 +1,29 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pair of log entries applied and a new {@link GlobalGroupState}.
|
||||||
|
*/
|
||||||
|
final class AdvanceGroupStateResult {
|
||||||
|
|
||||||
|
@NonNull private final Collection<GroupLogEntry> processedLogEntries;
|
||||||
|
@NonNull private final GlobalGroupState newGlobalGroupState;
|
||||||
|
|
||||||
|
AdvanceGroupStateResult(@NonNull Collection<GroupLogEntry> processedLogEntries,
|
||||||
|
@NonNull GlobalGroupState newGlobalGroupState)
|
||||||
|
{
|
||||||
|
this.processedLogEntries = processedLogEntries;
|
||||||
|
this.newGlobalGroupState = newGlobalGroupState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Collection<GroupLogEntry> getProcessedLogEntries() {
|
||||||
|
return processedLogEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull GlobalGroupState getNewGlobalGroupState() {
|
||||||
|
return newGlobalGroupState;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combination of Local and Server group state.
|
||||||
|
*/
|
||||||
|
final class GlobalGroupState {
|
||||||
|
|
||||||
|
@Nullable private final DecryptedGroup localState;
|
||||||
|
@NonNull private final List<GroupLogEntry> history;
|
||||||
|
|
||||||
|
GlobalGroupState(@Nullable DecryptedGroup localState,
|
||||||
|
@NonNull List<GroupLogEntry> serverStates)
|
||||||
|
{
|
||||||
|
this.localState = localState;
|
||||||
|
this.history = serverStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable DecryptedGroup getLocalState() {
|
||||||
|
return localState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Collection<GroupLogEntry> getHistory() {
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getLatestVersionNumber() {
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
if (localState == null) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
return localState.getVersion();
|
||||||
|
}
|
||||||
|
return history.get(history.size() - 1).getGroup().getVersion();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pair of a group state and optionally the corresponding change.
|
||||||
|
* <p>
|
||||||
|
* Changes are typically not available for pending members.
|
||||||
|
*/
|
||||||
|
final class GroupLogEntry {
|
||||||
|
|
||||||
|
@NonNull private final DecryptedGroup group;
|
||||||
|
@Nullable private final DecryptedGroupChange change;
|
||||||
|
|
||||||
|
GroupLogEntry(@NonNull DecryptedGroup group, @Nullable DecryptedGroupChange change) {
|
||||||
|
if (change != null && group.getVersion() != change.getVersion()) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.group = group;
|
||||||
|
this.change = change;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull DecryptedGroup getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable DecryptedGroupChange getChange() {
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
final class GroupStateMapper {
|
||||||
|
|
||||||
|
static final int LATEST = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
private static final Comparator<GroupLogEntry> BY_VERSION = (o1, o2) -> Integer.compare(o1.getGroup().getVersion(), o2.getGroup().getVersion());
|
||||||
|
|
||||||
|
private GroupStateMapper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an input {@link GlobalGroupState} and a {@param maximumVersionToApply}, returns a result
|
||||||
|
* containing what the new local group state should be, and any remaining version history to apply.
|
||||||
|
* <p>
|
||||||
|
* Function is pure.
|
||||||
|
* @param maximumVersionToApply Use {@link #LATEST} to apply the very latest.
|
||||||
|
*/
|
||||||
|
static @NonNull AdvanceGroupStateResult partiallyAdvanceGroupState(@NonNull GlobalGroupState inputState,
|
||||||
|
int maximumVersionToApply)
|
||||||
|
{
|
||||||
|
final ArrayList<GroupLogEntry> statesToApplyNow = new ArrayList<>(inputState.getHistory().size());
|
||||||
|
final ArrayList<GroupLogEntry> statesToApplyLater = new ArrayList<>(inputState.getHistory().size());
|
||||||
|
final DecryptedGroup newLocalState;
|
||||||
|
final GlobalGroupState newGlobalGroupState;
|
||||||
|
|
||||||
|
for (GroupLogEntry entry : inputState.getHistory()) {
|
||||||
|
if (inputState.getLocalState() != null &&
|
||||||
|
inputState.getLocalState().getVersion() >= entry.getGroup().getVersion())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.getGroup().getVersion() > maximumVersionToApply) {
|
||||||
|
statesToApplyLater.add(entry);
|
||||||
|
} else {
|
||||||
|
statesToApplyNow.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(statesToApplyNow, BY_VERSION);
|
||||||
|
Collections.sort(statesToApplyLater, BY_VERSION);
|
||||||
|
|
||||||
|
if (statesToApplyNow.size() > 0) {
|
||||||
|
newLocalState = statesToApplyNow.get(statesToApplyNow.size() - 1)
|
||||||
|
.getGroup();
|
||||||
|
} else {
|
||||||
|
newLocalState = inputState.getLocalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
newGlobalGroupState = new GlobalGroupState(newLocalState, statesToApplyLater);
|
||||||
|
|
||||||
|
return new AdvanceGroupStateResult(statesToApplyNow, newGlobalGroupState);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public final class GlobalGroupStateTest {
|
||||||
|
|
||||||
|
@Test(expected = AssertionError.class)
|
||||||
|
public void cannot_ask_latestVersionNumber_of_empty_state() {
|
||||||
|
GlobalGroupState emptyState = new GlobalGroupState(null, emptyList());
|
||||||
|
|
||||||
|
emptyState.getLatestVersionNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void latestVersionNumber_of_state_and_empty_list() {
|
||||||
|
GlobalGroupState emptyState = new GlobalGroupState(state(10), emptyList());
|
||||||
|
|
||||||
|
assertEquals(10, emptyState.getLatestVersionNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void latestVersionNumber_of_state_and_list() {
|
||||||
|
GlobalGroupState emptyState = new GlobalGroupState(state(2), asList(logEntry(3), logEntry(4)));
|
||||||
|
|
||||||
|
assertEquals(4, emptyState.getLatestVersionNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GroupLogEntry logEntry(int version) {
|
||||||
|
return new GroupLogEntry(state(version), change(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecryptedGroup state(int version) {
|
||||||
|
return DecryptedGroup.newBuilder().setVersion(version).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecryptedGroupChange change(int version) {
|
||||||
|
return DecryptedGroupChange.newBuilder().setVersion(version).build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.thoughtcrime.securesms.groups.v2.processing.GroupStateMapper.LATEST;
|
||||||
|
|
||||||
|
public final class GroupStateMapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknown_group_with_no_states_to_update() {
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, emptyList()), 10);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
||||||
|
assertNull(advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_with_no_states_to_update() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, emptyList()), 10);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
||||||
|
assertSame(currentState, advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknown_group_single_state_to_update() {
|
||||||
|
GroupLogEntry log0 = logEntry(0);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(null, singletonList(log0)), 10);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(log0)));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
||||||
|
assertEquals(log0.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_single_state_to_update() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
GroupLogEntry log1 = logEntry(1);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, singletonList(log1)), 1);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(log1)));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
||||||
|
assertEquals(log1.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_two_states_to_update() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
GroupLogEntry log1 = logEntry(1);
|
||||||
|
GroupLogEntry log2 = logEntry(2);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2)));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
||||||
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_two_states_to_update_already_on_one() {
|
||||||
|
DecryptedGroup currentState = state(1);
|
||||||
|
GroupLogEntry log1 = logEntry(1);
|
||||||
|
GroupLogEntry log2 = logEntry(2);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), 2);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(log2)));
|
||||||
|
assertTrue(advanceGroupStateResult.getNewGlobalGroupState().getHistory().isEmpty());
|
||||||
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_three_states_to_update_stop_at_2() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
GroupLogEntry log1 = logEntry(1);
|
||||||
|
GroupLogEntry log2 = logEntry(2);
|
||||||
|
GroupLogEntry log3 = logEntry(3);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), 2);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2)));
|
||||||
|
assertNewState(new GlobalGroupState(log2.getGroup(), singletonList(log3)), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void known_group_three_states_to_update_update_latest() {
|
||||||
|
DecryptedGroup currentState = state(0);
|
||||||
|
GroupLogEntry log1 = logEntry(1);
|
||||||
|
GroupLogEntry log2 = logEntry(2);
|
||||||
|
GroupLogEntry log3 = logEntry(3);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2, log3)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2, log3)));
|
||||||
|
assertNewState(new GlobalGroupState(log3.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log3.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void apply_maximum_group_versions() {
|
||||||
|
DecryptedGroup currentState = state(Integer.MAX_VALUE - 2);
|
||||||
|
GroupLogEntry log1 = logEntry(Integer.MAX_VALUE - 1);
|
||||||
|
GroupLogEntry log2 = logEntry(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(new GlobalGroupState(currentState, asList(log1, log2)), LATEST);
|
||||||
|
|
||||||
|
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(log1, log2)));
|
||||||
|
assertNewState(new GlobalGroupState(log2.getGroup(), emptyList()), advanceGroupStateResult.getNewGlobalGroupState());
|
||||||
|
assertEquals(log2.getGroup(), advanceGroupStateResult.getNewGlobalGroupState().getLocalState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertNewState(GlobalGroupState expected, GlobalGroupState actual) {
|
||||||
|
assertEquals(expected.getLocalState(), actual.getLocalState());
|
||||||
|
assertThat(actual.getHistory(), is(expected.getHistory()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GroupLogEntry logEntry(int version) {
|
||||||
|
return new GroupLogEntry(state(version), change(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecryptedGroup state(int version) {
|
||||||
|
return DecryptedGroup.newBuilder().setVersion(version).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecryptedGroupChange change(int version) {
|
||||||
|
return DecryptedGroupChange.newBuilder().setVersion(version).build();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue