From f4c5855dd8dba6be943e516fcaae6fe41615e3ae Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Mon, 16 Apr 2018 14:55:02 +0100
Subject: [PATCH] Use client versioning for private groups.

---
 .../invitation/AbstractProtocolEngine.java    | 16 +++-
 .../invitation/CreatorProtocolEngine.java     |  8 +-
 .../privategroup/invitation/CreatorState.java | 20 +++-
 .../invitation/GroupInvitationConstants.java  |  1 +
 .../GroupInvitationManagerImpl.java           | 91 ++++++++++++++++++-
 .../invitation/GroupInvitationModule.java     | 15 ++-
 .../invitation/InviteeProtocolEngine.java     | 14 ++-
 .../privategroup/invitation/InviteeState.java | 23 ++++-
 .../invitation/PeerProtocolEngine.java        |  8 +-
 .../privategroup/invitation/PeerState.java    | 23 ++++-
 .../invitation/ProtocolEngineFactoryImpl.java | 13 ++-
 .../invitation/SessionEncoderImpl.java        |  2 +
 .../invitation/SessionParser.java             |  2 +
 .../invitation/SessionParserImpl.java         |  6 ++
 .../briar/privategroup/invitation/State.java  |  4 +
 .../AbstractProtocolEngineTest.java           | 10 +-
 .../invitation/CreatorProtocolEngineTest.java |  7 +-
 .../GroupInvitationManagerImplTest.java       | 16 +++-
 .../invitation/InviteeProtocolEngineTest.java |  9 +-
 .../invitation/PeerProtocolEngineTest.java    |  7 +-
 20 files changed, 251 insertions(+), 44 deletions(-)

diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java
index c68f7b6625..c37c267251 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java
@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -28,6 +29,8 @@ import java.util.Map;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_ID;
+import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_VERSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
 import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
 import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE;
@@ -45,6 +48,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 	protected final PrivateGroupFactory privateGroupFactory;
 	protected final MessageTracker messageTracker;
 
+	private final ClientVersioningManager clientVersioningManager;
 	private final GroupMessageFactory groupMessageFactory;
 	private final IdentityManager identityManager;
 	private final MessageParser messageParser;
@@ -52,6 +56,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 	private final Clock clock;
 
 	AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
@@ -60,6 +65,7 @@ abstract class AbstractProtocolEngine<S extends Session>
 			Clock clock) {
 		this.db = db;
 		this.clientHelper = clientHelper;
+		this.clientVersioningManager = clientVersioningManager;
 		this.privateGroupManager = privateGroupManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.groupMessageFactory = groupMessageFactory;
@@ -90,10 +96,14 @@ abstract class AbstractProtocolEngine<S extends Session>
 		return expected != null && dependency.equals(expected);
 	}
 
