diff --git a/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationManager.java b/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationManager.java
index 8ab792c3e..e33faca9a 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationManager.java
@@ -72,6 +72,17 @@ public interface ConversationManager {
*/
boolean deleteAllMessages(Transaction txn,
ContactId c) throws DbException;
+
+ /**
+ * Deletes the given set of messages associated with the given contact.
+ *
+ * The set of message IDs must only include message IDs returned by
+ * {@link #getMessageIds}.
+ *
+ * @return true if all messages could be deleted, false otherwise
+ */
+ boolean deleteMessages(Transaction txn, ContactId c,
+ Set messageIds) throws DbException;
}
}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index 3be4536ca..60997da29 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -660,6 +660,12 @@ class IntroductionManagerImpl extends ConversationClientImpl
return allDeleted;
}
+ @Override
+ public boolean deleteMessages(Transaction txn, ContactId c,
+ Set messageIds) throws DbException {
+ return false;
+ }
+
@Override
public Set getMessageIds(Transaction txn, ContactId c)
throws DbException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
index 938b67a39..f17becb04 100644
--- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java
@@ -435,4 +435,42 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
return true;
}
+ @Override
+ public boolean deleteMessages(Transaction txn, ContactId c,
+ Set messageIds) throws DbException {
+ for (MessageId messageId : messageIds) {
+ db.deleteMessage(txn, messageId);
+ db.deleteMessageMetadata(txn, messageId);
+ }
+ GroupId g = getContactGroup(db.getContact(txn, c)).getId();
+ recalculateGroupCount(txn, g);
+ return true;
+ }
+
+ private void recalculateGroupCount(Transaction txn, GroupId g)
+ throws DbException {
+ BdfDictionary query = BdfDictionary.of(
+ new BdfEntry(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE));
+ Map results;
+ try {
+ results =
+ clientHelper.getMessageMetadataAsDictionary(txn, g, query);
+ } catch (FormatException e) {
+ throw new DbException(e);
+ }
+ int msgCount = results.size();
+ int unreadCount = 0;
+ for (Map.Entry entry : results.entrySet()) {
+ BdfDictionary meta = entry.getValue();
+ boolean read;
+ try {
+ read = meta.getBoolean(MSG_KEY_READ);
+ } catch (FormatException e) {
+ throw new DbException(e);
+ }
+ if (!read) unreadCount++;
+ }
+ messageTracker.resetGroupCount(txn, g, msgCount, unreadCount);
+ }
+
}
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 b8a8aed7b..78164d898 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
@@ -729,6 +729,12 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
return allDeleted;
}
+ @Override
+ public boolean deleteMessages(Transaction txn, ContactId c,
+ Set messageIds) throws DbException {
+ return false;
+ }
+
@Override
public Set getMessageIds(Transaction txn, ContactId c)
throws DbException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
index 4ebadd4c6..efc6b0b9f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
@@ -629,6 +629,12 @@ abstract class SharingManagerImpl
return allDeleted;
}
+ @Override
+ public boolean deleteMessages(Transaction txn, ContactId c,
+ Set messageIds) throws DbException {
+ return false;
+ }
+
@Override
public Set getMessageIds(Transaction txn, ContactId c)
throws DbException {
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java
new file mode 100644
index 000000000..f23bd3334
--- /dev/null
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java
@@ -0,0 +1,296 @@
+package org.briarproject.briar.messaging;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.MessageDeletedException;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.test.TestDatabaseConfigModule;
+import org.briarproject.briar.api.conversation.ConversationMessageHeader;
+import org.briarproject.briar.api.messaging.AttachmentHeader;
+import org.briarproject.briar.api.messaging.MessagingManager;
+import org.briarproject.briar.api.messaging.PrivateMessage;
+import org.briarproject.briar.api.messaging.PrivateMessageFactory;
+import org.briarproject.briar.api.messaging.PrivateMessageHeader;
+import org.briarproject.briar.test.BriarIntegrationTest;
+import org.briarproject.briar.test.BriarIntegrationTestComponent;
+import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
+import static org.briarproject.bramble.util.StringUtils.getRandomString;
+import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class MessagingManagerIntegrationTest
+ extends BriarIntegrationTest {
+
+ private DatabaseComponent db0, db1;
+ private MessagingManager messagingManager0, messagingManager1;
+ private PrivateMessageFactory messageFactory;
+ private ContactId contactId;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ db0 = c0.getDatabaseComponent();
+ db1 = c1.getDatabaseComponent();
+ messagingManager0 = c0.getMessagingManager();
+ messagingManager1 = c1.getMessagingManager();
+ messageFactory = c0.getPrivateMessageFactory();
+ assertEquals(contact0From1, contact1From0);
+ contactId = contactId0From1;
+ }
+
+ @Override
+ protected void createComponents() {
+ BriarIntegrationTestComponent component =
+ DaggerBriarIntegrationTestComponent.builder().build();
+ component.injectBriarEagerSingletons();
+ component.inject(this);
+
+ c0 = DaggerBriarIntegrationTestComponent.builder()
+ .testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir))
+ .build();
+ c0.injectBriarEagerSingletons();
+
+ c1 = DaggerBriarIntegrationTestComponent.builder()
+ .testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir))
+ .build();
+ c1.injectBriarEagerSingletons();
+
+ c2 = DaggerBriarIntegrationTestComponent.builder()
+ .testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir))
+ .build();
+ c2.injectBriarEagerSingletons();
+ }
+
+ @Test
+ public void testSimpleConversation() throws Exception {
+ // conversation start out empty
+ Collection messages0 = getMessages(c0);
+ Collection messages1 = getMessages(c1);
+ assertEquals(0, messages0.size());
+ assertEquals(0, messages1.size());
+
+ // message is sent/displayed properly
+ String text = getRandomString(42);
+ sendMessage(c0, c1, text);
+ messages0 = getMessages(c0);
+ messages1 = getMessages(c1);
+ assertEquals(1, messages0.size());
+ assertEquals(1, messages1.size());
+ PrivateMessageHeader m0 =
+ (PrivateMessageHeader) messages0.iterator().next();
+ PrivateMessageHeader m1 =
+ (PrivateMessageHeader) messages1.iterator().next();
+ assertTrue(m0.hasText());
+ assertTrue(m1.hasText());
+ assertTrue(m0.isRead());
+ assertFalse(m1.isRead());
+ assertGroupCounts(c0, 1, 0);
+ assertGroupCounts(c1, 1, 1);
+
+ // same for reply
+ String text2 = getRandomString(42);
+ sendMessage(c1, c0, text2);
+ messages0 = getMessages(c0);
+ messages1 = getMessages(c1);
+ assertEquals(2, messages0.size());
+ assertEquals(2, messages1.size());
+ assertGroupCounts(c0, 2, 1);
+ assertGroupCounts(c1, 2, 1);
+ }
+
+ @Test
+ public void testAttachments() throws Exception {
+ // send message with attachment
+ AttachmentHeader h = addAttachment(c0);
+ sendMessage(c0, c1, null, singletonList(h));
+
+ // message with attachment is sent/displayed properly
+ Collection messages0 = getMessages(c0);
+ Collection messages1 = getMessages(c1);
+ assertEquals(1, messages0.size());
+ assertEquals(1, messages1.size());
+ PrivateMessageHeader m0 =
+ (PrivateMessageHeader) messages0.iterator().next();
+ PrivateMessageHeader m1 =
+ (PrivateMessageHeader) messages1.iterator().next();
+ assertFalse(m0.hasText());
+ assertFalse(m1.hasText());
+ assertEquals(1, m0.getAttachmentHeaders().size());
+ assertEquals(1, m1.getAttachmentHeaders().size());
+ assertGroupCounts(c0, 1, 0);
+ assertGroupCounts(c1, 1, 1);
+ }
+
+ @Test
+ public void testDeleteAll() throws Exception {
+ // send 3 message (1 with attachment)
+ sendMessage(c0, c1, getRandomString(42));
+ sendMessage(c0, c1, getRandomString(23));
+ sendMessage(c0, c1, null, singletonList(addAttachment(c0)));
+ assertEquals(3, getMessages(c0).size());
+ assertEquals(3, getMessages(c1).size());
+ assertGroupCounts(c0, 3, 0);
+ assertGroupCounts(c1, 3, 3);
+
+ // delete all messages on both sides (deletes all, because returns true)
+ assertTrue(db0.transactionWithResult(false,
+ txn -> messagingManager0.deleteAllMessages(txn, contactId)));
+ assertTrue(db1.transactionWithResult(false,
+ txn -> messagingManager1.deleteAllMessages(txn, contactId)));
+
+ // all messages are gone
+ assertEquals(0, getMessages(c0).size());
+ assertEquals(0, getMessages(c1).size());
+ assertGroupCounts(c0, 0, 0);
+ assertGroupCounts(c1, 0, 0);
+ }
+
+ @Test
+ public void testDeleteSubset() throws Exception {
+ // send 3 message (1 with attachment)
+ PrivateMessage m0 = sendMessage(c0, c1, getRandomString(42));
+ PrivateMessage m1 = sendMessage(c0, c1, getRandomString(23));
+ PrivateMessage m2 =
+ sendMessage(c0, c1, null, singletonList(addAttachment(c0)));
+ assertGroupCounts(c0, 3, 0);
+ assertGroupCounts(c1, 3, 3);
+
+ // delete 2 messages on both sides (deletes all, because returns true)
+ Set toDelete = new HashSet<>();
+ toDelete.add(m1.getMessage().getId());
+ toDelete.add(m2.getMessage().getId());
+ assertTrue(db0.transactionWithResult(false, txn ->
+ messagingManager0.deleteMessages(txn, contactId, toDelete)));
+ assertTrue(db1.transactionWithResult(false, txn ->
+ messagingManager1.deleteMessages(txn, contactId, toDelete)));
+
+ // all messages except 1 are gone
+ assertEquals(1, getMessages(c0).size());
+ assertEquals(1, getMessages(c1).size());
+ assertEquals(m0.getMessage().getId(),
+ getMessages(c0).iterator().next().getId());
+ assertEquals(m0.getMessage().getId(),
+ getMessages(c1).iterator().next().getId());
+ assertGroupCounts(c0, 1, 0);
+ assertGroupCounts(c1, 1, 1);
+
+ // remove also last message
+ toDelete.clear();
+ toDelete.add(m0.getMessage().getId());
+ assertTrue(db0.transactionWithResult(false, txn ->
+ messagingManager0.deleteMessages(txn, contactId, toDelete)));
+ assertEquals(0, getMessages(c0).size());
+ assertGroupCounts(c0, 0, 0);
+ }
+
+ @Test
+ public void testDeleteAttachment() throws Exception {
+ // send one message with attachment
+ AttachmentHeader h = addAttachment(c0);
+ sendMessage(c0, c1, getRandomString(42), singletonList(h));
+
+ // attachment exists on both devices
+ db0.transaction(true, txn -> db0.getMessage(txn, h.getMessageId()));
+ db1.transaction(true, txn -> db1.getMessage(txn, h.getMessageId()));
+
+ // delete message on both sides (deletes all, because returns true)
+ assertTrue(db0.transactionWithResult(false,
+ txn -> messagingManager0.deleteAllMessages(txn, contactId)));
+ assertTrue(db1.transactionWithResult(false,
+ txn -> messagingManager1.deleteAllMessages(txn, contactId)));
+
+ // attachment was deleted on both devices
+ try {
+ db0.transaction(true, txn -> db0.getMessage(txn, h.getMessageId()));
+ fail();
+ } catch (MessageDeletedException e) {
+ // expected
+ }
+ try {
+ db1.transaction(true, txn -> db1.getMessage(txn, h.getMessageId()));
+ fail();
+ } catch (MessageDeletedException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testDeletingEmptySet() throws Exception {
+ assertTrue(db0.transactionWithResult(false, txn ->
+ messagingManager0.deleteMessages(txn, contactId, emptySet())));
+ }
+
+ private PrivateMessage sendMessage(BriarIntegrationTestComponent from,
+ BriarIntegrationTestComponent to, String text)
+ throws Exception {
+ return sendMessage(from, to, text, emptyList());
+ }
+
+ private PrivateMessage sendMessage(BriarIntegrationTestComponent from,
+ BriarIntegrationTestComponent to, @Nullable String text,
+ List attachments) throws Exception {
+ GroupId g = from.getMessagingManager().getConversationId(contactId);
+ PrivateMessage m = messageFactory.createPrivateMessage(g,
+ clock.currentTimeMillis(), text, attachments);
+ from.getMessagingManager().addLocalMessage(m);
+ syncMessage(from, to, contactId, 1 + attachments.size(), true);
+ return m;
+ }
+
+ private AttachmentHeader addAttachment(BriarIntegrationTestComponent c)
+ throws Exception {
+ GroupId g = c.getMessagingManager().getConversationId(contactId);
+ InputStream stream = new ByteArrayInputStream(getRandomBytes(42));
+ return c.getMessagingManager().addLocalAttachment(g,
+ clock.currentTimeMillis(), "image/jpeg", stream);
+ }
+
+ private Collection getMessages(
+ BriarIntegrationTestComponent c)
+ throws Exception {
+ Collection messages =
+ c.getDatabaseComponent().transactionWithResult(true,
+ txn -> c.getMessagingManager()
+ .getMessageHeaders(txn, contactId));
+ Set ids =
+ c.getDatabaseComponent().transactionWithResult(true,
+ txn ->
+ c.getMessagingManager()
+ .getMessageIds(txn, contactId));
+ assertEquals(messages.size(), ids.size());
+ for (ConversationMessageHeader h : messages) {
+ assertTrue(ids.contains(h.getId()));
+ }
+ return messages;
+ }
+
+ private void assertGroupCounts(BriarIntegrationTestComponent c,
+ long msgCount, long unreadCount) throws Exception {
+ GroupId g = c.getMessagingManager().getConversationId(contactId);
+ assertGroupCount(c.getMessageTracker(), g, msgCount, unreadCount);
+ }
+
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
index fee6261db..c900410d2 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTest.java
@@ -354,7 +354,7 @@ public abstract class BriarIntegrationTest