Delete private messages when their timers expire (needs UI support).

This commit is contained in:
akwizgran
2021-02-25 15:56:29 +00:00
committed by Torsten Grote
parent c89bde08db
commit 240e619248
16 changed files with 710 additions and 145 deletions

View File

@@ -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()) {

View File

@@ -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>

View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 ->

View File

@@ -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(

View File

@@ -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())