diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/autodelete/AutoDeleteConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/autodelete/AutoDeleteConstants.java new file mode 100644 index 000000000..ca1aaaf8e --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/autodelete/AutoDeleteConstants.java @@ -0,0 +1,23 @@ +package org.briarproject.bramble.api.autodelete; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MINUTES; + +public interface AutoDeleteConstants { + + /** + * The minimum valid auto-delete timer duration in milliseconds. + */ + long MIN_AUTO_DELETE_TIMER_MS = MINUTES.toMillis(1); + + /** + * The maximum valid auto-delete timer duration in milliseconds. + */ + long MAX_AUTO_DELETE_TIMER_MS = DAYS.toMillis(365); + + /** + * Placeholder value indicating that a message has no auto-delete timer. + * This value should not be sent over the wire - send null instead. + */ + long NO_AUTO_DELETE_TIMER = -1; +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index db32a1914..a741cf5bb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -140,6 +140,7 @@ import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -273,15 +274,11 @@ public class ConversationActivity extends BriarActivity ImagePreview imagePreview = findViewById(R.id.imagePreview); sendController = new TextAttachmentController(textInputView, imagePreview, this, viewModel); - viewModel.hasImageSupport().observe(this, new Observer() { - @Override - public void onChanged(@Nullable Boolean hasSupport) { - if (hasSupport != null && hasSupport) { - // TODO: remove cast when removing feature flag - ((TextAttachmentController) sendController) - .setImagesSupported(); - viewModel.hasImageSupport().removeObserver(this); - } + observeOnce(viewModel.getPrivateMessageFormat(), this, format -> { + if (format != TEXT_ONLY) { + // TODO: remove cast when removing feature flag + ((TextAttachmentController) sendController) + .setImagesSupported(); } }); } else { @@ -657,8 +654,8 @@ public class ConversationActivity extends BriarActivity supportFinishAfterTransition(); } } else if (e instanceof ConversationMessageReceivedEvent) { - ConversationMessageReceivedEvent p = - (ConversationMessageReceivedEvent) e; + ConversationMessageReceivedEvent p = + (ConversationMessageReceivedEvent) e; if (p.getContactId().equals(contactId)) { LOG.info("Message received, adding"); onNewConversationMessage(p.getMessageHeader()); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index 8e65db26c..9f2e034a6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -32,6 +32,7 @@ import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessageFactory; +import org.briarproject.briar.api.messaging.PrivateMessageFormat; import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; @@ -52,17 +53,20 @@ import androidx.lifecycle.Transformations; import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY; @NotNullByDefault public class ConversationViewModel extends AndroidViewModel implements EventListener, AttachmentManager { - private static Logger LOG = + private static final Logger LOG = getLogger(ConversationViewModel.class.getName()); private static final String SHOW_ONBOARDING_IMAGE = @@ -89,7 +93,7 @@ public class ConversationViewModel extends AndroidViewModel private final LiveData contactName = Transformations.map(contact, UiUtils::getContactDisplayName); private final LiveData messagingGroupId; - private final MutableLiveData imageSupport = + private final MutableLiveData privateMessageFormat = new MutableLiveData<>(); private final MutableLiveEvent showImageOnboarding = new MutableLiveEvent<>(); @@ -210,11 +214,8 @@ public class ConversationViewModel extends AndroidViewModel // messagingGroupId is loaded with the contact observeForeverOnce(messagingGroupId, groupId -> { requireNonNull(groupId); - observeForeverOnce(imageSupport, hasImageSupport -> { - requireNonNull(hasImageSupport); - createMessage(groupId, text, headers, timestamp, - hasImageSupport); - }); + observeForeverOnce(privateMessageFormat, format -> + createMessage(groupId, text, headers, timestamp, format)); }); } @@ -244,10 +245,10 @@ public class ConversationViewModel extends AndroidViewModel @DatabaseExecutor private void checkFeaturesAndOnboarding(ContactId c) throws DbException { - // check if images are supported - boolean imagesSupported = db.transactionWithResult(true, txn -> - messagingManager.contactSupportsImages(txn, c)); - imageSupport.postValue(imagesSupported); + // check if images and auto-deletion are supported + PrivateMessageFormat format = db.transactionWithResult(true, txn -> + messagingManager.getContactMessageFormat(txn, c)); + privateMessageFormat.postValue(format); // check if introductions are supported Collection contacts = contactManager.getContacts(); @@ -256,7 +257,7 @@ public class ConversationViewModel extends AndroidViewModel // we only show one onboarding dialog at a time Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE); - if (imagesSupported && + if (format != TEXT_ONLY && settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) { onOnboardingShown(SHOW_ONBOARDING_IMAGE); showImageOnboarding.postEvent(true); @@ -277,15 +278,19 @@ public class ConversationViewModel extends AndroidViewModel @UiThread private void createMessage(GroupId groupId, @Nullable String text, List headers, long timestamp, - boolean hasImageSupport) { + PrivateMessageFormat format) { try { PrivateMessage pm; - if (hasImageSupport) { + if (format == TEXT_ONLY) { + pm = privateMessageFactory.createLegacyPrivateMessage( + groupId, timestamp, requireNonNull(text)); + } else if (format == TEXT_IMAGES) { pm = privateMessageFactory.createPrivateMessage(groupId, timestamp, text, headers); } else { - pm = privateMessageFactory.createLegacyPrivateMessage( - groupId, timestamp, requireNonNull(text)); + // TODO: Look up auto-delete timer + pm = privateMessageFactory.createPrivateMessage(groupId, + timestamp, text, headers, NO_AUTO_DELETE_TIMER); } storeMessage(pm); } catch (FormatException e) { @@ -305,7 +310,8 @@ public class ConversationViewModel extends AndroidViewModel PrivateMessageHeader h = new PrivateMessageHeader( message.getId(), message.getGroupId(), message.getTimestamp(), true, true, false, false, - m.hasText(), m.getAttachmentHeaders()); + m.hasText(), m.getAttachmentHeaders(), + m.getAutoDeleteTimer()); // TODO add text to cache when available here addedHeader.postEvent(h); } catch (DbException e) { @@ -330,8 +336,8 @@ public class ConversationViewModel extends AndroidViewModel return contactName; } - LiveData hasImageSupport() { - return imageSupport; + LiveData getPrivateMessageFormat() { + return privateMessageFormat; } LiveEvent showImageOnboarding() { diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java index ccb8f776f..413bc6204 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java @@ -30,7 +30,7 @@ public interface MessagingManager extends ConversationClient { /** * The current minor version of the messaging client. */ - int MINOR_VERSION = 2; + int MINOR_VERSION = 3; /** * Stores a local private message. @@ -77,12 +77,8 @@ public interface MessagingManager extends ConversationClient { Attachment getAttachment(AttachmentHeader h) throws DbException; /** - * Returns true if the contact with the given {@link ContactId} does support - * image attachments. - * - * Added: 2019-01-01 + * Returns the private message format supported by the given contact. */ - boolean contactSupportsImages(Transaction txn, ContactId c) + PrivateMessageFormat getContactMessageFormat(Transaction txn, ContactId c) throws DbException; - } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessage.java index 58bde1576..f9d17c6c1 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessage.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessage.java @@ -8,44 +8,66 @@ import java.util.List; import javax.annotation.concurrent.Immutable; import static java.util.Collections.emptyList; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY; @Immutable @NotNullByDefault public class PrivateMessage { private final Message message; - private final boolean legacyFormat, hasText; + private final boolean hasText; private final List attachmentHeaders; + private final long autoDeleteTimer; + private final PrivateMessageFormat format; /** - * Constructor for private messages in the legacy format, which does not - * support attachments. + * Constructor for private messages in the + * {@link PrivateMessageFormat#TEXT_ONLY TEXT_ONLY} format. */ public PrivateMessage(Message message) { this.message = message; - legacyFormat = true; hasText = true; attachmentHeaders = emptyList(); + autoDeleteTimer = NO_AUTO_DELETE_TIMER; + format = TEXT_ONLY; } /** - * Constructor for private messages in the current format, which supports - * attachments. + * Constructor for private messages in the + * {@link PrivateMessageFormat#TEXT_IMAGES TEXT_IMAGES} format. */ public PrivateMessage(Message message, boolean hasText, List headers) { this.message = message; this.hasText = hasText; this.attachmentHeaders = headers; - legacyFormat = false; + autoDeleteTimer = NO_AUTO_DELETE_TIMER; + format = TEXT_IMAGES; + } + + /** + * Constructor for private messages in the + * {@link PrivateMessageFormat#TEXT_IMAGES_AUTO_DELETE TEXT_IMAGES_AUTO_DELETE} + * format. + */ + public PrivateMessage(Message message, boolean hasText, + List headers, long autoDeleteTimer) { + this.message = message; + this.hasText = hasText; + this.attachmentHeaders = headers; + this.autoDeleteTimer = autoDeleteTimer; + format = TEXT_IMAGES_AUTO_DELETE; } public Message getMessage() { return message; } - public boolean isLegacyFormat() { - return legacyFormat; + public PrivateMessageFormat getFormat() { + return format; } public boolean hasText() { @@ -55,4 +77,8 @@ public class PrivateMessage { public List getAttachmentHeaders() { return attachmentHeaders; } + + public long getAutoDeleteTimer() { + return autoDeleteTimer; + } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFactory.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFactory.java index 2f7e1127b..0215bafcb 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFactory.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFactory.java @@ -11,11 +11,29 @@ import javax.annotation.Nullable; @NotNullByDefault public interface PrivateMessageFactory { + /** + * Creates a private message in the + * {@link PrivateMessageFormat#TEXT_ONLY TEXT_ONLY} format. + */ PrivateMessage createLegacyPrivateMessage(GroupId groupId, long timestamp, String text) throws FormatException; + /** + * Creates a private message in the + * {@link PrivateMessageFormat#TEXT_IMAGES TEXT_IMAGES} format. This format + * requires the contact to support client version 0.1 or higher. + */ PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, @Nullable String text, List headers) throws FormatException; + /** + * Creates a private message in the + * {@link PrivateMessageFormat#TEXT_IMAGES_AUTO_DELETE TEXT_IMAGES_AUTO_DELETE} + * format. This format requires the contact to support client version 0.3 + * or higher. + */ + PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, + @Nullable String text, List headers, + long autoDeleteTimer) throws FormatException; } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFormat.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFormat.java new file mode 100644 index 000000000..b7f08b9f3 --- /dev/null +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageFormat.java @@ -0,0 +1,24 @@ +package org.briarproject.briar.api.messaging; + +public enum PrivateMessageFormat { + + /** + * First version of the private message format, which doesn't support + * image attachments or auto-deletion. + */ + TEXT_ONLY, + + /** + * Second version of the private message format, which supports image + * attachments but not auto-deletion. Support for this format was + * added in client version 0.1. + */ + TEXT_IMAGES, + + /** + * Third version of the private message format, which supports image + * attachments and auto-deletion. Support for this format was added + * in client version 0.3. + */ + TEXT_IMAGES_AUTO_DELETE +} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageHeader.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageHeader.java index 010466f6f..71d7d0c23 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageHeader.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/PrivateMessageHeader.java @@ -16,13 +16,16 @@ public class PrivateMessageHeader extends ConversationMessageHeader { private final boolean hasText; private final List attachmentHeaders; + private final long autoDeleteTimer; public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp, boolean local, boolean read, boolean sent, boolean seen, - boolean hasText, List headers) { + boolean hasText, List headers, + long autoDeleteTimer) { super(id, groupId, timestamp, local, read, sent, seen); this.hasText = hasText; this.attachmentHeaders = headers; + this.autoDeleteTimer = autoDeleteTimer; } public boolean hasText() { @@ -33,9 +36,12 @@ public class PrivateMessageHeader extends ConversationMessageHeader { return attachmentHeaders; } + public long getAutoDeleteTimer() { + return autoDeleteTimer; + } + @Override public T accept(ConversationMessageVisitor v) { return v.visitPrivateMessageHeader(this); } - } diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java index c9cb6b1eb..757c63eac 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingConstants.java @@ -13,4 +13,5 @@ interface MessagingConstants { String MSG_KEY_DESCRIPTOR_LENGTH = "descriptorLength"; String MSG_KEY_HAS_TEXT = "hasText"; String MSG_KEY_ATTACHMENT_HEADERS = "attachmentHeaders"; + String MSG_KEY_AUTO_DELETE_TIMER = "autoDeleteTimer"; } diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java index c048e0034..fac667ecf 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java @@ -38,6 +38,7 @@ import org.briarproject.briar.api.messaging.FileTooBigException; import org.briarproject.briar.api.messaging.InvalidAttachmentException; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessage; +import org.briarproject.briar.api.messaging.PrivateMessageFormat; import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent; @@ -57,14 +58,19 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static java.util.Collections.emptyList; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; 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.messaging.PrivateMessageFormat.TEXT_IMAGES; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE; +import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY; 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.GROUP_KEY_CONTACT_ID; 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_CONTENT_TYPE; import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_DESCRIPTOR_LENGTH; import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT; @@ -196,9 +202,11 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, long timestamp = meta.getLong(MSG_KEY_TIMESTAMP); boolean local = meta.getBoolean(MSG_KEY_LOCAL); boolean read = meta.getBoolean(MSG_KEY_READ); + long timer = meta.getLong(MSG_KEY_AUTO_DELETE_TIMER, + NO_AUTO_DELETE_TIMER); PrivateMessageHeader header = new PrivateMessageHeader(m.getId(), groupId, timestamp, local, - read, false, false, hasText, headers); + read, false, false, hasText, headers, timer); ContactId contactId = getContactId(txn, groupId); PrivateMessageReceivedEvent event = new PrivateMessageReceivedEvent(header, contactId); @@ -234,7 +242,7 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, meta.put(MSG_KEY_TIMESTAMP, m.getMessage().getTimestamp()); meta.put(MSG_KEY_LOCAL, true); meta.put(MSG_KEY_READ, true); - if (!m.isLegacyFormat()) { + if (m.getFormat() != TEXT_ONLY) { meta.put(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE); meta.put(MSG_KEY_HAS_TEXT, m.hasText()); BdfList headers = new BdfList(); @@ -243,6 +251,12 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, BdfList.of(a.getMessageId(), a.getContentType())); } meta.put(MSG_KEY_ATTACHMENT_HEADERS, headers); + if (m.getFormat() == TEXT_IMAGES_AUTO_DELETE) { + long timer = m.getAutoDeleteTimer(); + if (timer != NO_AUTO_DELETE_TIMER) { + meta.put(MSG_KEY_AUTO_DELETE_TIMER, timer); + } + } } // Mark attachments as shared and permanent now we're ready to send for (AttachmentHeader a : m.getAttachmentHeaders()) { @@ -355,12 +369,14 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, if (messageType == null) { headers.add(new PrivateMessageHeader(id, g, timestamp, local, read, s.isSent(), s.isSeen(), true, - emptyList())); + emptyList(), NO_AUTO_DELETE_TIMER)); } else { boolean hasText = meta.getBoolean(MSG_KEY_HAS_TEXT); + long timer = meta.getLong(MSG_KEY_AUTO_DELETE_TIMER, + NO_AUTO_DELETE_TIMER); headers.add(new PrivateMessageHeader(id, g, timestamp, local, read, s.isSent(), s.isSeen(), hasText, - parseAttachmentHeaders(meta))); + parseAttachmentHeaders(meta), timer)); } } catch (FormatException e) { throw new DbException(e); @@ -422,12 +438,13 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, } @Override - public boolean contactSupportsImages(Transaction txn, ContactId c) - throws DbException { + public PrivateMessageFormat getContactMessageFormat(Transaction txn, + ContactId c) throws DbException { int minorVersion = clientVersioningManager .getClientMinorVersion(txn, c, CLIENT_ID, 0); - // support was added in 0.1 - return minorVersion > 0; + if (minorVersion >= 3) return TEXT_IMAGES_AUTO_DELETE; + else if (minorVersion >= 1) return TEXT_IMAGES; + else return TEXT_ONLY; } @Override diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageFactoryImpl.java index 467540a3c..4c330db48 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageFactoryImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageFactoryImpl.java @@ -16,6 +16,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE; @@ -47,21 +48,43 @@ class PrivateMessageFactoryImpl implements PrivateMessageFactory { public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, @Nullable String text, List headers) throws FormatException { - // Validate the arguments - if (text == null) { - if (headers.isEmpty()) throw new IllegalArgumentException(); - } else if (utf8IsTooLong(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH)) { - throw new IllegalArgumentException(); - } - // Serialise the attachment headers - BdfList attachmentList = new BdfList(); - for (AttachmentHeader a : headers) { - attachmentList.add( - BdfList.of(a.getMessageId(), a.getContentType())); - } + validateTextAndAttachmentHeaders(text, headers); + BdfList attachmentList = serialiseAttachmentHeaders(headers); // Serialise the message BdfList body = BdfList.of(PRIVATE_MESSAGE, text, attachmentList); Message m = clientHelper.createMessage(groupId, timestamp, body); return new PrivateMessage(m, text != null, headers); } + + @Override + public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, + @Nullable String text, List headers, + long autoDeleteTimer) throws FormatException { + validateTextAndAttachmentHeaders(text, headers); + BdfList attachmentList = serialiseAttachmentHeaders(headers); + // Serialise the message + Long timer = autoDeleteTimer == NO_AUTO_DELETE_TIMER ? + null : autoDeleteTimer; + BdfList body = BdfList.of(PRIVATE_MESSAGE, text, attachmentList, timer); + Message m = clientHelper.createMessage(groupId, timestamp, body); + return new PrivateMessage(m, text != null, headers, autoDeleteTimer); + } + + private void validateTextAndAttachmentHeaders(@Nullable String text, + List headers) { + if (text == null) { + if (headers.isEmpty()) throw new IllegalArgumentException(); + } else if (utf8IsTooLong(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH)) { + throw new IllegalArgumentException(); + } + } + + private BdfList serialiseAttachmentHeaders(List headers) { + BdfList attachmentList = new BdfList(); + for (AttachmentHeader a : headers) { + attachmentList.add( + BdfList.of(a.getMessageId(), a.getContentType())); + } + return attachmentList; + } } diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageValidator.java b/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageValidator.java index 0b4909bf8..692dd580d 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageValidator.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/PrivateMessageValidator.java @@ -23,6 +23,8 @@ import java.io.InputStream; import javax.annotation.concurrent.Immutable; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.MAX_AUTO_DELETE_TIMER_MS; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; import static org.briarproject.bramble.util.ValidationUtils.checkLength; @@ -34,6 +36,7 @@ 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.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_CONTENT_TYPE; import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_DESCRIPTOR_LENGTH; import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT; @@ -99,7 +102,7 @@ class PrivateMessageValidator implements MessageValidator { private BdfMessageContext validateLegacyPrivateMessage(Message m, BdfList body) throws FormatException { - // Private message text + // Client version 0.0: Private message text checkSize(body, 1); String text = body.getString(0); checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH); @@ -113,8 +116,11 @@ class PrivateMessageValidator implements MessageValidator { private BdfMessageContext validatePrivateMessage(Message m, BdfList body) throws FormatException { - // Message type, optional private message text, attachment headers - checkSize(body, 3); + // Client version 0.1 to 0.2: Message type, optional private message + // text, attachment headers. + // Client version 0.3: Message type, optional private message text, + // attachment headers, optional auto-delete timer. + checkSize(body, 3, 4); String text = body.getOptionalString(1); checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH); BdfList headers = body.getList(2); @@ -129,6 +135,12 @@ class PrivateMessageValidator implements MessageValidator { String contentType = header.getString(1); checkLength(contentType, 1, MAX_CONTENT_TYPE_BYTES); } + Long timer = null; + if (body.size() == 4) timer = body.getOptionalLong(3); + if (timer != null && (timer < MIN_AUTO_DELETE_TIMER_MS || + timer > MAX_AUTO_DELETE_TIMER_MS)) { + throw new FormatException(); + } // Return the metadata BdfDictionary meta = new BdfDictionary(); meta.put(MSG_KEY_TIMESTAMP, m.getTimestamp()); @@ -137,6 +149,7 @@ class PrivateMessageValidator implements MessageValidator { meta.put(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE); meta.put(MSG_KEY_HAS_TEXT, text != null); meta.put(MSG_KEY_ATTACHMENT_HEADERS, headers); + if (timer != null) meta.put(MSG_KEY_AUTO_DELETE_TIMER, timer); return new BdfMessageContext(meta); } diff --git a/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java b/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java index ebd45cfcb..9f3e71f94 100644 --- a/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/test/TestDataCreatorImpl.java @@ -54,6 +54,8 @@ import javax.inject.Inject; import static java.util.Collections.emptyList; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.util.StringUtils.getRandomString; @@ -316,13 +318,17 @@ public class TestDataCreatorImpl implements TestDataCreator { long timestamp = clock.currentTimeMillis() - num * 60 * 1000; String text = getRandomText(); boolean local = random.nextBoolean(); - createPrivateMessage(groupId, text, timestamp, local); + boolean autoDelete = random.nextBoolean(); + createPrivateMessage(groupId, text, timestamp, local, autoDelete); } private void createPrivateMessage(GroupId groupId, String text, - long timestamp, boolean local) throws DbException, FormatException { - PrivateMessage m = privateMessageFactory - .createPrivateMessage(groupId, timestamp, text, emptyList()); + long timestamp, boolean local, boolean autoDelete) + throws DbException, FormatException { + long timer = autoDelete ? + MIN_AUTO_DELETE_TIMER_MS : NO_AUTO_DELETE_TIMER; + PrivateMessage m = privateMessageFactory.createPrivateMessage(groupId, + timestamp, text, emptyList(), timer); BdfDictionary meta = new BdfDictionary(); meta.put("timestamp", timestamp); meta.put("local", local); diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java index d07067887..2a2c4a829 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java @@ -19,6 +19,7 @@ import java.util.List; import javax.inject.Inject; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.MAX_AUTO_DELETE_TIMER_MS; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES; @@ -77,12 +78,12 @@ public class MessageSizeIntegrationTest extends BriarTestCase { getRandomString(MAX_CONTENT_TYPE_BYTES))); } PrivateMessage message = privateMessageFactory.createPrivateMessage( - groupId, timestamp, text, headers); + groupId, timestamp, text, headers, MAX_AUTO_DELETE_TIMER_MS); // Check the size of the serialised message int length = message.getMessage().getRawLength(); assertTrue(length > UniqueId.LENGTH + 8 + MAX_PRIVATE_MESSAGE_TEXT_LENGTH + MAX_ATTACHMENTS_PER_MESSAGE - * (UniqueId.LENGTH + MAX_CONTENT_TYPE_BYTES)); + * (UniqueId.LENGTH + MAX_CONTENT_TYPE_BYTES) + 4); assertTrue(length <= MAX_RECORD_PAYLOAD_BYTES); } diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java index 352154e7b..bf42ff5ed 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java @@ -32,6 +32,8 @@ 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.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; 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; @@ -89,7 +91,7 @@ public class MessagingManagerIntegrationTest @Test public void testSimpleConversation() throws Exception { - // conversation start out empty + // conversation starts out empty Collection messages0 = getMessages(c0); Collection messages1 = getMessages(c1); assertEquals(0, messages0.size()); @@ -108,6 +110,10 @@ public class MessagingManagerIntegrationTest (PrivateMessageHeader) messages1.iterator().next(); assertTrue(m0.hasText()); assertTrue(m1.hasText()); + assertEquals(0, m0.getAttachmentHeaders().size()); + assertEquals(0, m1.getAttachmentHeaders().size()); + assertEquals(NO_AUTO_DELETE_TIMER, m0.getAutoDeleteTimer()); + assertEquals(NO_AUTO_DELETE_TIMER, m1.getAutoDeleteTimer()); assertTrue(m0.isRead()); assertFalse(m1.isRead()); assertGroupCounts(c0, 1, 0); @@ -143,13 +149,44 @@ public class MessagingManagerIntegrationTest assertFalse(m1.hasText()); assertEquals(1, m0.getAttachmentHeaders().size()); assertEquals(1, m1.getAttachmentHeaders().size()); + assertEquals(NO_AUTO_DELETE_TIMER, m0.getAutoDeleteTimer()); + assertEquals(NO_AUTO_DELETE_TIMER, m1.getAutoDeleteTimer()); + assertTrue(m0.isRead()); + assertFalse(m1.isRead()); + assertGroupCounts(c0, 1, 0); + assertGroupCounts(c1, 1, 1); + } + + @Test + public void testAutoDeleteTimer() throws Exception { + // send message with auto-delete timer + sendMessage(c0, c1, getRandomString(123), emptyList(), + MIN_AUTO_DELETE_TIMER_MS); + + // message with timer is sent/displayed properly + Collection messages0 = getMessages(c0); + Collection messages1 = getMessages(c1); + assertEquals(1, messages0.size()); + assertEquals(1, messages1.size()); + PrivateMessageHeader m0 = + (PrivateMessageHeader) messages0.iterator().next(); + PrivateMessageHeader m1 = + (PrivateMessageHeader) messages1.iterator().next(); + assertTrue(m0.hasText()); + assertTrue(m1.hasText()); + assertEquals(0, m0.getAttachmentHeaders().size()); + assertEquals(0, m1.getAttachmentHeaders().size()); + assertEquals(MIN_AUTO_DELETE_TIMER_MS, m0.getAutoDeleteTimer()); + assertEquals(MIN_AUTO_DELETE_TIMER_MS, m1.getAutoDeleteTimer()); + assertTrue(m0.isRead()); + assertFalse(m1.isRead()); assertGroupCounts(c0, 1, 0); assertGroupCounts(c1, 1, 1); } @Test public void testDeleteAll() throws Exception { - // send 3 message (1 with attachment) + // send 3 messages (1 with attachment) sendMessage(c0, c1, getRandomString(42)); sendMessage(c0, c1, getRandomString(23)); sendMessage(c0, c1, null, singletonList(addAttachment(c0))); @@ -331,17 +368,23 @@ public class MessagingManagerIntegrationTest } private PrivateMessage sendMessage(BriarIntegrationTestComponent from, - BriarIntegrationTestComponent to, String text) - throws Exception { + BriarIntegrationTestComponent to, String text) throws Exception { return sendMessage(from, to, text, emptyList()); } private PrivateMessage sendMessage(BriarIntegrationTestComponent from, BriarIntegrationTestComponent to, @Nullable String text, List attachments) throws Exception { + return sendMessage(from, to, text, attachments, NO_AUTO_DELETE_TIMER); + } + + private PrivateMessage sendMessage(BriarIntegrationTestComponent from, + BriarIntegrationTestComponent to, @Nullable String text, + List attachments, long autoDeleteTimer) + throws Exception { GroupId g = from.getMessagingManager().getConversationId(contactId); PrivateMessage m = messageFactory.createPrivateMessage(g, - clock.currentTimeMillis(), text, attachments); + clock.currentTimeMillis(), text, attachments, autoDeleteTimer); from.getMessagingManager().addLocalMessage(m); syncMessage(from, to, contactId, 1 + attachments.size(), true); return m; diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/PrivateMessageValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/PrivateMessageValidatorTest.java index 1f0503f2b..a99c3a1eb 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/PrivateMessageValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/PrivateMessageValidatorTest.java @@ -20,6 +20,8 @@ import org.junit.Test; import java.io.InputStream; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.MAX_AUTO_DELETE_TIMER_MS; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; import static org.briarproject.bramble.test.TestUtils.getClientId; import static org.briarproject.bramble.test.TestUtils.getGroup; @@ -34,6 +36,7 @@ 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.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_CONTENT_TYPE; import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_DESCRIPTOR_LENGTH; import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT; @@ -391,6 +394,48 @@ public class PrivateMessageValidatorTest extends BrambleMockTestCase { validator.validateMessage(message, group); } + @Test + public void testAcceptsNullTimerForPrivateMessage() throws Exception { + testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, + new BdfList(), null), noAttachmentsMeta); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsNonLongTimerForPrivateMessage() throws Exception { + testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, + new BdfList(), "foo")); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsTooSmallTimerForPrivateMessage() throws Exception { + testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, + new BdfList(), MIN_AUTO_DELETE_TIMER_MS - 1)); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsTooBigTimerForPrivateMessage() throws Exception { + testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, + new BdfList(), MAX_AUTO_DELETE_TIMER_MS + 1)); + } + + @Test + public void testAcceptsMinTimerForPrivateMessage() throws Exception { + BdfDictionary minTimerMeta = new BdfDictionary(noAttachmentsMeta); + minTimerMeta.put(MSG_KEY_AUTO_DELETE_TIMER, MIN_AUTO_DELETE_TIMER_MS); + + testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, + new BdfList(), MIN_AUTO_DELETE_TIMER_MS), minTimerMeta); + } + + @Test + public void testAcceptsMaxTimerForPrivateMessage() throws Exception { + BdfDictionary maxTimerMeta = new BdfDictionary(noAttachmentsMeta); + maxTimerMeta.put(MSG_KEY_AUTO_DELETE_TIMER, MAX_AUTO_DELETE_TIMER_MS); + + testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, + new BdfList(), MAX_AUTO_DELETE_TIMER_MS), maxTimerMeta); + } + private void testRejectsLegacyMessage(BdfList body) throws Exception { expectCheckTimestamp(now); expectParseList(body); diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java index 3671c01e1..dff4034ff 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTest.java @@ -34,6 +34,7 @@ import java.util.concurrent.CountDownLatch; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; @@ -123,7 +124,8 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { PrivateMessageFactory privateMessageFactory = device.getPrivateMessageFactory(); PrivateMessage message = privateMessageFactory.createPrivateMessage( - groupId, timestamp, "Hi!", singletonList(attachmentHeader)); + groupId, timestamp, "Hi!", singletonList(attachmentHeader), + NO_AUTO_DELETE_TIMER); messagingManager.addLocalMessage(message); } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/event/WebSocketControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/event/WebSocketControllerTest.kt index 2dbc6614c..4d423305d 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/event/WebSocketControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/event/WebSocketControllerTest.kt @@ -6,6 +6,7 @@ import io.mockk.CapturingSlot import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER import org.briarproject.bramble.api.identity.AuthorInfo import org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED import org.briarproject.bramble.test.ImmediateExecutor @@ -41,7 +42,8 @@ internal class WebSocketControllerTest : ControllerTest() { true, true, true, - emptyList() + emptyList(), + NO_AUTO_DELETE_TIMER ) private val event = PrivateMessageReceivedEvent(header, contact.id) private val outputEvent = OutputEvent(EVENT_CONVERSATION_MESSAGE, event.output(text)) diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt index 84c7c5ec2..2f1e451f3 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt @@ -4,7 +4,14 @@ import io.javalin.http.BadRequestResponse import io.javalin.http.Context import io.javalin.http.NotFoundResponse import io.javalin.plugin.json.JavalinJson.toJson -import io.mockk.* +import io.mockk.CapturingSlot +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.runs +import org.briarproject.bramble.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER import org.briarproject.bramble.api.contact.ContactId import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.identity.AuthorInfo @@ -62,7 +69,8 @@ internal class MessagingControllerImplTest : ControllerTest() { true, true, true, - emptyList() + emptyList(), + NO_AUTO_DELETE_TIMER ) private val sessionId = SessionId(getRandomId()) private val privateMessage = PrivateMessage(message)