diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java
index d2bfdaad7..08219d4a5 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java
@@ -829,10 +829,6 @@ public class ConversationActivity extends BriarActivity
fails.add(getString(
R.string.dialog_message_not_deleted_ongoing_invitations));
}
- if (result.hasNotFullyDownloaded()) {
- fails.add(getString(
- R.string.dialog_message_not_deleted_partly_downloaded));
- }
// add problems the user can resolve
if (result.hasNotAllIntroductionSelected() &&
result.hasNotAllInvitationSelected()) {
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 698320fc5..67803bff2 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -200,7 +200,6 @@
Messages related to ongoing invitations and introductions cannot be deleted until they conclude.
Messages related to ongoing introductions cannot be deleted until they conclude.
Messages related to ongoing invitations cannot be deleted until they conclude.
- Partly downloaded messages cannot be deleted until they have finished downloading.
To delete an invitation or introduction, you need to select the request and the response.
To delete an introduction, you need to select the request and the response.
To delete an invitation, you need to select the request and the response.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java
index 45c2ecb56..d0e624934 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java
@@ -61,8 +61,11 @@ public interface MessageTracker {
/**
* Marks a message as read or unread and updates the group count.
+ *
+ * @return True if the message was previously marked as read
*/
- void setReadFlag(GroupId g, MessageId m, boolean read) throws DbException;
+ boolean setReadFlag(Transaction txn, GroupId g, MessageId m, boolean read)
+ throws DbException;
/**
* Resets the {@link GroupCount} to the given msgCount and unreadCount.
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 a8fae64a5..063c8d947 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
@@ -21,7 +21,6 @@ public interface ConversationManager {
int DELETE_SESSION_INVITATION_INCOMPLETE = 1 << 1;
int DELETE_SESSION_INTRODUCTION_IN_PROGRESS = 1 << 2;
int DELETE_SESSION_INVITATION_IN_PROGRESS = 1 << 3;
- int DELETE_NOT_DOWNLOADED = 1 << 4;
/**
* Clients that present messages in a private conversation need to
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/conversation/DeletionResult.java b/briar-api/src/main/java/org/briarproject/briar/api/conversation/DeletionResult.java
index fbaf362e8..b8c7b20af 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/conversation/DeletionResult.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/conversation/DeletionResult.java
@@ -4,7 +4,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.NotThreadSafe;
-import static org.briarproject.briar.api.conversation.ConversationManager.DELETE_NOT_DOWNLOADED;
import static org.briarproject.briar.api.conversation.ConversationManager.DELETE_SESSION_INTRODUCTION_INCOMPLETE;
import static org.briarproject.briar.api.conversation.ConversationManager.DELETE_SESSION_INTRODUCTION_IN_PROGRESS;
import static org.briarproject.briar.api.conversation.ConversationManager.DELETE_SESSION_INVITATION_INCOMPLETE;
@@ -36,10 +35,6 @@ public class DeletionResult {
result |= DELETE_SESSION_INTRODUCTION_IN_PROGRESS;
}
- public void addNotFullyDownloaded() {
- result |= DELETE_NOT_DOWNLOADED;
- }
-
public boolean allDeleted() {
return result == 0;
}
@@ -59,9 +54,4 @@ public class DeletionResult {
public boolean hasNotAllInvitationSelected() {
return (result & DELETE_SESSION_INVITATION_INCOMPLETE) != 0;
}
-
- public boolean hasNotFullyDownloaded() {
- return (result & DELETE_NOT_DOWNLOADED) != 0;
- }
-
}
diff --git a/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java
index 4ba35c020..7cebf96b5 100644
--- a/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/client/ConversationClientImpl.java
@@ -42,6 +42,7 @@ public abstract class ConversationClientImpl extends BdfIncomingMessageHook
@Override
public void setReadFlag(GroupId g, MessageId m, boolean read)
throws DbException {
- messageTracker.setReadFlag(g, m, read);
+ db.transaction(false, txn ->
+ messageTracker.setReadFlag(txn, g, m, read));
}
}
diff --git a/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java
index 08a94985a..4c7c65030 100644
--- a/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java
@@ -138,9 +138,8 @@ class MessageTrackerImpl implements MessageTracker {
}
@Override
- public void setReadFlag(GroupId g, MessageId m, boolean read)
- throws DbException {
- Transaction txn = db.startTransaction(false);
+ public boolean setReadFlag(Transaction txn, GroupId g, MessageId m,
+ boolean read) throws DbException {
try {
// check current read status of message
BdfDictionary old =
@@ -161,11 +160,9 @@ class MessageTrackerImpl implements MessageTracker {
storeGroupCount(txn, g, new GroupCount(c.getMsgCount(),
unreadCount, c.getLatestMsgTime()));
}
- db.commitTransaction(txn);
+ return wasRead;
} catch (FormatException e) {
throw new DbException(e);
- } finally {
- db.endTransaction(txn);
}
}
diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
index 6dacf6c4d..79c3d51d6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java
@@ -263,7 +263,8 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
@Override
public void setReadFlag(GroupId g, MessageId m, boolean read)
throws DbException {
- messageTracker.setReadFlag(g, m, read);
+ db.transaction(false, txn ->
+ messageTracker.setReadFlag(txn, g, m, read));
}
private Forum parseForum(Group g) throws FormatException {
diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java
index 88d0a4b5a..73a387a35 100644
--- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java
@@ -1,5 +1,7 @@
package org.briarproject.briar.messaging;
+import static java.util.concurrent.TimeUnit.DAYS;
+
interface MessagingConstants {
// Metadata keys for messages
@@ -9,4 +11,10 @@ interface MessagingConstants {
String MSG_KEY_HAS_TEXT = "hasText";
String MSG_KEY_ATTACHMENT_HEADERS = "attachmentHeaders";
String MSG_KEY_AUTO_DELETE_TIMER = "autoDeleteTimer";
+
+ /**
+ * How long to keep incoming attachments that aren't listed by any private
+ * message before deleting them.
+ */
+ long MISSING_ATTACHMENT_CLEANUP_DURATION_MS = DAYS.toMillis(28);
}
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 45ff75687..6cf238249 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
@@ -1,6 +1,7 @@
package org.briarproject.briar.messaging;
import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.cleanup.CleanupHook;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact;
@@ -50,6 +51,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.concurrent.Immutable;
@@ -58,7 +60,6 @@ import javax.inject.Inject;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
-import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.briar.api.attachment.MediaConstants.MSG_KEY_CONTENT_TYPE;
import static org.briarproject.briar.api.attachment.MediaConstants.MSG_KEY_DESCRIPTOR_LENGTH;
@@ -69,6 +70,7 @@ import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONL
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
import static org.briarproject.briar.messaging.MessageTypes.ATTACHMENT;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
+import static org.briarproject.briar.messaging.MessagingConstants.MISSING_ATTACHMENT_CLEANUP_DURATION_MS;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_ATTACHMENT_HEADERS;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_AUTO_DELETE_TIMER;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT;
@@ -80,7 +82,7 @@ import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_TIMEST
@NotNullByDefault
class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
ConversationClient, OpenDatabaseHook, ContactHook,
- ClientVersioningHook {
+ ClientVersioningHook, CleanupHook {
private final DatabaseComponent db;
private final ClientHelper clientHelper;
@@ -119,7 +121,10 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
@Override
public void setReadFlag(GroupId g, MessageId m, boolean read)
throws DbException {
- messageTracker.setReadFlag(g, m, read);
+ db.transaction(false, txn -> {
+ boolean wasRead = messageTracker.setReadFlag(txn, g, m, read);
+ if (read && !wasRead) db.startCleanupTimer(txn, m);
+ });
}
@Override
@@ -210,8 +215,12 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
new PrivateMessageReceivedEvent(header, contactId);
txn.attach(event);
messageTracker.trackIncomingMessage(txn, m);
+ if (timer != NO_AUTO_DELETE_TIMER) {
+ db.setCleanupTimerDuration(txn, m.getId(), timer);
+ }
autoDeleteManager.receiveAutoDeleteTimer(txn, contactId, timer,
timestamp);
+ if (!headers.isEmpty()) stopAttachmentCleanupTimers(txn, m, headers);
}
private List parseAttachmentHeaders(GroupId g,
@@ -228,10 +237,50 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
return headers;
}
+ private void stopAttachmentCleanupTimers(Transaction txn, Message m,
+ List headers)
+ throws DbException, FormatException {
+ // Fetch the IDs of all remote attachments
+ BdfDictionary query = BdfDictionary.of(
+ new BdfEntry(MSG_KEY_MSG_TYPE, ATTACHMENT),
+ new BdfEntry(MSG_KEY_LOCAL, false));
+ Collection results =
+ clientHelper.getMessageIds(txn, m.getGroupId(), query);
+ // Stop the cleanup timers of any attachments that have already
+ // been delivered
+ for (AttachmentHeader h : headers) {
+ MessageId id = h.getMessageId();
+ if (results.contains(id)) db.stopCleanupTimer(txn, id);
+ }
+ }
+
private void incomingAttachment(Transaction txn, Message m)
throws DbException {
ContactId contactId = getContactId(txn, m.getGroupId());
txn.attach(new AttachmentReceivedEvent(m.getId(), contactId));
+ // If no private messages that list this attachment have been
+ // delivered, start the cleanup timer. It will be stopped when a
+ // private message that lists this attachment is delivered
+ BdfDictionary query = BdfDictionary.of(
+ new BdfEntry(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE),
+ new BdfEntry(MSG_KEY_LOCAL, false));
+ try {
+ Map results = clientHelper
+ .getMessageMetadataAsDictionary(txn, m.getGroupId(), query);
+ for (BdfDictionary meta : results.values()) {
+ List headers =
+ parseAttachmentHeaders(m.getGroupId(), meta);
+ for (AttachmentHeader h : headers) {
+ if (h.getMessageId().equals(m.getId())) return;
+ }
+ }
+ // No private messages list this attachment - start the timer
+ db.setCleanupTimerDuration(txn, m.getId(),
+ MISSING_ATTACHMENT_CLEANUP_DURATION_MS);
+ db.startCleanupTimer(txn, m.getId());
+ } catch (FormatException e) {
+ throw new DbException(e);
+ }
}
@Override
@@ -268,8 +317,12 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
db.setMessageShared(txn, a.getMessageId());
db.setMessagePermanent(txn, a.getMessageId());
}
+ long timer = m.getAutoDeleteTimer();
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true,
false);
+ if (timer != NO_AUTO_DELETE_TIMER) {
+ db.setCleanupTimerDuration(txn, m.getMessage().getId(), timer);
+ }
messageTracker.trackOutgoingMessage(txn, m.getMessage());
} catch (FormatException e) {
throw new AssertionError(e);
@@ -395,8 +448,7 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
try {
Map messages =
clientHelper.getMessageMetadataAsDictionary(txn, g);
- for (Map.Entry entry : messages
- .entrySet()) {
+ for (Entry entry : messages.entrySet()) {
Long type = entry.getValue().getOptionalLong(MSG_KEY_MSG_TYPE);
if (type == null || type == PRIVATE_MESSAGE)
result.add(entry.getKey());
@@ -445,46 +497,55 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
@Override
public DeletionResult deleteMessages(Transaction txn, ContactId c,
Set messageIds) throws DbException {
- DeletionResult result = new DeletionResult();
GroupId g = getContactGroup(db.getContact(txn, c)).getId();
- for (MessageId m : messageIds) {
- // get attachment headers
- List headers;
- try {
- BdfDictionary meta =
- clientHelper.getMessageMetadataAsDictionary(txn, m);
- Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE);
- if (messageType != null && messageType != PRIVATE_MESSAGE)
- throw new AssertionError("not supported");
- headers = messageType == null ? emptyList() :
- parseAttachmentHeaders(g, meta);
- } catch (FormatException e) {
- throw new DbException(e);
- }
- // check if all attachments have been delivered
- boolean allAttachmentsDelivered = true;
- try {
- for (AttachmentHeader h : headers) {
- if (db.getMessageState(txn, h.getMessageId()) != DELIVERED)
- throw new NoSuchMessageException();
- }
- } catch (NoSuchMessageException e) {
- allAttachmentsDelivered = false;
- }
- // delete messages, if all attachments were delivered
- if (allAttachmentsDelivered) {
- for (AttachmentHeader h : headers) {
- db.deleteMessage(txn, h.getMessageId());
- db.deleteMessageMetadata(txn, h.getMessageId());
- }
- db.deleteMessage(txn, m);
- db.deleteMessageMetadata(txn, m);
- } else {
- result.addNotFullyDownloaded();
- }
- }
+ for (MessageId m : messageIds) deleteMessage(txn, g, m);
recalculateGroupCount(txn, g);
- return result;
+ return new DeletionResult();
+ }
+
+ private List getAttachmentHeaders(Transaction txn,
+ MessageId m, GroupId g) throws DbException {
+ try {
+ BdfDictionary meta =
+ clientHelper.getMessageMetadataAsDictionary(txn, m);
+ Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE);
+ if (messageType != null && messageType != PRIVATE_MESSAGE)
+ throw new IllegalArgumentException();
+ return messageType == null ? emptyList() :
+ parseAttachmentHeaders(g, meta);
+ } catch (FormatException e) {
+ throw new DbException(e);
+ }
+ }
+
+ @Override
+ public void deleteMessages(Transaction txn, GroupId g,
+ Collection messageIds) throws DbException {
+ for (MessageId m : messageIds) deleteMessage(txn, g, m);
+ recalculateGroupCount(txn, g);
+ }
+
+ private void deleteMessage(Transaction txn, GroupId g, MessageId m)
+ throws DbException {
+ try {
+ BdfDictionary meta =
+ clientHelper.getMessageMetadataAsDictionary(txn, m);
+ Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE);
+ if (messageType != null && messageType == PRIVATE_MESSAGE) {
+ for (AttachmentHeader h : getAttachmentHeaders(txn, m, g)) {
+ try {
+ db.deleteMessage(txn, h.getMessageId());
+ db.deleteMessageMetadata(txn, h.getMessageId());
+ } catch (NoSuchMessageException e) {
+ // Continue
+ }
+ }
+ }
+ db.deleteMessage(txn, m);
+ db.deleteMessageMetadata(txn, m);
+ } catch (FormatException e) {
+ throw new DbException(e);
+ }
}
private void recalculateGroupCount(Transaction txn, GroupId g)
@@ -500,7 +561,7 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
}
int msgCount = results.size();
int unreadCount = 0;
- for (Map.Entry entry : results.entrySet()) {
+ for (Entry entry : results.entrySet()) {
BdfDictionary meta = entry.getValue();
boolean read;
try {
@@ -512,5 +573,4 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
}
messageTracker.resetGroupCount(txn, g, msgCount, unreadCount);
}
-
}
diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java
index 4f795d2cc..a36c8452e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingModule.java
@@ -1,5 +1,6 @@
package org.briarproject.briar.messaging;
+import org.briarproject.bramble.api.cleanup.CleanupManager;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.MetadataEncoder;
@@ -55,6 +56,7 @@ public class MessagingModule {
ContactManager contactManager, ValidationManager validationManager,
ConversationManager conversationManager,
ClientVersioningManager clientVersioningManager,
+ CleanupManager cleanupManager,
MessagingManagerImpl messagingManager) {
lifecycleManager.registerOpenDatabaseHook(messagingManager);
contactManager.registerContactHook(messagingManager);
@@ -63,6 +65,8 @@ public class MessagingModule {
conversationManager.registerConversationClient(messagingManager);
clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
MINOR_VERSION, messagingManager);
+ cleanupManager.registerCleanupHook(CLIENT_ID, MAJOR_VERSION,
+ messagingManager);
return messagingManager;
}
}
diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
index 4f0a101f5..5093963fe 100644
--- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java
@@ -478,7 +478,8 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
@Override
public void setReadFlag(GroupId g, MessageId m, boolean read)
throws DbException {
- messageTracker.setReadFlag(g, m, read);
+ db.transaction(false, txn ->
+ messageTracker.setReadFlag(txn, g, m, read));
}
@Override
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java
index 163c445d1..d8507685a 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java
@@ -1,11 +1,15 @@
package org.briarproject.briar.messaging;
+import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
+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.system.TimeTravelModule;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
+import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
@@ -15,17 +19,27 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory;
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.ArrayList;
import java.util.Collection;
import java.util.List;
import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
import static java.util.Collections.sort;
+import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS;
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
+import static org.briarproject.briar.messaging.MessagingConstants.MISSING_ATTACHMENT_CLEANUP_DURATION_MS;
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 AutoDeleteIntegrationTest
extends BriarIntegrationTest {
@@ -39,18 +53,42 @@ public class AutoDeleteIntegrationTest
c0 = DaggerBriarIntegrationTestComponent.builder()
.testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir))
+ .timeTravelModule(new TimeTravelModule(true))
.build();
BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0);
c1 = DaggerBriarIntegrationTestComponent.builder()
.testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir))
+ .timeTravelModule(new TimeTravelModule(true))
.build();
BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1);
c2 = DaggerBriarIntegrationTestComponent.builder()
.testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir))
+ .timeTravelModule(new TimeTravelModule(true))
.build();
BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2);
+
+ // Use different times to avoid creating identical messages that are
+ // treated as redundant copies of the same message (#1907)
+ try {
+ long now = System.currentTimeMillis();
+ c0.getTimeTravel().setCurrentTimeMillis(now);
+ c1.getTimeTravel().setCurrentTimeMillis(now + 1);
+ c2.getTimeTravel().setCurrentTimeMillis(now + 2);
+ } catch (InterruptedException e) {
+ fail();
+ }
+ }
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ // Run the initial cleanup task that was scheduled at startup
+ c0.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS);
+ c1.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS);
+ c2.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS);
}
@Test
@@ -91,6 +129,8 @@ public class AutoDeleteIntegrationTest
assertEquals(NO_AUTO_DELETE_TIMER, h0.getAutoDeleteTimer());
// Sync the message to 1
sync0To1(1, true);
+ // Sync the ack to 0
+ ack1To0(1);
// The message should have been added to 1's view of the conversation
List headers1 =
getMessageHeaders(c1, contactId0From1);
@@ -106,6 +146,78 @@ public class AutoDeleteIntegrationTest
getAutoDeleteTimer(c1, contactId0From1));
}
+ @Test
+ public void testNonDefaultTimer() throws Exception {
+ // Set 0's timer
+ setAutoDeleteTimer(c0, contactId1From0, MIN_AUTO_DELETE_TIMER_MS);
+ // 0 should be using the new timer
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ getAutoDeleteTimer(c0, contactId1From0));
+ // 1 should still be using the default timer
+ assertEquals(NO_AUTO_DELETE_TIMER,
+ getAutoDeleteTimer(c1, contactId0From1));
+ // 0 creates a message with the new timer
+ MessageId messageId = createMessageWithTimer(c0, contactId1From0);
+ // The message should have been added to 0's view of the conversation
+ List headers0 =
+ getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertEquals(messageId, headers0.get(0).getId());
+ // The message should have the new timer
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ headers0.get(0).getAutoDeleteTimer());
+ // Sync the message to 1
+ sync0To1(1, true);
+ // Sync the ack to 0 - this starts 0's timer
+ ack1To0(1);
+ // The message should have been added to 1's view of the conversation
+ List headers1 =
+ getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertEquals(messageId, headers1.get(0).getId());
+ // The message should have the new timer
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ headers1.get(0).getAutoDeleteTimer());
+ // Both peers should be using the new timer
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ getAutoDeleteTimer(c0, contactId1From0));
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ getAutoDeleteTimer(c1, contactId0From1));
+ // Before 0's timer elapses, both peers should still see the message
+ long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation but 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // 1 marks the message as read - this starts 1's timer
+ markMessageRead(c1, contact0From1, messageId);
+ // Before 1's timer elapses, 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ }
+
@Test
public void testTimerIsMirrored() throws Exception {
// Set 0's timer
@@ -122,50 +234,450 @@ public class AutoDeleteIntegrationTest
List headers0 =
getMessageHeaders(c0, contactId1From0);
assertEquals(1, headers0.size());
- ConversationMessageHeader h0 = headers0.get(0);
- assertEquals(messageId0, h0.getId());
+ assertEquals(messageId0, headers0.get(0).getId());
// The message should have the new timer
- assertEquals(MIN_AUTO_DELETE_TIMER_MS, h0.getAutoDeleteTimer());
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ headers0.get(0).getAutoDeleteTimer());
// Sync the message to 1
sync0To1(1, true);
+ // Sync the ack to 0 - this starts 0's timer
+ ack1To0(1);
// The message should have been added to 1's view of the conversation
List headers1 =
getMessageHeaders(c1, contactId0From1);
assertEquals(1, headers1.size());
- ConversationMessageHeader h1 = headers1.get(0);
- assertEquals(messageId0, h1.getId());
+ assertEquals(messageId0, headers1.get(0).getId());
// The message should have the new timer
- assertEquals(MIN_AUTO_DELETE_TIMER_MS, h1.getAutoDeleteTimer());
+ assertEquals(MIN_AUTO_DELETE_TIMER_MS,
+ headers1.get(0).getAutoDeleteTimer());
// 0 and 1 should both be using the new timer
assertEquals(MIN_AUTO_DELETE_TIMER_MS,
getAutoDeleteTimer(c0, contactId1From0));
assertEquals(MIN_AUTO_DELETE_TIMER_MS,
getAutoDeleteTimer(c1, contactId0From1));
+ // Before 0's timer elapses, both peers should still see the message
+ long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation but 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // 1 marks the message as read - this starts 1's timer
+ markMessageRead(c1, contact0From1, messageId0);
+ // Before 1's timer elapses, 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
// 1 creates a message
MessageId messageId1 = createMessageWithTimer(c1, contactId0From1);
// The message should have been added to 1's view of the conversation
headers1 = getMessageHeaders(c1, contactId0From1);
- assertEquals(2, headers1.size());
- assertEquals(messageId0, headers1.get(0).getId());
- assertEquals(messageId1, headers1.get(1).getId());
+ assertEquals(1, headers1.size());
+ assertEquals(messageId1, headers1.get(0).getId());
// The message should have the new timer
assertEquals(MIN_AUTO_DELETE_TIMER_MS,
- headers1.get(1).getAutoDeleteTimer());
+ headers1.get(0).getAutoDeleteTimer());
// Sync the message to 0
sync1To0(1, true);
+ // Sync the ack to 1 - this starts 1's timer
+ ack0To1(1);
// The message should have been added to 0's view of the conversation
headers0 = getMessageHeaders(c0, contactId1From0);
- assertEquals(2, headers0.size());
- assertEquals(messageId0, headers0.get(0).getId());
- assertEquals(messageId1, headers0.get(1).getId());
+ assertEquals(1, headers0.size());
+ assertEquals(messageId1, headers0.get(0).getId());
// The message should have the new timer
assertEquals(MIN_AUTO_DELETE_TIMER_MS,
- headers0.get(1).getAutoDeleteTimer());
+ headers0.get(0).getAutoDeleteTimer());
// 0 and 1 should both be using the new timer
assertEquals(MIN_AUTO_DELETE_TIMER_MS,
getAutoDeleteTimer(c0, contactId1From0));
assertEquals(MIN_AUTO_DELETE_TIMER_MS,
getAutoDeleteTimer(c1, contactId0From1));
+ // Before 1's timer elapses, both peers should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation but 0 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ // 0 marks the message as read - this starts 0's timer
+ markMessageRead(c0, contact1From0, messageId1);
+ // Before 0's timer elapses, 0 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ }
+
+ @Test
+ public void testMessageWithAttachment() throws Exception {
+ // Set 0's timer
+ setAutoDeleteTimer(c0, contactId1From0, MIN_AUTO_DELETE_TIMER_MS);
+ // 0 creates an attachment
+ AttachmentHeader attachmentHeader =
+ createAttachment(c0, contactId1From0);
+ // 0 creates a message with the new timer and the attachment
+ MessageId messageId = createMessageWithTimer(c0, contactId1From0,
+ singletonList(attachmentHeader));
+ // The message should have been added to 0's view of the conversation
+ List headers0 =
+ getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertEquals(messageId, headers0.get(0).getId());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ // Sync the message and the attachment to 1
+ sync0To1(2, true);
+ // Sync the acks to 0 - this starts 0's timer
+ ack1To0(2);
+ // The message should have been added to 1's view of the conversation
+ List headers1 =
+ getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertEquals(messageId, headers1.get(0).getId());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // Before 0's timer elapses, both peers should still see the message
+ // and both should have the attachment
+ long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation but 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // 1 marks the message as read - this starts 1's timer
+ markMessageRead(c1, contact0From1, messageId);
+ // Before 1's timer elapses, 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ }
+
+ @Test
+ public void testPrivateMessageWithMissingAttachmentIsDeleted()
+ throws Exception {
+ // Set 0's timer
+ setAutoDeleteTimer(c0, contactId1From0, MIN_AUTO_DELETE_TIMER_MS);
+ // 0 creates an attachment
+ AttachmentHeader attachmentHeader =
+ createAttachment(c0, contactId1From0);
+ // 0 creates a message with the new timer and the attachment
+ MessageId messageId = createMessageWithTimer(c0, contactId1From0,
+ singletonList(attachmentHeader));
+ // The message should have been added to 0's view of the conversation
+ List headers0 =
+ getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertEquals(messageId, headers0.get(0).getId());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ // Unshare the attachment so it won't be synced yet
+ setMessageNotShared(c0, attachmentHeader.getMessageId());
+ // Sync the message (but not the attachment) to 1
+ sync0To1(1, true);
+ // Sync the ack to 0 - this starts 0's timer
+ ack1To0(1);
+ // The message should have been added to 1's view of the conversation
+ List headers1 =
+ getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertEquals(messageId, headers1.get(0).getId());
+ // Before 0's timer elapses, both peers should still see the message
+ // and 0 should still have the attachment (1 hasn't received it)
+ long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation but 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // 1 marks the message as read - this starts 1's timer
+ markMessageRead(c1, contact0From1, messageId);
+ // Before 1's timer elapses, 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ }
+
+ @Test
+ public void testOrphanedAttachmentIsDeleted() throws Exception {
+ // Set 0's timer
+ setAutoDeleteTimer(c0, contactId1From0, MIN_AUTO_DELETE_TIMER_MS);
+ // 0 creates an attachment
+ AttachmentHeader attachmentHeader =
+ createAttachment(c0, contactId1From0);
+ // 0 creates a message with the new timer and the attachment
+ MessageId messageId = createMessageWithTimer(c0, contactId1From0,
+ singletonList(attachmentHeader));
+ // The message should have been added to 0's view of the conversation
+ List headers0 =
+ getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertEquals(messageId, headers0.get(0).getId());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ // Unshare the private message so it won't be synced yet
+ setMessageNotShared(c0, messageId);
+ // Sync the attachment (but not the message) to 1 - this starts 1's
+ // orphan cleanup timer
+ sync0To1(1, true);
+ // Sync the ack to 0
+ ack1To0(1);
+ // The message should not have been added to 1's view of the
+ // conversation
+ List headers1 =
+ getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // Before 1's timer elapses, both peers should still have the attachment
+ long timerLatency =
+ MISSING_ATTACHMENT_CLEANUP_DURATION_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 1's timer has elapsed, 1 should no longer have the attachment
+ // but 0 should still have it
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // Share the private message and sync it - too late to stop 1's orphan
+ // cleanup timer
+ setMessageShared(c0, messageId);
+ sync0To1(1, true);
+ // Sync the ack to 0 - this starts 0's timer
+ ack1To0(1);
+ // The message should have been added to 1's view of the conversation
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // Before 0's timer elapses, both peers should still see the message
+ // and 0 should still have the attachment (1 has deleted it)
+ timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation but 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // 1 marks the message as read - this starts 1's timer
+ markMessageRead(c1, contact0From1, messageId);
+ // Before 1's timer elapses, 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ }
+
+ @Test
+ public void testOrphanedAttachmentIsNotDeletedIfPrivateMessageArrives()
+ throws Exception {
+ // Set 0's timer
+ setAutoDeleteTimer(c0, contactId1From0, MIN_AUTO_DELETE_TIMER_MS);
+ // 0 creates an attachment
+ AttachmentHeader attachmentHeader =
+ createAttachment(c0, contactId1From0);
+ // 0 creates a message with the new timer and the attachment
+ MessageId messageId = createMessageWithTimer(c0, contactId1From0,
+ singletonList(attachmentHeader));
+ // The message should have been added to 0's view of the conversation
+ List headers0 =
+ getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertEquals(messageId, headers0.get(0).getId());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ // Unshare the private message so it won't be synced yet
+ setMessageNotShared(c0, messageId);
+ // Sync the attachment (but not the message) to 1 - this starts 1's
+ // orphan cleanup timer
+ sync0To1(1, true);
+ // Sync the ack to 0
+ ack1To0(1);
+ // The message should not have been added to 1's view of the
+ // conversation
+ List headers1 =
+ getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ // Before 1's timer elapses, both peers should still have the attachment
+ long timerLatency =
+ MISSING_ATTACHMENT_CLEANUP_DURATION_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // Share the private message and sync it - just in time to stop 1's
+ // orphan cleanup timer
+ setMessageShared(c0, messageId);
+ sync0To1(1, true);
+ // The message should have been added to 1's view of the conversation
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 1's timer has elapsed, both peers should still see the message
+ // and both should still have the attachment
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // Sync the ack to 0 - this starts 0's timer
+ ack1To0(1);
+ // Before 0's timer elapses, both peers should still see the message
+ // and both should still have the attachment
+ timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(1, headers0.size());
+ assertFalse(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 0's timer has elapsed, the message should be deleted from 0's
+ // view of the conversation but 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // 1 marks the message as read - this starts 1's timer
+ markMessageRead(c1, contact0From1, messageId);
+ // Before 1's timer elapses, 1 should still see the message
+ c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(1, headers1.size());
+ assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId()));
+ // When 1's timer has elapsed, the message should be deleted from 1's
+ // view of the conversation
+ c0.getTimeTravel().addCurrentTimeMillis(1);
+ c1.getTimeTravel().addCurrentTimeMillis(1);
+ headers0 = getMessageHeaders(c0, contactId1From0);
+ assertEquals(0, headers0.size());
+ assertTrue(messageIsDeleted(c0, attachmentHeader.getMessageId()));
+ headers1 = getMessageHeaders(c1, contactId0From1);
+ assertEquals(0, headers1.size());
+ assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId()));
}
private MessageId createMessageWithoutTimer(
@@ -191,6 +703,12 @@ public class AutoDeleteIntegrationTest
private MessageId createMessageWithTimer(
BriarIntegrationTestComponent component, ContactId contactId)
throws Exception {
+ return createMessageWithTimer(component, contactId, emptyList());
+ }
+
+ private MessageId createMessageWithTimer(
+ BriarIntegrationTestComponent component, ContactId contactId,
+ List attachmentHeaders) throws Exception {
DatabaseComponent db = component.getDatabaseComponent();
ConversationManager conversationManager =
component.getConversationManager();
@@ -205,12 +723,37 @@ public class AutoDeleteIntegrationTest
long timer = autoDeleteManager
.getAutoDeleteTimer(txn, contactId, timestamp);
PrivateMessage m = factory.createPrivateMessage(groupId, timestamp,
- "Hi!", emptyList(), timer);
+ "Hi!", attachmentHeaders, timer);
messagingManager.addLocalMessage(txn, m);
return m.getMessage().getId();
});
}
+ private AttachmentHeader createAttachment(
+ BriarIntegrationTestComponent component, ContactId contactId)
+ throws Exception {
+ MessagingManager messagingManager = component.getMessagingManager();
+
+ GroupId groupId = messagingManager.getConversationId(contactId);
+ InputStream in = new ByteArrayInputStream(getRandomBytes(1234));
+ return messagingManager.addLocalAttachment(groupId,
+ component.getClock().currentTimeMillis(), "image/jpeg", in);
+ }
+
+ private void setMessageNotShared(BriarIntegrationTestComponent component,
+ MessageId messageId) throws Exception {
+ DatabaseComponent db = component.getDatabaseComponent();
+
+ db.transaction(false, txn -> db.setMessageNotShared(txn, messageId));
+ }
+
+ private void setMessageShared(BriarIntegrationTestComponent component,
+ MessageId messageId) throws Exception {
+ DatabaseComponent db = component.getDatabaseComponent();
+
+ db.transaction(false, txn -> db.setMessageShared(txn, messageId));
+ }
+
private List getMessageHeaders(
BriarIntegrationTestComponent component, ContactId contactId)
throws Exception {
@@ -230,6 +773,26 @@ public class AutoDeleteIntegrationTest
txn -> autoDeleteManager.getAutoDeleteTimer(txn, contactId));
}
+ private void markMessageRead(BriarIntegrationTestComponent component,
+ Contact contact, MessageId messageId) throws DbException {
+ MessagingManager messagingManager = component.getMessagingManager();
+
+ GroupId groupId = messagingManager.getContactGroup(contact).getId();
+ messagingManager.setReadFlag(groupId, messageId, true);
+ }
+
+ private boolean messageIsDeleted(BriarIntegrationTestComponent component,
+ MessageId messageId) throws DbException {
+ DatabaseComponent db = component.getDatabaseComponent();
+
+ try {
+ db.transaction(true, txn -> db.getMessage(txn, messageId));
+ return false;
+ } catch (MessageDeletedException e) {
+ return true;
+ }
+ }
+
@SuppressWarnings({"UseCompareMethod", "Java8ListSort"}) // Animal Sniffer
private List sortHeaders(
Collection in) {
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
index 2c534363b..f93596047 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java
@@ -8,7 +8,6 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
-import org.briarproject.briar.api.conversation.DeletionResult;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
@@ -30,10 +29,7 @@ import javax.annotation.Nullable;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
-import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
-import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
-import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
@@ -310,56 +306,6 @@ public class MessagingManagerIntegrationTest
}
}
- @Test
- public void testDeleteSomeAttachment() throws Exception {
- // send one message with attachment
- AttachmentHeader h = addAttachment(c0);
- PrivateMessage m =
- sendMessage(c0, c1, getRandomString(42), singletonList(h));
-
- // attachment exists on both devices, state set to PENDING for receiver
- db1.transaction(false, txn -> {
- db1.getMessage(txn, h.getMessageId());
- db1.setMessageState(txn, h.getMessageId(), PENDING);
- });
-
- // deleting succeeds for sender
- Set toDelete = singleton(m.getMessage().getId());
- DeletionResult result0 = db0.transactionWithResult(false, txn ->
- messagingManager0.deleteMessages(txn, contactId, toDelete));
- assertTrue(result0.allDeleted());
-
- // deleting message fails for receiver,
- // because attachment is not yet delivered
- DeletionResult result1 = db1.transactionWithResult(false, txn ->
- messagingManager1.deleteMessages(txn, contactId, toDelete));
- assertFalse(result1.allDeleted());
- assertTrue(result1.hasNotFullyDownloaded());
-
- // deliver attachment
- db1.transaction(false,
- txn -> db1.setMessageState(txn, h.getMessageId(), DELIVERED));
-
- // deleting message and attachment works for sender now
- assertTrue(db1.transactionWithResult(false, txn ->
- messagingManager1.deleteMessages(txn, contactId, toDelete))
- .allDeleted());
-
- // 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 ->
diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt
index 487bcdbb0..caa10209b 100644
--- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt
+++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputConversationMessage.kt
@@ -53,8 +53,7 @@ internal fun DeletionResult.output() = JsonDict(
"hasIntroductionSessionInProgress" to hasIntroductionSessionInProgress(),
"hasInvitationSessionInProgress" to hasInvitationSessionInProgress(),
"hasNotAllIntroductionSelected" to hasNotAllIntroductionSelected(),
- "hasNotAllInvitationSelected" to hasNotAllInvitationSelected(),
- "hasNotFullyDownloaded" to hasNotFullyDownloaded()
+ "hasNotAllInvitationSelected" to hasNotAllInvitationSelected()
)
internal fun MessagesAckedEvent.output() = JsonDict(
diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt
index d58044cad..3dde2510b 100644
--- a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt
+++ b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt
@@ -386,15 +386,13 @@ internal class MessagingControllerImplTest : ControllerTest() {
if (Random.nextBoolean()) result.addInvitationSessionInProgress()
if (Random.nextBoolean()) result.addIntroductionNotAllSelected()
if (Random.nextBoolean()) result.addIntroductionSessionInProgress()
- if (Random.nextBoolean()) result.addNotFullyDownloaded()
val json = """
{
"allDeleted": ${result.allDeleted()},
"hasIntroductionSessionInProgress": ${result.hasIntroductionSessionInProgress()},
"hasInvitationSessionInProgress": ${result.hasInvitationSessionInProgress()},
"hasNotAllIntroductionSelected": ${result.hasNotAllIntroductionSelected()},
- "hasNotAllInvitationSelected": ${result.hasNotAllInvitationSelected()},
- "hasNotFullyDownloaded": ${result.hasNotFullyDownloaded()}
+ "hasNotAllInvitationSelected": ${result.hasNotAllInvitationSelected()}
}
"""
assertJsonEquals(json, result.output())