mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Delete private messages when their timers expire (needs UI support).
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -200,7 +200,6 @@
|
||||
<string name="dialog_message_not_deleted_ongoing_both">Messages related to ongoing invitations and introductions cannot be deleted until they conclude.</string>
|
||||
<string name="dialog_message_not_deleted_ongoing_introductions">Messages related to ongoing introductions cannot be deleted until they conclude.</string>
|
||||
<string name="dialog_message_not_deleted_ongoing_invitations">Messages related to ongoing invitations cannot be deleted until they conclude.</string>
|
||||
<string name="dialog_message_not_deleted_partly_downloaded">Partly downloaded messages cannot be deleted until they have finished downloading.</string>
|
||||
<string name="dialog_message_not_deleted_not_all_selected_both">To delete an invitation or introduction, you need to select the request and the response.</string>
|
||||
<string name="dialog_message_not_deleted_not_all_selected_introductions">To delete an introduction, you need to select the request and the response.</string>
|
||||
<string name="dialog_message_not_deleted_not_all_selected_invitations">To delete an invitation, you need to select the request and the response.</string>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<AttachmentHeader> parseAttachmentHeaders(GroupId g,
|
||||
@@ -228,10 +237,50 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
|
||||
return headers;
|
||||
}
|
||||
|
||||
private void stopAttachmentCleanupTimers(Transaction txn, Message m,
|
||||
List<AttachmentHeader> 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<MessageId> 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<MessageId, BdfDictionary> results = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, m.getGroupId(), query);
|
||||
for (BdfDictionary meta : results.values()) {
|
||||
List<AttachmentHeader> 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<MessageId, BdfDictionary> messages =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
for (Map.Entry<MessageId, BdfDictionary> entry : messages
|
||||
.entrySet()) {
|
||||
for (Entry<MessageId, BdfDictionary> 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<MessageId> messageIds) throws DbException {
|
||||
DeletionResult result = new DeletionResult();
|
||||
GroupId g = getContactGroup(db.getContact(txn, c)).getId();
|
||||
for (MessageId m : messageIds) {
|
||||
// get attachment headers
|
||||
List<AttachmentHeader> 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<AttachmentHeader> 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<MessageId> 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<MessageId, BdfDictionary> entry : results.entrySet()) {
|
||||
for (Entry<MessageId, BdfDictionary> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<BriarIntegrationTestComponent> {
|
||||
@@ -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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> 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<AttachmentHeader> 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<ConversationMessageHeader> 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<ConversationMessageHeader> sortHeaders(
|
||||
Collection<ConversationMessageHeader> in) {
|
||||
|
||||
@@ -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<MessageId> 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 ->
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user