-	void setPrivateGroupVisibility(Transaction txn, S session, Visibility v)
-			throws DbException, FormatException {
+	void setPrivateGroupVisibility(Transaction txn, S session,
+			Visibility preferred) throws DbException, FormatException {
+		// Apply min of preferred visibility and client's visibility
 		ContactId contactId = getContactId(txn, session.getContactGroupId());
-		db.setGroupVisibility(txn, contactId, session.getPrivateGroupId(), v);
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				contactId, CLIENT_ID, CLIENT_VERSION);
+		Visibility min = Visibility.min(preferred, client);
+		db.setGroupVisibility(txn, contactId, session.getPrivateGroupId(), min);
 	}
 
 	Message sendInviteMessage(Transaction txn, S session,
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
index ec5315a6d0..62030a09b7 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java
@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.MessageTracker;
@@ -36,15 +37,16 @@ import static org.briarproject.briar.privategroup.invitation.CreatorState.START;
 class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 
 	CreatorProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
 			IdentityManager identityManager, MessageParser messageParser,
 			MessageEncoder messageEncoder, MessageTracker messageTracker,
 			Clock clock) {
-		super(db, clientHelper, privateGroupManager, privateGroupFactory,
-				groupMessageFactory, identityManager, messageParser,
-				messageEncoder, messageTracker, clock);
+		super(db, clientHelper, clientVersioningManager, privateGroupManager,
+				privateGroupFactory, groupMessageFactory, identityManager,
+				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java
index 4afe89c4d8..6fe69a08c9 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorState.java
@@ -2,19 +2,30 @@ package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+
 @Immutable
 @NotNullByDefault
 enum CreatorState implements State {
 
-	START(0), INVITED(1), JOINED(2), LEFT(3), DISSOLVED(4), ERROR(5);
+	START(0, INVISIBLE),
+	INVITED(1, INVISIBLE),
+	JOINED(2, SHARED),
+	LEFT(3, INVISIBLE),
+	DISSOLVED(4, INVISIBLE),
+	ERROR(5, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	CreatorState(int value) {
+	CreatorState(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	@Override
@@ -22,6 +33,11 @@ enum CreatorState implements State {
 		return value;
 	}
 
+	@Override
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	static CreatorState fromValue(int value) throws FormatException {
 		for (CreatorState s : values()) if (s.value == value) return s;
 		throw new FormatException();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java
index a9a6ef0929..90c0fcd299 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java
@@ -15,6 +15,7 @@ interface GroupInvitationConstants {
 	String MSG_KEY_INVITATION_ACCEPTED = "invitationAccepted";
 
 	// Session keys
+	String SESSION_KEY_IS_SESSION = "isSession";
 	String SESSION_KEY_SESSION_ID = "sessionId";
 	String SESSION_KEY_PRIVATE_GROUP_ID = "privateGroupId";
 	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
index 5ae9a3f81b..6275fad703 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -16,7 +16,10 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
+import org.briarproject.bramble.api.sync.ClientVersioningManager.ClientVersioningHook;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
@@ -36,15 +39,17 @@ import org.briarproject.briar.client.ConversationClientImpl;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static java.util.logging.Level.INFO;
 import static org.briarproject.briar.privategroup.invitation.CreatorState.START;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
 import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT;
@@ -59,8 +64,12 @@ import static org.briarproject.briar.privategroup.invitation.Role.PEER;
 @NotNullByDefault
 class GroupInvitationManagerImpl extends ConversationClientImpl
 		implements GroupInvitationManager, Client, ContactHook,
-		PrivateGroupHook {
+		PrivateGroupHook, ClientVersioningHook {
 
+	private static final Logger LOG =
+			Logger.getLogger(GroupInvitationManagerImpl.class.getName());
+
+	private final ClientVersioningManager clientVersioningManager;
 	private final ContactGroupFactory contactGroupFactory;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final PrivateGroupManager privateGroupManager;
@@ -73,8 +82,9 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 
 	@Inject
 	GroupInvitationManagerImpl(DatabaseComponent db,
-			ClientHelper clientHelper, MetadataParser metadataParser,
-			MessageTracker messageTracker,
+			ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
+			MetadataParser metadataParser, MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
 			PrivateGroupFactory privateGroupFactory,
 			PrivateGroupManager privateGroupManager,
@@ -82,6 +92,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 			SessionEncoder sessionEncoder,
 			ProtocolEngineFactory engineFactory) {
 		super(db, clientHelper, metadataParser, messageTracker);
+		this.clientVersioningManager = clientVersioningManager;
 		this.contactGroupFactory = contactGroupFactory;
 		this.privateGroupFactory = privateGroupFactory;
 		this.privateGroupManager = privateGroupManager;
@@ -110,7 +121,11 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		Group g = getContactGroup(c);
 		// Store the group and share it with the contact
 		db.addGroup(txn, g);
-		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
+		Visibility client = clientVersioningManager.getClientVisibility(txn,
+				c.getId(), CLIENT_ID, CLIENT_VERSION);
+		if (LOG.isLoggable(INFO))
+			LOG.info("Applying visibility " + client + " to new contact group");
+		db.setGroupVisibility(txn, c.getId(), g.getId(), client);
 		// Attach the contact ID to the group
 		BdfDictionary meta = new BdfDictionary();
 		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
@@ -565,6 +580,72 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		}
 	}
 
+	@Override
+	public void onClientVisibilityChanging(Transaction txn, Contact c,
+			Visibility v) throws DbException {
+		// Apply the client's visibility to the contact group
+		if (LOG.isLoggable(INFO))
+			LOG.info("Applying visibility " + v + " to contact group");
+		Group g = getContactGroup(c);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), v);
+	}
+
+	ClientVersioningHook getPrivateGroupClientVersioningHook() {
+		return this::onPrivateGroupClientVisibilityChanging;
+	}
+
+	private void onPrivateGroupClientVisibilityChanging(Transaction txn,
+			Contact c, Visibility client) throws DbException {
+		try {
+			Collection<Group> shareables =
+					db.getGroups(txn, PrivateGroupManager.CLIENT_ID,
+							PrivateGroupManager.CLIENT_VERSION);
+			Map<GroupId, Visibility> m = getPreferredVisibilities(txn, c);
+			for (Group g : shareables) {
+				Visibility preferred = m.get(g.getId());
+				if (preferred == null) continue; // No session for this group
+				// Apply min of preferred visibility and client's visibility
+				Visibility min = Visibility.min(preferred, client);
+				if (LOG.isLoggable(INFO)) {
+					LOG.info("Applying visibility " + min
+							+ " to private group, preferred " + preferred
+							+ ", client " + client);
+				}
+				db.setGroupVisibility(txn, c.getId(), g.getId(), min);
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private Map<GroupId, Visibility> getPreferredVisibilities(Transaction txn,
+			Contact c) throws DbException, FormatException {
+		GroupId contactGroupId = getContactGroup(c).getId();
+		BdfDictionary query = sessionParser.getAllSessionsQuery();
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, contactGroupId, query);
+		Map<GroupId, Visibility> m = new HashMap<>();
+		for (BdfDictionary d : results.values()) {
+			Role role = sessionParser.getRole(d);
+			if (role == CREATOR) {
+				CreatorSession s =
+						sessionParser.parseCreatorSession(contactGroupId, d);
+				m.put(s.getPrivateGroupId(), s.getState().getVisibility());
+			} else if (role == INVITEE) {
+				InviteeSession s =
+						sessionParser.parseInviteeSession(contactGroupId, d);
+				m.put(s.getPrivateGroupId(), s.getState().getVisibility());
+			} else if (role == PEER) {
+				PeerSession s =
+						sessionParser.parsePeerSession(contactGroupId, d);
+				m.put(s.getPrivateGroupId(), s.getState().getVisibility());
+			} else {
+				throw new AssertionError();
+			}
+		}
+		return m;
+	}
+
 	private static class StoredSession {
 
 		private final MessageId storageId;
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java
index 178cafdd09..f022875743 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java
@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.messaging.ConversationManager;
@@ -38,13 +39,25 @@ public class GroupInvitationModule {
 			LifecycleManager lifecycleManager,
 			ValidationManager validationManager, ContactManager contactManager,
 			PrivateGroupManager privateGroupManager,
-			ConversationManager conversationManager) {
+			ConversationManager conversationManager,
+			ClientVersioningManager clientVersioningManager) {
 		lifecycleManager.registerClient(groupInvitationManager);
 		validationManager.registerIncomingMessageHook(CLIENT_ID, CLIENT_VERSION,
 				groupInvitationManager);
 		contactManager.registerContactHook(groupInvitationManager);
 		privateGroupManager.registerPrivateGroupHook(groupInvitationManager);
 		conversationManager.registerConversationClient(groupInvitationManager);
+		clientVersioningManager.registerClient(CLIENT_ID, CLIENT_VERSION);
+		clientVersioningManager.registerClientVersioningHook(CLIENT_ID,
+				CLIENT_VERSION, groupInvitationManager);
+		// The group invitation manager handles client visibility changes for
+		// the private group manager
+		clientVersioningManager.registerClient(PrivateGroupManager.CLIENT_ID,
+				PrivateGroupManager.CLIENT_VERSION);
+		clientVersioningManager.registerClientVersioningHook(
+				PrivateGroupManager.CLIENT_ID,
+				PrivateGroupManager.CLIENT_VERSION,
+				groupInvitationManager.getPrivateGroupClientVersioningHook());
 		return groupInvitationManager;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
index f181d1f776..064c1d5936 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
@@ -41,15 +42,16 @@ import static org.briarproject.briar.privategroup.invitation.InviteeState.START;
 class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 
 	InviteeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
 			IdentityManager identityManager, MessageParser messageParser,
 			MessageEncoder messageEncoder, MessageTracker messageTracker,
 			Clock clock) {
-		super(db, clientHelper, privateGroupManager, privateGroupFactory,
-				groupMessageFactory, identityManager, messageParser,
-				messageEncoder, messageTracker, clock);
+		super(db, clientHelper, clientVersioningManager, privateGroupManager,
+				privateGroupFactory, groupMessageFactory, identityManager,
+				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
@@ -212,6 +214,12 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 			throws DbException {
 		// Send a LEAVE message
 		Message sent = sendLeaveMessage(txn, s, false);
+		try {
+			// Make the private group invisible to the contact
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
 		// Move to the LEFT state
 		return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java
index 7fa69e6af9..f347ca0618 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeState.java
@@ -2,20 +2,32 @@ package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+
 @Immutable
 @NotNullByDefault
 enum InviteeState implements State {
 
-	START(0), INVITED(1), ACCEPTED(2), JOINED(3), LEFT(4), DISSOLVED(5),
-	ERROR(6);
+	START(0, INVISIBLE),
+	INVITED(1, INVISIBLE),
+	ACCEPTED(2, VISIBLE),
+	JOINED(3, SHARED),
+	LEFT(4, INVISIBLE),
+	DISSOLVED(5, INVISIBLE),
+	ERROR(6, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	InviteeState(int value) {
+	InviteeState(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	@Override
@@ -23,6 +35,11 @@ enum InviteeState implements State {
 		return value;
 	}
 
+	@Override
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	static InviteeState fromValue(int value) throws FormatException {
 		for (InviteeState s : values()) if (s.value == value) return s;
 		throw new FormatException();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
index c1908735fd..5a7f1c6c3a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.MessageTracker;
@@ -36,15 +37,16 @@ import static org.briarproject.briar.privategroup.invitation.PeerState.START;
 class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 
 	PeerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
 			IdentityManager identityManager, MessageParser messageParser,
 			MessageEncoder messageEncoder, MessageTracker messageTracker,
 			Clock clock) {
-		super(db, clientHelper, privateGroupManager, privateGroupFactory,
-				groupMessageFactory, identityManager, messageParser,
-				messageEncoder, messageTracker, clock);
+		super(db, clientHelper, clientVersioningManager, privateGroupManager,
+				privateGroupFactory, groupMessageFactory, identityManager,
+				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java
index 94f9d19afe..041528266e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerState.java
@@ -2,20 +2,32 @@ package org.briarproject.briar.privategroup.invitation;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+
 @Immutable
 @NotNullByDefault
 enum PeerState implements State {
 
-	START(0), AWAIT_MEMBER(1), NEITHER_JOINED(2), LOCAL_JOINED(3),
-	BOTH_JOINED(4), LOCAL_LEFT(5), ERROR(6);
+	START(0, INVISIBLE),
+	AWAIT_MEMBER(1, INVISIBLE),
+	NEITHER_JOINED(2, INVISIBLE),
+	LOCAL_JOINED(3, VISIBLE),
+	BOTH_JOINED(4, SHARED),
+	LOCAL_LEFT(5, INVISIBLE),
+	ERROR(6, INVISIBLE);
 
 	private final int value;
+	private final Visibility visibility;
 
-	PeerState(int value) {
+	PeerState(int value, Visibility visibility) {
 		this.value = value;
+		this.visibility = visibility;
 	}
 
 	@Override
@@ -23,6 +35,11 @@ enum PeerState implements State {
 		return value;
 	}
 
+	@Override
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
 	static PeerState fromValue(int value) throws FormatException {
 		for (PeerState s : values()) if (s.value == value) return s;
 		throw new FormatException();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java
index af3bbe8167..801d16d06e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngineFactoryImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -19,6 +20,7 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 
 	private final DatabaseComponent db;
 	private final ClientHelper clientHelper;
+	private final ClientVersioningManager clientVersioningManager;
 	private final PrivateGroupManager privateGroupManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final GroupMessageFactory groupMessageFactory;
@@ -30,6 +32,7 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 
 	@Inject
 	ProtocolEngineFactoryImpl(DatabaseComponent db, ClientHelper clientHelper,
+			ClientVersioningManager clientVersioningManager,
 			PrivateGroupManager privateGroupManager,
 			PrivateGroupFactory privateGroupFactory,
 			GroupMessageFactory groupMessageFactory,
@@ -38,6 +41,7 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 			Clock clock) {
 		this.db = db;
 		this.clientHelper = clientHelper;
+		this.clientVersioningManager = clientVersioningManager;
 		this.privateGroupManager = privateGroupManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.groupMessageFactory = groupMessageFactory;
@@ -50,21 +54,24 @@ class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
 
 	@Override
 	public ProtocolEngine<CreatorSession> createCreatorEngine() {
-		return new CreatorProtocolEngine(db, clientHelper, privateGroupManager,
+		return new CreatorProtocolEngine(db, clientHelper,
+				clientVersioningManager, privateGroupManager,
 				privateGroupFactory, groupMessageFactory, identityManager,
 				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
 	public ProtocolEngine<InviteeSession> createInviteeEngine() {
-		return new InviteeProtocolEngine(db, clientHelper, privateGroupManager,
+		return new InviteeProtocolEngine(db, clientHelper,
+				clientVersioningManager, privateGroupManager,
 				privateGroupFactory, groupMessageFactory, identityManager,
 				messageParser, messageEncoder, messageTracker, clock);
 	}
 
 	@Override
 	public ProtocolEngine<PeerSession> createPeerEngine() {
-		return new PeerProtocolEngine(db, clientHelper, privateGroupManager,
+		return new PeerProtocolEngine(db, clientHelper,
+				clientVersioningManager, privateGroupManager,
 				privateGroupFactory, groupMessageFactory, identityManager,
 				messageParser, messageEncoder, messageTracker, clock);
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java
index 78e36bf3a5..86fe5e3a04 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionEncoderImpl.java
@@ -9,6 +9,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
+import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_IS_SESSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
@@ -28,6 +29,7 @@ class SessionEncoderImpl implements SessionEncoder {
 	@Override
 	public BdfDictionary encodeSession(Session s) {
 		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_IS_SESSION, true);
 		d.put(SESSION_KEY_SESSION_ID, s.getPrivateGroupId());
 		d.put(SESSION_KEY_PRIVATE_GROUP_ID, s.getPrivateGroupId());
 		MessageId lastLocalMessageId = s.getLastLocalMessageId();
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
index baf0de80e2..152ed067e3 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParser.java
@@ -11,6 +11,8 @@ interface SessionParser {
 
 	BdfDictionary getSessionQuery(SessionId s);
 
+	BdfDictionary getAllSessionsQuery();
+
 	Role getRole(BdfDictionary d) throws FormatException;
 
 	CreatorSession parseCreatorSession(GroupId contactGroupId, BdfDictionary d)
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
index e8813b6dcd..19a424d707 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/SessionParserImpl.java
@@ -13,6 +13,7 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
+import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_IS_SESSION;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
 import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
@@ -37,6 +38,11 @@ class SessionParserImpl implements SessionParser {
 		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
 	}
 
+	@Override
+	public BdfDictionary getAllSessionsQuery() {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_IS_SESSION, true));
+	}
+
 	@Override
 	public Role getRole(BdfDictionary d) throws FormatException {
 		return Role.fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java
index 46df85efdc..2313f96c7e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/State.java
@@ -1,6 +1,10 @@
 package org.briarproject.briar.privategroup.invitation;
 
+import org.briarproject.bramble.api.sync.Group.Visibility;
+
 interface State {
 
 	int getValue();
+
+	Visibility getVisibility();
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java
index 3ae2fb61e4..8c8056512f 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java
@@ -10,7 +10,9 @@ import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.Group.Visibility;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
@@ -24,6 +26,7 @@ import org.briarproject.briar.api.privategroup.PrivateGroupManager;
 import org.jmock.Expectations;
 
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getGroup;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
@@ -47,6 +50,8 @@ public abstract class AbstractProtocolEngineTest extends BrambleMockTestCase {
 			context.mock(DatabaseComponent.class);
 	protected final ClientHelper clientHelper =
 			context.mock(ClientHelper.class);
+	protected final ClientVersioningManager clientVersioningManager =
+			context.mock(ClientVersioningManager.class);
 	protected final PrivateGroupFactory privateGroupFactory =
 			context.mock(PrivateGroupFactory.class);
 	protected final PrivateGroupManager privateGroupManager =
@@ -181,10 +186,13 @@ public abstract class AbstractProtocolEngineTest extends BrambleMockTestCase {
 		}});
 	}
 
-	protected void expectSetPrivateGroupVisibility(Group.Visibility v)
+	protected void expectSetPrivateGroupVisibility(Visibility v)
 			throws Exception {
 		expectGetContactId();
 		context.checking(new Expectations() {{
+			oneOf(clientVersioningManager).getClientVisibility(txn, contactId,
+					CLIENT_ID, CLIENT_VERSION);
+			will(returnValue(SHARED));
 			oneOf(db).setGroupVisibility(txn, contactId, privateGroupId, v);
 		}});
 	}
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
index 22e64a9f98..c1b2bd9b27 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java
@@ -19,9 +19,10 @@ import static org.junit.Assert.assertEquals;
 public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest {
 
 	private final CreatorProtocolEngine engine =
-			new CreatorProtocolEngine(db, clientHelper, privateGroupManager,
-					privateGroupFactory, groupMessageFactory, identityManager,
-					messageParser, messageEncoder, messageTracker, clock);
+			new CreatorProtocolEngine(db, clientHelper, clientVersioningManager,
+					privateGroupManager, privateGroupFactory,
+					groupMessageFactory, identityManager, messageParser,
+					messageEncoder, messageTracker, clock);
 
 	private CreatorSession getDefaultSession(CreatorState state) {
 		return new CreatorSession(contactGroupId, privateGroupId,
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
index 1a7ae0857d..e7db720c76 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java
@@ -15,6 +15,7 @@ import org.briarproject.bramble.api.db.Metadata;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.sync.ClientVersioningManager;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
@@ -69,6 +70,8 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 
 	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
 	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
+	private final ClientVersioningManager clientVersioningManager =
+			context.mock(ClientVersioningManager.class);
 	private final ContactGroupFactory contactGroupFactory =
 			context.mock(ContactGroupFactory.class);
 	private final PrivateGroupFactory privateGroupFactory =
@@ -140,11 +143,11 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 		}});
 		MetadataParser metadataParser = context.mock(MetadataParser.class);
 		MessageTracker messageTracker = context.mock(MessageTracker.class);
-		groupInvitationManager =
-				new GroupInvitationManagerImpl(db, clientHelper, metadataParser,
-						messageTracker, contactGroupFactory,
-						privateGroupFactory, privateGroupManager, messageParser,
-						sessionParser, sessionEncoder, engineFactory);
+		groupInvitationManager = new GroupInvitationManagerImpl(db,
+				clientHelper, clientVersioningManager, metadataParser,
+				messageTracker, contactGroupFactory, privateGroupFactory,
+				privateGroupManager, messageParser, sessionParser,
+				sessionEncoder, engineFactory);
 	}
 
 	@Test
@@ -184,6 +187,9 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase {
 					CLIENT_VERSION, c);
 			will(returnValue(contactGroup));
 			oneOf(db).addGroup(txn, contactGroup);
+			oneOf(clientVersioningManager).getClientVisibility(txn, contactId,
+					CLIENT_ID, CLIENT_VERSION);
+			will(returnValue(SHARED));
 			oneOf(db).setGroupVisibility(txn, c.getId(), contactGroup.getId(),
 					SHARED);
 			oneOf(clientHelper)
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
index 0030945428..f398d3a7fd 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java
@@ -38,9 +38,10 @@ import static org.junit.Assert.assertTrue;
 public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 
 	private final InviteeProtocolEngine engine =
-			new InviteeProtocolEngine(db, clientHelper, privateGroupManager,
-					privateGroupFactory, groupMessageFactory, identityManager,
-					messageParser, messageEncoder, messageTracker, clock);
+			new InviteeProtocolEngine(db, clientHelper, clientVersioningManager,
+					privateGroupManager, privateGroupFactory,
+					groupMessageFactory, identityManager, messageParser,
+					messageEncoder, messageTracker, clock);
 	private final LocalAuthor localAuthor = getLocalAuthor();
 
 	private InviteeSession getDefaultSession(InviteeState state) {
@@ -238,6 +239,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 	@Test
 	public void testOnLeaveActionFromAccepted() throws Exception {
 		expectSendLeaveMessage(false);
+		expectSetPrivateGroupVisibility(INVISIBLE);
 		InviteeSession session = getDefaultSession(ACCEPTED);
 		InviteeSession newSession = engine.onLeaveAction(txn, session);
 
@@ -249,6 +251,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest {
 	@Test
 	public void testOnLeaveActionFromJoined() throws Exception {
 		expectSendLeaveMessage(false);
+		expectSetPrivateGroupVisibility(INVISIBLE);
 		InviteeSession session = getDefaultSession(JOINED);
 		InviteeSession newSession = engine.onLeaveAction(txn, session);
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
index d5d53168fa..05c0920c54 100644
--- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java
@@ -24,9 +24,10 @@ import static org.junit.Assert.assertTrue;
 public class PeerProtocolEngineTest extends AbstractProtocolEngineTest {
 
 	private final PeerProtocolEngine engine =
-			new PeerProtocolEngine(db, clientHelper, privateGroupManager,
-					privateGroupFactory, groupMessageFactory, identityManager,
-					messageParser, messageEncoder, messageTracker, clock);
+			new PeerProtocolEngine(db, clientHelper, clientVersioningManager,
+					privateGroupManager, privateGroupFactory,
+					groupMessageFactory, identityManager, messageParser,
+					messageEncoder, messageTracker, clock);
 
 	private PeerSession getDefaultSession(PeerState state) {
 		return new PeerSession(contactGroupId, privateGroupId,
-- 
GitLab