diff --git a/.idea/dictionaries/briar.xml b/.idea/dictionaries/briar.xml
index 71bcd22b4..7eb591723 100644
--- a/.idea/dictionaries/briar.xml
+++ b/.idea/dictionaries/briar.xml
@@ -7,6 +7,7 @@
encrypteridenticonintroducee
+ introduceesintroduceronboarding
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
index daf9858db..f0acbd8e1 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
@@ -9,4 +9,5 @@ public interface FeatureFlags {
boolean shouldEnableProfilePictures();
+ boolean shouldEnableDisappearingMessages();
}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/CleanupHook.java b/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/CleanupHook.java
new file mode 100644
index 000000000..6dca7a224
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/CleanupHook.java
@@ -0,0 +1,29 @@
+package org.briarproject.bramble.api.cleanup;
+
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import java.util.Collection;
+
+/**
+ * An interface for registering a hook with the {@link CleanupManager}
+ * that will be called when a message's cleanup deadline is reached.
+ */
+@NotNullByDefault
+public interface CleanupHook {
+
+ /**
+ * Called when the cleanup deadlines of one or more messages are reached.
+ *
+ * The callee is not required to delete the messages, but the hook won't be
+ * called again for these messages unless another cleanup timer is set (see
+ * {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)}
+ * and {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)}).
+ */
+ void deleteMessages(Transaction txn, GroupId g,
+ Collection messageIds) throws DbException;
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/CleanupManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/CleanupManager.java
new file mode 100644
index 000000000..d416d8fed
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/CleanupManager.java
@@ -0,0 +1,42 @@
+package org.briarproject.bramble.api.cleanup;
+
+import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.MessageId;
+
+/**
+ * The CleanupManager is responsible for tracking the cleanup deadlines of
+ * messages and passing them to their respective
+ * {@link CleanupHook CleanupHooks} when the deadlines are reached.
+ *
+ * The CleanupManager responds to
+ * {@link CleanupTimerStartedEvent CleanupTimerStartedEvents} broadcast by the
+ * {@link DatabaseComponent}.
+ *
+ * See {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)},
+ * {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)},
+ * {@link DatabaseComponent#stopCleanupTimer(Transaction, MessageId)}.
+ */
+@NotNullByDefault
+public interface CleanupManager {
+
+ /**
+ * When scheduling a cleanup task we overshoot the deadline by this many
+ * milliseconds to reduce the number of tasks that need to be scheduled
+ * when messages have cleanup deadlines that are close together.
+ */
+ long BATCH_DELAY_MS = 1000;
+
+ /**
+ * Registers a hook to be called when messages are due for cleanup.
+ * This method should be called before
+ * {@link LifecycleManager#startServices(SecretKey)}.
+ */
+ void registerCleanupHook(ClientId c, int majorVersion,
+ CleanupHook hook);
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/event/CleanupTimerStartedEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/event/CleanupTimerStartedEvent.java
new file mode 100644
index 000000000..f941cd806
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/cleanup/event/CleanupTimerStartedEvent.java
@@ -0,0 +1,32 @@
+package org.briarproject.bramble.api.cleanup.event;
+
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An event that is broadcast when a message's cleanup timer is started.
+ */
+@Immutable
+@NotNullByDefault
+public class CleanupTimerStartedEvent extends Event {
+
+ private final MessageId messageId;
+ private final long cleanupDeadline;
+
+ public CleanupTimerStartedEvent(MessageId messageId,
+ long cleanupDeadline) {
+ this.messageId = messageId;
+ this.cleanupDeadline = cleanupDeadline;
+ }
+
+ public MessageId getMessageId() {
+ return messageId;
+ }
+
+ public long getCleanupDeadline() {
+ return cleanupDeadline;
+ }
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/client/ClientHelper.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ClientHelper.java
index df68d7933..c04454d30 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/client/ClientHelper.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ClientHelper.java
@@ -1,6 +1,7 @@
package org.briarproject.bramble.api.client;
import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary;
@@ -16,6 +17,7 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import java.security.GeneralSecurityException;
+import java.util.Collection;
import java.util.Map;
@NotNullByDefault
@@ -50,9 +52,11 @@ public interface ClientHelper {
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
throws DbException, FormatException;
+ Collection getMessageIds(Transaction txn, GroupId g,
+ BdfDictionary query) throws DbException, FormatException;
+
BdfDictionary getMessageMetadataAsDictionary(MessageId m)
- throws DbException,
- FormatException;
+ throws DbException, FormatException;
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
throws DbException, FormatException;
@@ -119,4 +123,17 @@ public interface ClientHelper {
Map parseAndValidateTransportPropertiesMap(
BdfDictionary properties) throws FormatException;
+ /**
+ * Retrieves the contact ID from the group metadata of the given contact
+ * group.
+ */
+ ContactId getContactId(Transaction txn, GroupId contactGroupId)
+ throws DbException, FormatException;
+
+ /**
+ * Stores the given contact ID in the group metadata of the given contact
+ * group.
+ */
+ void setContactId(Transaction txn, GroupId contactGroupId, ContactId c)
+ throws DbException;
}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupConstants.java
new file mode 100644
index 000000000..4c556bbe7
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/client/ContactGroupConstants.java
@@ -0,0 +1,9 @@
+package org.briarproject.bramble.api.client;
+
+public interface ContactGroupConstants {
+
+ /**
+ * Group metadata key for associating a contact ID with a contact group.
+ */
+ String GROUP_KEY_CONTACT_ID = "contactId";
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
index 7c525bcd3..b5cf1d617 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
@@ -41,6 +41,18 @@ import javax.annotation.Nullable;
@NotNullByDefault
public interface DatabaseComponent extends TransactionManager {
+ /**
+ * Return value for {@link #getNextCleanupDeadline(Transaction)} if
+ * no messages are scheduled to be deleted.
+ */
+ long NO_CLEANUP_DEADLINE = -1;
+
+ /**
+ * Return value for {@link #startCleanupTimer(Transaction, MessageId)}
+ * if the cleanup timer was not started.
+ */
+ long TIMER_NOT_STARTED = -1;
+
/**
* Opens the database and returns true if the database already existed.
*
@@ -288,6 +300,16 @@ public interface DatabaseComponent extends TransactionManager {
Collection getMessageIds(Transaction txn, GroupId g)
throws DbException;
+ /**
+ * Returns the IDs of any delivered messages in the given group with
+ * metadata that matches all entries in the given query. If the query is
+ * empty, the IDs of all delivered messages are returned.
+ *
+ * Read-only.
+ */
+ Collection getMessageIds(Transaction txn, GroupId g,
+ Metadata query) throws DbException;
+
/**
* Returns the IDs of any messages that need to be validated.
*
@@ -314,6 +336,15 @@ public interface DatabaseComponent extends TransactionManager {
Collection getMessagesToShare(Transaction txn)
throws DbException;
+ /**
+ * Returns the IDs of any messages of any messages that are due for
+ * deletion, along with their group IDs.
+ *
+ * Read-only.
+ */
+ Map> getMessagesToDelete(Transaction txn)
+ throws DbException;
+
/**
* Returns the metadata for all delivered messages in the given group.
*
@@ -395,6 +426,15 @@ public interface DatabaseComponent extends TransactionManager {
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
throws DbException;
+ /**
+ * Returns the next time (in milliseconds since the Unix epoch) when a
+ * message is due to be deleted, or {@link #NO_CLEANUP_DEADLINE}
+ * if no messages are scheduled to be deleted.
+ *
+ * Read-only.
+ */
+ long getNextCleanupDeadline(Transaction txn) throws DbException;
+
/*
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may
@@ -535,6 +575,13 @@ public interface DatabaseComponent extends TransactionManager {
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
throws DbException;
+ /**
+ * Sets the cleanup timer duration for the given message. This does not
+ * start the message's cleanup timer.
+ */
+ void setCleanupTimerDuration(Transaction txn, MessageId m, long duration)
+ throws DbException;
+
/**
* Marks the given contact as verified.
*/
@@ -557,6 +604,12 @@ public interface DatabaseComponent extends TransactionManager {
*/
void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
+ /**
+ * Marks the given message as not shared. This method is only meant for
+ * testing.
+ */
+ void setMessageNotShared(Transaction txn, MessageId m) throws DbException;
+
/**
* Marks the given message as shared.
*/
@@ -599,6 +652,22 @@ public interface DatabaseComponent extends TransactionManager {
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
throws DbException;
+ /**
+ * Starts the cleanup timer for the given message, if a timer duration
+ * has been set and the timer has not already been started.
+ *
+ * @return The cleanup deadline, or {@link #TIMER_NOT_STARTED} if no
+ * timer duration has been set for this message or its timer has already
+ * been started.
+ */
+ long startCleanupTimer(Transaction txn, MessageId m) throws DbException;
+
+ /**
+ * Stops the cleanup timer for the given message, if the timer has been
+ * started.
+ */
+ void stopCleanupTimer(Transaction txn, MessageId m) throws DbException;
+
/**
* Stores the given transport keys, deleting any keys they have replaced.
*/
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/util/ValidationUtils.java b/bramble-api/src/main/java/org/briarproject/bramble/util/ValidationUtils.java
index fffacae0c..346edc1f7 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/util/ValidationUtils.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/util/ValidationUtils.java
@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+@Immutable
@NotNullByDefault
public class ValidationUtils {
@@ -64,4 +66,9 @@ public class ValidationUtils {
if (dictionary != null && dictionary.size() != size)
throw new FormatException();
}
+
+ public static void checkRange(@Nullable Long l, long min, long max)
+ throws FormatException {
+ if (l != null && (l < min || l > max)) throw new FormatException();
+ }
}
diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/TimeTravel.java b/bramble-api/src/test/java/org/briarproject/bramble/test/TimeTravel.java
new file mode 100644
index 000000000..2287fe6c5
--- /dev/null
+++ b/bramble-api/src/test/java/org/briarproject/bramble/test/TimeTravel.java
@@ -0,0 +1,8 @@
+package org.briarproject.bramble.test;
+
+public interface TimeTravel {
+
+ void setCurrentTimeMillis(long now) throws InterruptedException;
+
+ void addCurrentTimeMillis(long add) throws InterruptedException;
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java
index ac13a5612..c256759ff 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java
@@ -1,5 +1,6 @@
package org.briarproject.bramble;
+import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoExecutorModule;
import org.briarproject.bramble.db.DatabaseExecutorModule;
@@ -14,6 +15,8 @@ import org.briarproject.bramble.versioning.VersioningModule;
public interface BrambleCoreEagerSingletons {
+ void inject(CleanupModule.EagerSingletons init);
+
void inject(ContactModule.EagerSingletons init);
void inject(CryptoExecutorModule.EagerSingletons init);
@@ -39,6 +42,7 @@ public interface BrambleCoreEagerSingletons {
class Helper {
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
+ c.inject(new CleanupModule.EagerSingletons());
c.inject(new ContactModule.EagerSingletons());
c.inject(new CryptoExecutorModule.EagerSingletons());
c.inject(new DatabaseExecutorModule.EagerSingletons());
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java
index 473df2b77..447bd5cb6 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java
@@ -1,5 +1,6 @@
package org.briarproject.bramble;
+import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.client.ClientModule;
import org.briarproject.bramble.connection.ConnectionModule;
import org.briarproject.bramble.contact.ContactModule;
@@ -21,15 +22,14 @@ import org.briarproject.bramble.rendezvous.RendezvousModule;
import org.briarproject.bramble.settings.SettingsModule;
import org.briarproject.bramble.sync.SyncModule;
import org.briarproject.bramble.sync.validation.ValidationModule;
-import org.briarproject.bramble.system.ClockModule;
import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.bramble.versioning.VersioningModule;
import dagger.Module;
@Module(includes = {
+ CleanupModule.class,
ClientModule.class,
- ClockModule.class,
ConnectionModule.class,
ContactModule.class,
CryptoModule.class,
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/cleanup/CleanupManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/cleanup/CleanupManagerImpl.java
new file mode 100644
index 000000000..4c88e57c7
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/cleanup/CleanupManagerImpl.java
@@ -0,0 +1,159 @@
+package org.briarproject.bramble.cleanup;
+
+import org.briarproject.bramble.api.cleanup.CleanupHook;
+import org.briarproject.bramble.api.cleanup.CleanupManager;
+import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
+import org.briarproject.bramble.api.db.DatabaseComponent;
+import org.briarproject.bramble.api.db.DatabaseExecutor;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.lifecycle.Service;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.ClientId;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.system.TaskScheduler;
+import org.briarproject.bramble.api.versioning.ClientMajorVersion;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+import javax.inject.Inject;
+
+import static java.lang.Math.max;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+@ThreadSafe
+@NotNullByDefault
+class CleanupManagerImpl implements CleanupManager, Service, EventListener {
+
+ private static final Logger LOG =
+ getLogger(CleanupManagerImpl.class.getName());
+
+ private final Executor dbExecutor;
+ private final DatabaseComponent db;
+ private final TaskScheduler taskScheduler;
+ private final Clock clock;
+ private final Map hooks =
+ new ConcurrentHashMap<>();
+ private final Object lock = new Object();
+
+ @GuardedBy("lock")
+ private final Set pending = new HashSet<>();
+
+ @Inject
+ CleanupManagerImpl(@DatabaseExecutor Executor dbExecutor,
+ DatabaseComponent db, TaskScheduler taskScheduler, Clock clock) {
+ this.dbExecutor = dbExecutor;
+ this.db = db;
+ this.taskScheduler = taskScheduler;
+ this.clock = clock;
+ }
+
+ @Override
+ public void registerCleanupHook(ClientId c, int majorVersion,
+ CleanupHook hook) {
+ hooks.put(new ClientMajorVersion(c, majorVersion), hook);
+ }
+
+ @Override
+ public void startService() {
+ maybeScheduleTask(clock.currentTimeMillis());
+ }
+
+ @Override
+ public void stopService() {
+ }
+
+ @Override
+ public void eventOccurred(Event e) {
+ if (e instanceof CleanupTimerStartedEvent) {
+ CleanupTimerStartedEvent a = (CleanupTimerStartedEvent) e;
+ maybeScheduleTask(a.getCleanupDeadline());
+ }
+ }
+
+ private void maybeScheduleTask(long deadline) {
+ synchronized (lock) {
+ for (CleanupTask task : pending) {
+ if (task.deadline <= deadline) return;
+ }
+ CleanupTask task = new CleanupTask(deadline);
+ pending.add(task);
+ scheduleTask(task);
+ }
+ }
+
+ private void scheduleTask(CleanupTask task) {
+ long now = clock.currentTimeMillis();
+ long delay = max(0, task.deadline - now + BATCH_DELAY_MS);
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("Scheduling cleanup task in " + delay + " ms");
+ }
+ taskScheduler.schedule(() -> deleteMessagesAndScheduleNextTask(task),
+ dbExecutor, delay, MILLISECONDS);
+ }
+
+ private void deleteMessagesAndScheduleNextTask(CleanupTask task) {
+ try {
+ synchronized (lock) {
+ pending.remove(task);
+ }
+ long deadline = db.transactionWithResult(false, txn -> {
+ deleteMessages(txn);
+ return db.getNextCleanupDeadline(txn);
+ });
+ if (deadline != NO_CLEANUP_DEADLINE) {
+ maybeScheduleTask(deadline);
+ }
+ } catch (DbException e) {
+ logException(LOG, WARNING, e);
+ }
+ }
+
+ private void deleteMessages(Transaction txn) throws DbException {
+ Map> ids = db.getMessagesToDelete(txn);
+ for (Entry> e : ids.entrySet()) {
+ GroupId groupId = e.getKey();
+ Collection messageIds = e.getValue();
+ if (LOG.isLoggable(INFO)) {
+ LOG.info(messageIds.size() + " messages to delete");
+ }
+ for (MessageId m : messageIds) db.stopCleanupTimer(txn, m);
+ Group group = db.getGroup(txn, groupId);
+ ClientMajorVersion cv = new ClientMajorVersion(group.getClientId(),
+ group.getMajorVersion());
+ CleanupHook hook = hooks.get(cv);
+ if (hook == null) {
+ throw new IllegalStateException("No cleanup hook for " + cv);
+ }
+ hook.deleteMessages(txn, groupId, messageIds);
+ }
+ }
+
+ private static class CleanupTask {
+
+ private final long deadline;
+
+ private CleanupTask(long deadline) {
+ this.deadline = deadline;
+ }
+ }
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/cleanup/CleanupModule.java b/bramble-core/src/main/java/org/briarproject/bramble/cleanup/CleanupModule.java
new file mode 100644
index 000000000..955dd3bdc
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/cleanup/CleanupModule.java
@@ -0,0 +1,29 @@
+package org.briarproject.bramble.cleanup;
+
+import org.briarproject.bramble.api.cleanup.CleanupManager;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class CleanupModule {
+
+ public static class EagerSingletons {
+ @Inject
+ CleanupManager cleanupManager;
+ }
+
+ @Provides
+ @Singleton
+ CleanupManager provideCleanupManager(LifecycleManager lifecycleManager,
+ EventBus eventBus, CleanupManagerImpl cleanupManager) {
+ lifecycleManager.registerService(cleanupManager);
+ eventBus.addListener(cleanupManager);
+ return cleanupManager;
+ }
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java
index 604570aee..f483acdc8 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/client/ClientHelperImpl.java
@@ -2,11 +2,13 @@ package org.briarproject.bramble.client;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
@@ -32,6 +34,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@@ -39,6 +42,7 @@ import java.util.Map.Entry;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
+import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
@@ -151,6 +155,12 @@ class ClientHelperImpl implements ClientHelper {
return metadataParser.parse(metadata);
}
+ @Override
+ public Collection getMessageIds(Transaction txn, GroupId g,
+ BdfDictionary query) throws DbException, FormatException {
+ return db.getMessageIds(txn, g, metadataEncoder.encode(query));
+ }
+
@Override
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException, FormatException {
@@ -389,4 +399,27 @@ class ClientHelperImpl implements ClientHelper {
return tpMap;
}
+ @Override
+ public ContactId getContactId(Transaction txn, GroupId contactGroupId)
+ throws DbException {
+ try {
+ BdfDictionary meta =
+ getGroupMetadataAsDictionary(txn, contactGroupId);
+ return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
+ } catch (FormatException e) {
+ throw new DbException(e); // Invalid group metadata
+ }
+ }
+
+ @Override
+ public void setContactId(Transaction txn, GroupId contactGroupId,
+ ContactId c) throws DbException {
+ BdfDictionary meta = BdfDictionary.of(
+ new BdfEntry(GROUP_KEY_CONTACT_ID, c.getInt()));
+ try {
+ mergeGroupMetadata(txn, contactGroupId, meta);
+ } catch (FormatException e) {
+ throw new AssertionError(e);
+ }
+ }
}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
index a256d91ff..b6832d1e9 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
@@ -504,6 +504,25 @@ interface Database {
*/
Collection getMessagesToShare(T txn) throws DbException;
+ /**
+ * Returns the IDs of any messages of any messages that are due for
+ * deletion, along with their group IDs.
+ *
+ * Read-only.
+ */
+ Map> getMessagesToDelete(T txn)
+ throws DbException;
+
+ /**
+ * Returns the next time (in milliseconds since the Unix epoch) when a
+ * message is due to be deleted, or
+ * {@link DatabaseComponent#NO_CLEANUP_DEADLINE} if no messages are
+ * scheduled to be deleted.
+ *
+ * Read-only.
+ */
+ long getNextCleanupDeadline(T txn) throws DbException;
+
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may
@@ -613,8 +632,10 @@ interface Database {
/**
* Marks a message as having been seen by the given contact.
+ *
+ * @return True if the message was not already marked as seen.
*/
- void raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
+ boolean raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
/**
* Removes a contact from the database.
@@ -678,6 +699,13 @@ interface Database {
*/
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
+ /**
+ * Sets the cleanup timer duration for the given message. This does not
+ * start the message's cleanup timer.
+ */
+ void setCleanupTimerDuration(T txn, MessageId m, long duration)
+ throws DbException;
+
/**
* Marks the given contact as verified.
*/
@@ -708,9 +736,10 @@ interface Database {
void setMessagePermanent(T txn, MessageId m) throws DbException;
/**
- * Marks the given message as shared.
+ * Marks the given message as shared or not.
*/
- void setMessageShared(T txn, MessageId m) throws DbException;
+ void setMessageShared(T txn, MessageId m, boolean shared)
+ throws DbException;
/**
* Sets the validation and delivery state of the given message.
@@ -737,6 +766,22 @@ interface Database {
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
throws DbException;
+ /**
+ * Starts the cleanup timer for the given message, if a timer duration
+ * has been set and the timer has not already been started.
+ *
+ * @return The cleanup deadline, or
+ * {@link DatabaseComponent#TIMER_NOT_STARTED} if no timer duration has
+ * been set for this message or its timer has already been started.
+ */
+ long startCleanupTimer(T txn, MessageId m) throws DbException;
+
+ /**
+ * Stops the cleanup timer for the given message, if the timer has been
+ * started.
+ */
+ void stopCleanupTimer(T txn, MessageId m) throws DbException;
+
/**
* Updates the transmission count, expiry time and estimated time of arrival
* of the given message with respect to the given contact, using the latency
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
index 6ec1760af..9b7ac74fa 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
@@ -1,5 +1,6 @@
package org.briarproject.bramble.db;
+import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContact;
@@ -576,6 +577,15 @@ class DatabaseComponentImpl implements DatabaseComponent {
return db.getMessageIds(txn, g);
}
+ @Override
+ public Collection getMessageIds(Transaction transaction,
+ GroupId g, Metadata query) throws DbException {
+ T txn = unbox(transaction);
+ if (!db.containsGroup(txn, g))
+ throw new NoSuchGroupException();
+ return db.getMessageIds(txn, g, query);
+ }
+
@Override
public Collection getMessagesToValidate(Transaction transaction)
throws DbException {
@@ -597,6 +607,13 @@ class DatabaseComponentImpl implements DatabaseComponent {
return db.getMessagesToShare(txn);
}
+ @Override
+ public Map> getMessagesToDelete(
+ Transaction transaction) throws DbException {
+ T txn = unbox(transaction);
+ return db.getMessagesToDelete(txn);
+ }
+
@Override
public Map getMessageMetadata(Transaction transaction,
GroupId g) throws DbException {
@@ -692,6 +709,13 @@ class DatabaseComponentImpl implements DatabaseComponent {
return db.getMessageDependents(txn, m);
}
+ @Override
+ public long getNextCleanupDeadline(Transaction transaction)
+ throws DbException {
+ T txn = unbox(transaction);
+ return db.getNextCleanupDeadline(txn);
+ }
+
@Override
public long getNextSendTime(Transaction transaction, ContactId c)
throws DbException {
@@ -795,8 +819,17 @@ class DatabaseComponentImpl implements DatabaseComponent {
Collection acked = new ArrayList<>();
for (MessageId m : a.getMessageIds()) {
if (db.containsVisibleMessage(txn, c, m)) {
- db.raiseSeenFlag(txn, c, m);
- acked.add(m);
+ if (db.raiseSeenFlag(txn, c, m)) {
+ // This is the first time the message has been acked by
+ // this contact. Start the cleanup timer (a no-op unless
+ // a cleanup deadline has been set for this message)
+ long deadline = db.startCleanupTimer(txn, m);
+ if (deadline != TIMER_NOT_STARTED) {
+ transaction.attach(new CleanupTimerStartedEvent(m,
+ deadline));
+ }
+ acked.add(m);
+ }
}
}
if (acked.size() > 0) {
@@ -952,6 +985,16 @@ class DatabaseComponentImpl implements DatabaseComponent {
db.removeTransportKeys(txn, t, k);
}
+ @Override
+ public void setCleanupTimerDuration(Transaction transaction, MessageId m,
+ long duration) throws DbException {
+ if (transaction.isReadOnly()) throw new IllegalArgumentException();
+ T txn = unbox(transaction);
+ if (!db.containsMessage(txn, m))
+ throw new NoSuchMessageException();
+ db.setCleanupTimerDuration(txn, m, duration);
+ }
+
@Override
public void setContactVerified(Transaction transaction, ContactId c)
throws DbException {
@@ -1001,6 +1044,16 @@ class DatabaseComponentImpl implements DatabaseComponent {
db.setMessagePermanent(txn, m);
}
+ @Override
+ public void setMessageNotShared(Transaction transaction, MessageId m)
+ throws DbException {
+ if (transaction.isReadOnly()) throw new IllegalArgumentException();
+ T txn = unbox(transaction);
+ if (!db.containsMessage(txn, m))
+ throw new NoSuchMessageException();
+ db.setMessageShared(txn, m, false);
+ }
+
@Override
public void setMessageShared(Transaction transaction, MessageId m)
throws DbException {
@@ -1010,7 +1063,7 @@ class DatabaseComponentImpl implements DatabaseComponent {
throw new NoSuchMessageException();
if (db.getMessageState(txn, m) != DELIVERED)
throw new IllegalArgumentException("Shared undelivered message");
- db.setMessageShared(txn, m);
+ db.setMessageShared(txn, m, true);
transaction.attach(new MessageSharedEvent(m));
}
@@ -1082,6 +1135,30 @@ class DatabaseComponentImpl implements DatabaseComponent {
db.setTransportKeysActive(txn, t, k);
}
+ @Override
+ public long startCleanupTimer(Transaction transaction, MessageId m)
+ throws DbException {
+ if (transaction.isReadOnly()) throw new IllegalArgumentException();
+ T txn = unbox(transaction);
+ if (!db.containsMessage(txn, m))
+ throw new NoSuchMessageException();
+ long deadline = db.startCleanupTimer(txn, m);
+ if (deadline != TIMER_NOT_STARTED) {
+ transaction.attach(new CleanupTimerStartedEvent(m, deadline));
+ }
+ return deadline;
+ }
+
+ @Override
+ public void stopCleanupTimer(Transaction transaction, MessageId m)
+ throws DbException {
+ if (transaction.isReadOnly()) throw new IllegalArgumentException();
+ T txn = unbox(transaction);
+ if (!db.containsMessage(txn, m))
+ throw new NoSuchMessageException();
+ db.stopCleanupTimer(txn, m);
+ }
+
@Override
public void updateTransportKeys(Transaction transaction,
Collection keys) throws DbException {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index b559b2924..72ceec594 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -72,6 +72,8 @@ import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
+import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -99,7 +101,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database {
// Package access for testing
- static final int CODE_SCHEMA_VERSION = 47;
+ static final int CODE_SCHEMA_VERSION = 48;
// Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1;
@@ -181,6 +183,11 @@ abstract class JdbcDatabase implements Database {
+ " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL,"
+ " temporary BOOLEAN NOT NULL,"
+ // Null if no timer duration has been set
+ + " cleanupTimerDuration BIGINT,"
+ // Null if no timer duration has been set or the timer
+ // hasn't started
+ + " cleanupDeadline BIGINT,"
+ " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId),"
@@ -337,6 +344,10 @@ abstract class JdbcDatabase implements Database {
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
+ " ON statuses (contactId, timestamp)";
+ private static final String INDEX_MESSAGES_BY_CLEANUP_DEADLINE =
+ "CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
+ + " ON messages (cleanupDeadline)";
+
private static final Logger LOG =
getLogger(JdbcDatabase.class.getName());
@@ -480,7 +491,8 @@ abstract class JdbcDatabase implements Database {
new Migration43_44(dbTypes),
new Migration44_45(),
new Migration45_46(),
- new Migration46_47(dbTypes)
+ new Migration46_47(dbTypes),
+ new Migration47_48()
);
}
@@ -558,6 +570,7 @@ abstract class JdbcDatabase implements Database {
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
+ s.executeUpdate(INDEX_MESSAGES_BY_CLEANUP_DEADLINE);
s.close();
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
@@ -1317,7 +1330,9 @@ abstract class JdbcDatabase implements Database {
public void deleteMessage(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null;
try {
- String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?";
+ String sql = "UPDATE messages"
+ + " SET raw = NULL, cleanupDeadline = NULL"
+ + " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate();
@@ -1796,7 +1811,6 @@ abstract class JdbcDatabase implements Database {
// Return early if there are no matches
if (intersection.isEmpty()) return Collections.emptySet();
}
- if (intersection == null) throw new AssertionError();
return intersection;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
@@ -2253,6 +2267,39 @@ abstract class JdbcDatabase implements Database {
}
}
+ @Override
+ public Map> getMessagesToDelete(
+ Connection txn) throws DbException {
+ long now = clock.currentTimeMillis();
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ String sql = "SELECT messageId, groupId FROM messages"
+ + " WHERE cleanupDeadline <= ?";
+ ps = txn.prepareStatement(sql);
+ ps.setLong(1, now);
+ rs = ps.executeQuery();
+ Map> ids = new HashMap<>();
+ while (rs.next()) {
+ MessageId m = new MessageId(rs.getBytes(1));
+ GroupId g = new GroupId(rs.getBytes(2));
+ Collection messageIds = ids.get(g);
+ if (messageIds == null) {
+ messageIds = new ArrayList<>();
+ ids.put(g, messageIds);
+ }
+ messageIds.add(m);
+ }
+ rs.close();
+ ps.close();
+ return ids;
+ } catch (SQLException e) {
+ tryToClose(rs, LOG, WARNING);
+ tryToClose(ps, LOG, WARNING);
+ throw new DbException(e);
+ }
+ }
+
@Override
public long getNextSendTime(Connection txn, ContactId c)
throws DbException {
@@ -2283,6 +2330,31 @@ abstract class JdbcDatabase implements Database {
}
}
+ @Override
+ public long getNextCleanupDeadline(Connection txn) throws DbException {
+ Statement s = null;
+ ResultSet rs = null;
+ try {
+ String sql = "SELECT cleanupDeadline FROM messages"
+ + " WHERE cleanupDeadline IS NOT NULL"
+ + " ORDER BY cleanupDeadline LIMIT 1";
+ s = txn.createStatement();
+ rs = s.executeQuery(sql);
+ long nextDeadline = NO_CLEANUP_DEADLINE;
+ if (rs.next()) {
+ nextDeadline = rs.getLong(1);
+ if (rs.next()) throw new AssertionError();
+ }
+ rs.close();
+ s.close();
+ return nextDeadline;
+ } catch (SQLException e) {
+ tryToClose(rs, LOG, WARNING);
+ tryToClose(s, LOG, WARNING);
+ throw new DbException(e);
+ }
+ }
+
@Override
public PendingContact getPendingContact(Connection txn, PendingContactId p)
throws DbException {
@@ -2803,7 +2875,7 @@ abstract class JdbcDatabase implements Database {
}
@Override
- public void raiseSeenFlag(Connection txn, ContactId c, MessageId m)
+ public boolean raiseSeenFlag(Connection txn, ContactId c, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
@@ -2815,6 +2887,7 @@ abstract class JdbcDatabase implements Database {
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
+ return affected == 1;
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
@@ -3048,6 +3121,25 @@ abstract class JdbcDatabase implements Database {
}
}
+ @Override
+ public void setCleanupTimerDuration(Connection txn, MessageId m,
+ long duration) throws DbException {
+ PreparedStatement ps = null;
+ try {
+ String sql = "UPDATE messages SET cleanupTimerDuration = ?"
+ + " WHERE messageId = ? AND cleanupTimerDuration IS NULL";
+ ps = txn.prepareStatement(sql);
+ ps.setLong(1, duration);
+ ps.setBytes(2, m.getBytes());
+ int affected = ps.executeUpdate();
+ if (affected < 0 || affected > 1) throw new DbStateException();
+ ps.close();
+ } catch (SQLException e) {
+ tryToClose(ps, LOG, WARNING);
+ throw new DbException(e);
+ }
+ }
+
@Override
public void setContactVerified(Connection txn, ContactId c)
throws DbException {
@@ -3155,22 +3247,24 @@ abstract class JdbcDatabase implements Database {
}
@Override
- public void setMessageShared(Connection txn, MessageId m)
+ public void setMessageShared(Connection txn, MessageId m, boolean shared)
throws DbException {
PreparedStatement ps = null;
try {
- String sql = "UPDATE messages SET shared = TRUE"
+ String sql = "UPDATE messages SET shared = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
- ps.setBytes(1, m.getBytes());
+ ps.setBoolean(1, shared);
+ ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
- sql = "UPDATE statuses SET messageShared = TRUE"
+ sql = "UPDATE statuses SET messageShared = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
- ps.setBytes(1, m.getBytes());
+ ps.setBoolean(1, shared);
+ ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
@@ -3299,6 +3393,60 @@ abstract class JdbcDatabase implements Database {
}
}
+ @Override
+ public long startCleanupTimer(Connection txn, MessageId m)
+ throws DbException {
+ long now = clock.currentTimeMillis();
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ String sql = "UPDATE messages"
+ + " SET cleanupDeadline = ? + cleanupTimerDuration"
+ + " WHERE messageId = ?"
+ + " AND cleanupTimerDuration IS NOT NULL"
+ + " AND cleanupDeadline IS NULL";
+ ps = txn.prepareStatement(sql);
+ ps.setLong(1, now);
+ ps.setBytes(2, m.getBytes());
+ int affected = ps.executeUpdate();
+ if (affected < 0 || affected > 1) throw new DbStateException();
+ ps.close();
+ if (affected == 0) return TIMER_NOT_STARTED;
+ sql = "SELECT cleanupDeadline FROM messages WHERE messageId = ?";
+ ps = txn.prepareStatement(sql);
+ ps.setBytes(1, m.getBytes());
+ rs = ps.executeQuery();
+ if (!rs.next()) throw new DbStateException();
+ long deadline = rs.getLong(1);
+ if (rs.next()) throw new DbStateException();
+ rs.close();
+ ps.close();
+ return deadline;
+ } catch (SQLException e) {
+ tryToClose(rs, LOG, WARNING);
+ tryToClose(ps, LOG, WARNING);
+ throw new DbException(e);
+ }
+ }
+
+ @Override
+ public void stopCleanupTimer(Connection txn, MessageId m)
+ throws DbException {
+ PreparedStatement ps = null;
+ try {
+ String sql = "UPDATE messages SET cleanupDeadline = NULL"
+ + " WHERE messageId = ?";
+ ps = txn.prepareStatement(sql);
+ ps.setBytes(1, m.getBytes());
+ int affected = ps.executeUpdate();
+ if (affected < 0 || affected > 1) throw new DbStateException();
+ ps.close();
+ } catch (SQLException e) {
+ tryToClose(ps, LOG, WARNING);
+ throw new DbException(e);
+ }
+ }
+
@Override
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
int maxLatency) throws DbException {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Migration47_48.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration47_48.java
new file mode 100644
index 000000000..690185794
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration47_48.java
@@ -0,0 +1,47 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.api.db.DbException;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
+
+class Migration47_48 implements Migration {
+
+ private static final Logger LOG = getLogger(Migration47_48.class.getName());
+
+ @Override
+ public int getStartVersion() {
+ return 47;
+ }
+
+ @Override
+ public int getEndVersion() {
+ return 48;
+ }
+
+ @Override
+ public void migrate(Connection txn) throws DbException {
+ Statement s = null;
+ try {
+ s = txn.createStatement();
+ // Null if no timer duration has been set
+ s.execute("ALTER TABLE messages"
+ + " ADD COLUMN cleanupTimerDuration BIGINT");
+ // Null if no timer duration has been set or the timer
+ // hasn't started
+ s.execute("ALTER TABLE messages"
+ + " ADD COLUMN cleanupDeadline BIGINT");
+ s.execute("CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
+ + " ON messages (cleanupDeadline)");
+ } catch (SQLException e) {
+ tryToClose(s, LOG, WARNING);
+ throw new DbException(e);
+ }
+ }
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java
index e68ecc468..d3822a23c 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningConstants.java
@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
// Metadata keys
String MSG_KEY_UPDATE_VERSION = "version";
String MSG_KEY_LOCAL = "local";
- String GROUP_KEY_CONTACT_ID = "contactId";
}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java
index b334f3bea..6aadf889a 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java
@@ -50,7 +50,6 @@ import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
-import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
@@ -161,13 +160,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
db.addGroup(txn, g);
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
// Attach the contact ID to the group
- BdfDictionary meta = new BdfDictionary();
- meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
- try {
- clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
- } catch (FormatException e) {
- throw new AssertionError(e);
- }
+ clientHelper.setContactId(txn, g.getId(), c.getId());
// Create and store the first local update
List versions = new ArrayList<>(clients);
Collections.sort(versions);
@@ -229,7 +222,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
Map after =
getVisibilities(newLocalStates, newRemoteStates);
// Call hooks for any visibilities that have changed
- ContactId c = getContactId(txn, m.getGroupId());
+ ContactId c = clientHelper.getContactId(txn, m.getGroupId());
if (!before.equals(after)) {
Contact contact = db.getContact(txn, c);
callVisibilityHooks(txn, contact, before, after);
@@ -521,17 +514,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
storeUpdate(txn, g, states, 1);
}
- private ContactId getContactId(Transaction txn, GroupId g)
- throws DbException {
- try {
- BdfDictionary meta =
- clientHelper.getGroupMetadataAsDictionary(txn, g);
- return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
- } catch (FormatException e) {
- throw new DbException(e);
- }
- }
-
private List updateStatesFromRemoteStates(
List oldLocalStates, List remoteStates) {
Set remoteSet = new HashSet<>();
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/BrambleCoreIntegrationTestEagerSingletons.java b/bramble-core/src/test/java/org/briarproject/bramble/BrambleCoreIntegrationTestEagerSingletons.java
index 679bf0719..0916b217b 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/BrambleCoreIntegrationTestEagerSingletons.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/BrambleCoreIntegrationTestEagerSingletons.java
@@ -1,18 +1,18 @@
package org.briarproject.bramble;
-import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
+import org.briarproject.bramble.system.TimeTravelModule;
public interface BrambleCoreIntegrationTestEagerSingletons
extends BrambleCoreEagerSingletons {
- void inject(DefaultTaskSchedulerModule.EagerSingletons init);
+ void inject(TimeTravelModule.EagerSingletons init);
class Helper {
public static void injectEagerSingletons(
BrambleCoreIntegrationTestEagerSingletons c) {
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
- c.inject(new DefaultTaskSchedulerModule.EagerSingletons());
+ c.inject(new TimeTravelModule.EagerSingletons());
}
}
}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
index b8fb04bc8..39615b2e5 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
@@ -1,5 +1,6 @@
package org.briarproject.bramble.db;
+import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId;
@@ -69,6 +70,8 @@ import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
@@ -510,11 +513,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the group is in the DB (which it's not)
- exactly(8).of(database).startTransaction();
+ exactly(10).of(database).startTransaction();
will(returnValue(txn));
- exactly(8).of(database).containsGroup(txn, groupId);
+ exactly(10).of(database).containsGroup(txn, groupId);
will(returnValue(false));
- exactly(8).of(database).abortTransaction(txn);
+ exactly(10).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -523,7 +526,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager);
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getGroup(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -531,7 +534,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getGroupMetadata(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -539,7 +542,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
+ db.getMessageIds(transaction, groupId));
+ fail();
+ } catch (NoSuchGroupException expected) {
+ // Expected
+ }
+
+ try {
+ db.transaction(true, transaction ->
+ db.getMessageIds(transaction, groupId, new Metadata()));
+ fail();
+ } catch (NoSuchGroupException expected) {
+ // Expected
+ }
+
+ try {
+ db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -547,7 +566,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId,
new Metadata()));
fail();
@@ -556,7 +575,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -594,11 +613,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the message is in the DB (which it's not)
- exactly(12).of(database).startTransaction();
+ exactly(15).of(database).startTransaction();
will(returnValue(txn));
- exactly(12).of(database).containsMessage(txn, messageId);
+ exactly(15).of(database).containsMessage(txn, messageId);
will(returnValue(false));
- exactly(12).of(database).abortTransaction(txn);
+ exactly(15).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -623,7 +642,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessage(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -631,7 +650,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageMetadata(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -639,7 +658,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageState(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -647,7 +666,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -662,6 +681,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected
}
+ try {
+ db.transaction(false, transaction ->
+ db.setCleanupTimerDuration(transaction, message.getId(),
+ HOURS.toMillis(1)));
+ fail();
+ } catch (NoSuchMessageException expected) {
+ // Expected
+ }
+
try {
db.transaction(false, transaction ->
db.setMessagePermanent(transaction, message.getId()));
@@ -687,7 +715,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageDependencies(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -695,12 +723,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
- db.transaction(false, transaction ->
+ db.transaction(true, transaction ->
db.getMessageDependents(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
+
+ try {
+ db.transaction(false, transaction ->
+ db.startCleanupTimer(transaction, messageId));
+ fail();
+ } catch (NoSuchMessageException expected) {
+ // Expected
+ }
+
+ try {
+ db.transaction(false, transaction ->
+ db.stopCleanupTimer(transaction, messageId));
+ fail();
+ } catch (NoSuchMessageException expected) {
+ // Expected
+ }
}
@Test
@@ -981,6 +1025,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
+ will(returnValue(true));
+ oneOf(database).startCleanupTimer(txn, messageId);
+ will(returnValue(TIMER_NOT_STARTED)); // No cleanup duration was set
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
}});
@@ -993,6 +1040,56 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
});
}
+ @Test
+ public void testReceiveDuplicateAck() throws Exception {
+ context.checking(new Expectations() {{
+ oneOf(database).startTransaction();
+ will(returnValue(txn));
+ oneOf(database).containsContact(txn, contactId);
+ will(returnValue(true));
+ oneOf(database).containsVisibleMessage(txn, contactId, messageId);
+ will(returnValue(true));
+ oneOf(database).raiseSeenFlag(txn, contactId, messageId);
+ will(returnValue(false)); // Already acked
+ oneOf(database).commitTransaction(txn);
+ }});
+ DatabaseComponent db = createDatabaseComponent(database, eventBus,
+ eventExecutor, shutdownManager);
+
+ db.transaction(false, transaction -> {
+ Ack a = new Ack(singletonList(messageId));
+ db.receiveAck(transaction, contactId, a);
+ });
+ }
+
+ @Test
+ public void testReceiveAckWithCleanupTimer() throws Exception {
+ long deadline = System.currentTimeMillis();
+ context.checking(new Expectations() {{
+ oneOf(database).startTransaction();
+ will(returnValue(txn));
+ oneOf(database).containsContact(txn, contactId);
+ will(returnValue(true));
+ oneOf(database).containsVisibleMessage(txn, contactId, messageId);
+ will(returnValue(true));
+ oneOf(database).raiseSeenFlag(txn, contactId, messageId);
+ will(returnValue(true));
+ oneOf(database).startCleanupTimer(txn, messageId);
+ will(returnValue(deadline));
+ oneOf(database).commitTransaction(txn);
+ oneOf(eventBus).broadcast(with(any(
+ CleanupTimerStartedEvent.class)));
+ oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
+ }});
+ DatabaseComponent db = createDatabaseComponent(database, eventBus,
+ eventExecutor, shutdownManager);
+
+ db.transaction(false, transaction -> {
+ Ack a = new Ack(singletonList(messageId));
+ db.receiveAck(transaction, contactId, a);
+ });
+ }
+
@Test
public void testReceiveMessage() throws Exception {
context.checking(new Expectations() {{
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index 40d9b7059..4dfec5fce 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -57,10 +57,11 @@ import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
-import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
+import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
@@ -355,7 +356,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
// Sharing the message should make it sendable
- db.setMessageShared(txn, messageId);
+ db.setMessageShared(txn, messageId, true);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
@@ -635,8 +636,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The group should not be visible to the contact
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
- assertEquals(emptyMap(),
- db.getGroupVisibility(txn, groupId));
+ assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
// Make the group visible to the contact
db.addGroupVisibility(txn, contactId, groupId, false);
@@ -659,8 +659,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Make the group invisible again
db.removeGroupVisibility(txn, contactId, groupId);
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
- assertEquals(emptyMap(),
- db.getGroupVisibility(txn, groupId));
+ assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
db.commitTransaction(txn);
db.close();
@@ -2044,7 +2043,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the message - now it should be sendable immediately
- db.setMessageShared(txn, messageId);
+ db.setMessageShared(txn, messageId, true);
assertEquals(0, db.getNextSendTime(txn, contactId));
// Mark the message as requested - it should still be sendable
@@ -2412,6 +2411,87 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertFalse(db.wasDirtyOnInitialisation());
}
+ @Test
+ public void testCleanupTimer() throws Exception {
+ long duration = 60_000;
+ long now = System.currentTimeMillis();
+ AtomicLong time = new AtomicLong(now);
+ Database db =
+ open(false, new TestMessageFactory(), new SettableClock(time));
+ Connection txn = db.startTransaction();
+
+ // No messages should be due or scheduled for deletion
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
+
+ // Add a group and a message
+ db.addGroup(txn, group);
+ db.addMessage(txn, message, DELIVERED, false, false, null);
+
+ // No messages should be due or scheduled for deletion
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
+
+ // Set the message's cleanup timer duration
+ db.setCleanupTimerDuration(txn, messageId, duration);
+
+ // No messages should be due or scheduled for deletion
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
+
+ // Start the message's cleanup timer
+ assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
+
+ // The timer can't be started again
+ assertEquals(TIMER_NOT_STARTED, db.startCleanupTimer(txn, messageId));
+
+ // No messages should be due for deletion, but the message should be
+ // scheduled for deletion
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(now + duration, db.getNextCleanupDeadline(txn));
+
+ // Stop the timer
+ db.stopCleanupTimer(txn, messageId);
+
+ // No messages should be due or scheduled for deletion
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
+
+ // Start the timer again
+ assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
+
+ // No messages should be due for deletion, but the message should be
+ // scheduled for deletion
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(now + duration, db.getNextCleanupDeadline(txn));
+
+ // 1 ms before the timer expires, no messages should be due for
+ // deletion but the message should be scheduled for deletion
+ time.set(now + duration - 1);
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(now + duration, db.getNextCleanupDeadline(txn));
+
+ // When the timer expires, the message should be due and scheduled for
+ // deletion
+ time.set(now + duration);
+ assertEquals(singletonMap(groupId, singletonList(messageId)),
+ db.getMessagesToDelete(txn));
+ assertEquals(now + duration, db.getNextCleanupDeadline(txn));
+
+ // 1 ms after the timer expires, the message should be due and
+ // scheduled for deletion
+ time.set(now + duration + 1);
+ assertEquals(singletonMap(groupId, singletonList(messageId)),
+ db.getMessagesToDelete(txn));
+ assertEquals(now + duration, db.getNextCleanupDeadline(txn));
+
+ // Once the message has been deleted, it should no longer be due
+ // or scheduled for deletion
+ db.deleteMessage(txn, messageId);
+ assertTrue(db.getMessagesToDelete(txn).isEmpty());
+ assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
+ }
+
private Database open(boolean resume) throws Exception {
return open(resume, new TestMessageFactory(), new SystemClock());
}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/system/TestTaskScheduler.java b/bramble-core/src/test/java/org/briarproject/bramble/system/TestTaskScheduler.java
new file mode 100644
index 000000000..55d753e22
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/system/TestTaskScheduler.java
@@ -0,0 +1,122 @@
+package org.briarproject.bramble.system;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.system.TaskScheduler;
+
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.fail;
+
+/**
+ * A {@link TaskScheduler} for use in tests. The scheduler keeps all scheduled
+ * tasks in a queue until {@link #runTasks()} is called.
+ */
+@NotNullByDefault
+class TestTaskScheduler implements TaskScheduler {
+
+ private final Queue queue = new PriorityBlockingQueue<>();
+ private final Clock clock;
+
+ TestTaskScheduler(Clock clock) {
+ this.clock = clock;
+ }
+
+ @Override
+ public Cancellable schedule(Runnable task, Executor executor, long delay,
+ TimeUnit unit) {
+ AtomicBoolean cancelled = new AtomicBoolean(false);
+ return schedule(task, executor, delay, unit, cancelled);
+ }
+
+ @Override
+ public Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
+ long delay, long interval, TimeUnit unit) {
+ AtomicBoolean cancelled = new AtomicBoolean(false);
+ return scheduleWithFixedDelay(task, executor, delay, interval, unit,
+ cancelled);
+ }
+
+ private Cancellable schedule(Runnable task, Executor executor, long delay,
+ TimeUnit unit, AtomicBoolean cancelled) {
+ long delayMillis = MILLISECONDS.convert(delay, unit);
+ long dueMillis = clock.currentTimeMillis() + delayMillis;
+ Task t = new Task(task, executor, dueMillis, cancelled);
+ queue.add(t);
+ return t;
+ }
+
+ private Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
+ long delay, long interval, TimeUnit unit, AtomicBoolean cancelled) {
+ // All executions of this periodic task share a cancelled flag
+ Runnable wrapped = () -> {
+ task.run();
+ scheduleWithFixedDelay(task, executor, interval, interval, unit,
+ cancelled);
+ };
+ return schedule(wrapped, executor, delay, unit, cancelled);
+ }
+
+ /**
+ * Runs any scheduled tasks that are due.
+ */
+ void runTasks() throws InterruptedException {
+ long now = clock.currentTimeMillis();
+ while (true) {
+ Task t = queue.peek();
+ if (t == null || t.dueMillis > now) return;
+ t = queue.poll();
+ // Submit the task to its executor and wait for it to finish
+ if (!t.run().await(1, MINUTES)) fail();
+ }
+ }
+
+ private static class Task
+ implements Cancellable, Comparable {
+
+ private final Runnable task;
+ private final Executor executor;
+ private final long dueMillis;
+ private final AtomicBoolean cancelled;
+
+ private Task(Runnable task, Executor executor, long dueMillis,
+ AtomicBoolean cancelled) {
+ this.task = task;
+ this.executor = executor;
+ this.dueMillis = dueMillis;
+ this.cancelled = cancelled;
+ }
+
+ @SuppressWarnings("UseCompareMethod") // Animal Sniffer
+ @Override
+ public int compareTo(Task task) {
+ return Long.valueOf(dueMillis).compareTo(task.dueMillis);
+ }
+
+ /**
+ * Submits the task to its executor and returns a latch that will be
+ * released when the task finishes.
+ */
+ public CountDownLatch run() {
+ if (cancelled.get()) return new CountDownLatch(0);
+ CountDownLatch latch = new CountDownLatch(1);
+ executor.execute(() -> {
+ task.run();
+ latch.countDown();
+ });
+ return latch;
+ }
+
+ @Override
+ public void cancel() {
+ cancelled.set(true);
+ }
+ }
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/system/TimeTravelModule.java b/bramble-core/src/test/java/org/briarproject/bramble/system/TimeTravelModule.java
new file mode 100644
index 000000000..d00d81300
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/system/TimeTravelModule.java
@@ -0,0 +1,98 @@
+package org.briarproject.bramble.system;
+
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.system.TaskScheduler;
+import org.briarproject.bramble.test.SettableClock;
+import org.briarproject.bramble.test.TimeTravel;
+
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class TimeTravelModule {
+
+ public static class EagerSingletons {
+ @Inject
+ TaskScheduler scheduler;
+ }
+
+ private final ScheduledExecutorService scheduledExecutorService;
+ private final Clock clock;
+ private final TaskScheduler taskScheduler;
+ private final TimeTravel timeTravel;
+
+ public TimeTravelModule() {
+ this(false);
+ }
+
+ public TimeTravelModule(boolean travel) {
+ // Discard tasks that are submitted during shutdown
+ RejectedExecutionHandler policy =
+ new ScheduledThreadPoolExecutor.DiscardPolicy();
+ scheduledExecutorService =
+ new ScheduledThreadPoolExecutor(1, policy);
+ if (travel) {
+ // Use a SettableClock and TestTaskScheduler to allow time travel
+ AtomicLong time = new AtomicLong(System.currentTimeMillis());
+ clock = new SettableClock(time);
+ TestTaskScheduler testTaskScheduler = new TestTaskScheduler(clock);
+ taskScheduler = testTaskScheduler;
+ timeTravel = new TimeTravel() {
+ @Override
+ public void setCurrentTimeMillis(long now)
+ throws InterruptedException {
+ time.set(now);
+ testTaskScheduler.runTasks();
+ }
+
+ @Override
+ public void addCurrentTimeMillis(long add)
+ throws InterruptedException {
+ time.addAndGet(add);
+ testTaskScheduler.runTasks();
+ }
+ };
+ } else {
+ // Use the default clock and task scheduler
+ clock = new SystemClock();
+ taskScheduler = new TaskSchedulerImpl(scheduledExecutorService);
+ timeTravel = new TimeTravel() {
+ @Override
+ public void setCurrentTimeMillis(long now) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addCurrentTimeMillis(long add) {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ }
+
+ @Provides
+ Clock provideClock() {
+ return clock;
+ }
+
+ @Provides
+ @Singleton
+ TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager) {
+ lifecycleManager.registerForShutdown(scheduledExecutorService);
+ return taskScheduler;
+ }
+
+ @Provides
+ TimeTravel provideTimeTravel() {
+ return timeTravel;
+ }
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
index 4ac540199..661df400a 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
@@ -3,8 +3,8 @@ package org.briarproject.bramble.test;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule;
-import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
+import org.briarproject.bramble.system.TimeTravelModule;
import dagger.Module;
import dagger.Provides;
@@ -12,11 +12,11 @@ import dagger.Provides;
@Module(includes = {
DefaultBatteryManagerModule.class,
DefaultEventExecutorModule.class,
- DefaultTaskSchedulerModule.class,
DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class,
TestPluginConfigModule.class,
- TestSecureRandomModule.class
+ TestSecureRandomModule.class,
+ TimeTravelModule.class
})
public class BrambleCoreIntegrationTestModule {
@@ -33,6 +33,11 @@ public class BrambleCoreIntegrationTestModule {
public boolean shouldEnableProfilePictures() {
return true;
}
+
+ @Override
+ public boolean shouldEnableDisappearingMessages() {
+ return true;
+ }
};
}
}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java
index 93cf97dcc..17d23087b 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java
@@ -38,7 +38,6 @@ import static org.briarproject.bramble.test.TestUtils.getContact;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
import static org.junit.Assert.assertEquals;
@@ -60,8 +59,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
private final ClientId clientId = getClientId();
private final long now = System.currentTimeMillis();
private final Transaction txn = new Transaction(null, false);
- private final BdfDictionary groupMeta = BdfDictionary.of(
- new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
private ClientVersioningManagerImpl createInstance() {
context.checking(new Expectations() {{
@@ -123,8 +120,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).addGroup(txn, contactGroup);
oneOf(db).setGroupVisibility(txn, contact.getId(),
contactGroup.getId(), SHARED);
- oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(),
- groupMeta);
+ oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
+ contact.getId());
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
@@ -460,9 +457,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
// Get contact ID
- oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
- contactGroup.getId());
- will(returnValue(groupMeta));
+ oneOf(clientHelper).getContactId(txn, contactGroup.getId());
+ will(returnValue(contact.getId()));
// No states or visibilities have changed
}});
@@ -492,10 +488,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the latest local update
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody));
- // Get client ID
- oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
- contactGroup.getId());
- will(returnValue(groupMeta));
+ // Get contact ID
+ oneOf(clientHelper).getContactId(txn, contactGroup.getId());
+ will(returnValue(contact.getId()));
// No states or visibilities have changed
}});
@@ -546,8 +541,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true));
- BdfDictionary groupMeta = BdfDictionary.of(
- new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate);
@@ -577,9 +570,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false);
// The client's visibility has changed
- oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
- contactGroup.getId());
- will(returnValue(groupMeta));
+ oneOf(clientHelper).getContactId(txn, contactGroup.getId());
+ will(returnValue(contact.getId()));
oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
@@ -619,8 +611,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true));
- BdfDictionary groupMeta = BdfDictionary.of(
- new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate);
@@ -650,9 +640,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false);
// The client's visibility has changed
- oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
- contactGroup.getId());
- will(returnValue(groupMeta));
+ oneOf(clientHelper).getContactId(txn, contactGroup.getId());
+ will(returnValue(contact.getId()));
oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
diff --git a/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java b/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
index 6ef05f5f1..f50ea15ae 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/BrambleJavaModule.java
@@ -8,9 +8,9 @@ import org.briarproject.bramble.system.JavaSystemModule;
import dagger.Module;
@Module(includes = {
+ CircumventionModule.class,
JavaNetworkModule.class,
JavaSystemModule.class,
- CircumventionModule.class,
SocksModule.class
})
public class BrambleJavaModule {
diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
index f4b7a6dee..6071103e0 100644
--- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
+++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
+import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.account.SignInTestCreateAccount;
import org.briarproject.briar.android.account.SignInTestSignIn;
@@ -18,6 +19,7 @@ import dagger.Component;
@Component(modules = {
AppModule.class,
AttachmentModule.class,
+ ClockModule.class,
MediaModule.class,
BriarCoreModule.class,
BrambleAndroidModule.class,
diff --git a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java
index af6d2ce02..5bed09f0f 100644
--- a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java
+++ b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
+import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule;
@@ -17,6 +18,7 @@ import dagger.Component;
@Component(modules = {
AppModule.class,
AttachmentModule.class,
+ ClockModule.class,
MediaModule.class,
BriarCoreModule.class,
BrambleAndroidModule.class,
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
index 5e04f88eb..aeed5817f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
+import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
@@ -46,6 +47,7 @@ import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager;
import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.attachment.AttachmentReader;
+import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -80,6 +82,7 @@ import dagger.Component;
BriarAccountModule.class,
AppModule.class,
AttachmentModule.class,
+ ClockModule.class,
MediaModule.class
})
public interface AndroidComponent
@@ -188,6 +191,8 @@ public interface AndroidComponent
Thread.UncaughtExceptionHandler exceptionHandler();
+ AutoDeleteManager autoDeleteManager();
+
void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java
index 4b007e3a0..f7a175805 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java
@@ -37,6 +37,7 @@ import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.util.BriarNotificationBuilder;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
+import org.briarproject.briar.api.conversation.ConversationResponse;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
@@ -226,6 +227,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent> p =
(ConversationMessageReceivedEvent>) e;
+ if (p.getMessageHeader() instanceof ConversationResponse) {
+ ConversationResponse r =
+ (ConversationResponse) p.getMessageHeader();
+ // don't show notification for own auto-decline responses
+ if (r.isAutoDecline()) return;
+ }
showContactNotification(p.getContactId());
} else if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
index 6d8507600..9a75f6e2e 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
@@ -293,6 +293,11 @@ public class AppModule {
public boolean shouldEnableProfilePictures() {
return IS_DEBUG_BUILD;
}
+
+ @Override
+ public boolean shouldEnableDisappearingMessages() {
+ return IS_DEBUG_BUILD;
+ }
};
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
index 1ce3bbd76..d1f3ad737 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
@@ -29,6 +29,7 @@ import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity;
+import org.briarproject.briar.android.conversation.ConversationSettingsDialog;
import org.briarproject.briar.android.conversation.ImageActivity;
import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity;
@@ -233,4 +234,6 @@ public interface ActivityComponent {
void inject(ConfirmAvatarDialogFragment fragment);
+ void inject(ConversationSettingsDialog dialog);
+
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java
index 003c7efe9..72beffa1a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java
@@ -17,6 +17,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
+import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.attachment.AttachmentHeader;
@@ -25,6 +26,8 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.FOCUS_DOWN;
@@ -34,6 +37,7 @@ import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -114,11 +118,12 @@ public class ReblogFragment extends BaseFragment implements SendListener {
}
@Override
- public void onSendClick(@Nullable String text,
- List headers) {
+ public LiveData onSendClick(@Nullable String text,
+ List headers, long expectedAutoDeleteTimer) {
ui.input.hideSoftKeyboard();
viewModel.repeatPost(item, text);
finish();
+ return new MutableLiveData<>(SENT);
}
private void showProgressBar() {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java
index e8532d6a8..e83f71d1d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java
@@ -31,12 +31,16 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
+import static org.briarproject.briar.android.view.TextSendController.SendState;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -112,8 +116,8 @@ public class WriteBlogPostActivity extends BriarActivity
}
@Override
- public void onSendClick(@Nullable String text,
- List headers) {
+ public LiveData onSendClick(@Nullable String text,
+ List headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar
@@ -122,6 +126,7 @@ public class WriteBlogPostActivity extends BriarActivity
progressBar.setVisibility(VISIBLE);
storePost(text);
+ return new MutableLiveData<>(SENT);
}
private void storePost(String text) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AliasDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AliasDialogFragment.java
index 30df0ecad..8db22b212 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AliasDialogFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AliasDialogFragment.java
@@ -31,6 +31,8 @@ import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
+// TODO: we can probably switch to androidx DialogFragment here but need to
+// test this properly
public class AliasDialogFragment extends AppCompatDialogFragment {
final static String TAG = AliasDialogFragment.class.getName();
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 01e0c1647..17da35e55 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
@@ -50,6 +50,7 @@ import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
import org.briarproject.briar.android.forum.ForumActivity;
+import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
@@ -59,8 +60,10 @@ import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
+import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.attachment.AttachmentHeader;
+import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
@@ -138,12 +141,14 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
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_IMAGES_AUTO_DELETE;
+import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
- implements EventListener, ConversationListener, TextCache,
- AttachmentCache, AttachmentListener, ActionMode.Callback {
+ implements BaseFragmentListener, EventListener, ConversationListener,
+ TextCache, AttachmentCache, AttachmentListener, ActionMode.Callback {
public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -268,15 +273,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 {
@@ -286,6 +287,9 @@ public class ConversationActivity extends BriarActivity
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
+
+ viewModel.getAutoDeleteTimer().observe(this, timer ->
+ sendController.setAutoDeleteTimer(timer));
}
private void scrollToBottom() {
@@ -369,6 +373,14 @@ public class ConversationActivity extends BriarActivity
// enable alias action if available
observeOnce(viewModel.getContactItem(), this, contact ->
menu.findItem(R.id.action_set_alias).setEnabled(true));
+ // Show auto-delete menu item if feature is enabled
+ if (featureFlags.shouldEnableDisappearingMessages()) {
+ MenuItem item = menu.findItem(R.id.action_conversation_settings);
+ item.setVisible(true);
+ // Enable menu item only if contact supports auto-delete
+ viewModel.getPrivateMessageFormat().observe(this, format ->
+ item.setEnabled(format == TEXT_IMAGES_AUTO_DELETE));
+ }
return super.onCreateOptionsMenu(menu);
}
@@ -390,6 +402,10 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG);
return true;
+ case R.id.action_conversation_settings:
+ if (contactId == null) return false;
+ onAutoDeleteTimerNoticeClicked();
+ return true;
case R.id.action_delete_all_messages:
askToDeleteAllMessages();
return true;
@@ -559,7 +575,7 @@ public class ConversationActivity extends BriarActivity
this::showImageOnboarding);
}
List items = createItems(headers);
- adapter.addAll(items);
+ adapter.replaceAll(items);
list.showData();
if (layoutManagerState == null) {
scrollToBottom();
@@ -640,8 +656,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());
@@ -658,6 +674,13 @@ public class ConversationActivity extends BriarActivity
LOG.info("Messages acked");
markMessages(m.getMessageIds(), true, true);
}
+ } else if (e instanceof ConversationMessagesDeletedEvent) {
+ ConversationMessagesDeletedEvent m =
+ (ConversationMessagesDeletedEvent) e;
+ if (m.getContactId().equals(contactId)) {
+ LOG.info("Messages auto-deleted");
+ onConversationMessagesDeleted(m.getMessageIds());
+ }
} else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e;
if (c.getContactId().equals(contactId)) {
@@ -705,6 +728,13 @@ public class ConversationActivity extends BriarActivity
}
}
+ @UiThread
+ private void onConversationMessagesDeleted(
+ Collection messageIds) {
+ adapter.incrementRevision();
+ adapter.removeItems(messageIds);
+ }
+
@UiThread
private void markMessages(Collection messageIds, boolean sent,
boolean seen) {
@@ -735,20 +765,13 @@ public class ConversationActivity extends BriarActivity
}
@Override
- public void onSendClick(@Nullable String text,
- List attachmentHeaders) {
+ public LiveData onSendClick(@Nullable String text,
+ List attachmentHeaders,
+ long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError();
- long timestamp = System.currentTimeMillis();
- timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
- viewModel.sendMessage(text, attachmentHeaders, timestamp);
- textInputView.clearText();
- }
-
- private long getMinTimestampForNewMessage() {
- // Don't use an earlier timestamp than the newest message
- ConversationItem item = adapter.getLastItem();
- return item == null ? 0 : item.getTime() + 1;
+ return viewModel
+ .sendMessage(text, attachmentHeaders, expectedAutoDeleteTimer);
}
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
@@ -823,10 +846,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()) {
@@ -958,13 +977,11 @@ public class ConversationActivity extends BriarActivity
adapter.notifyItemChanged(position, item);
}
runOnDbThread(() -> {
- long timestamp = System.currentTimeMillis();
- timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try {
switch (item.getRequestType()) {
case INTRODUCTION:
respondToIntroductionRequest(item.getSessionId(),
- accept, timestamp);
+ accept);
break;
case FORUM:
respondToForumRequest(item.getSessionId(), accept);
@@ -1038,11 +1055,18 @@ public class ConversationActivity extends BriarActivity
ActivityCompat.startActivity(this, i, options.toBundle());
}
+ @Override
+ public void onAutoDeleteTimerNoticeClicked() {
+ ConversationSettingsDialog dialog =
+ ConversationSettingsDialog.newInstance(contactId);
+ dialog.show(getSupportFragmentManager(),
+ ConversationSettingsDialog.TAG);
+ }
+
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
- boolean accept, long time) throws DbException {
- introductionManager.respondToIntroduction(contactId, sessionId, time,
- accept);
+ boolean accept) throws DbException {
+ introductionManager.respondToIntroduction(contactId, sessionId, accept);
}
@DatabaseExecutor
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java
index 1e7065df7..73d2a5f13 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java
@@ -13,20 +13,26 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.ItemReturningAdapter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@NotNullByDefault
class ConversationAdapter
extends BriarAdapter
implements ItemReturningAdapter {
- private ConversationListener listener;
+ private final ConversationListener listener;
private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration;
@Nullable
@@ -65,22 +71,20 @@ class ConversationAdapter
@LayoutRes int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
type, viewGroup, false);
- switch (type) {
- case R.layout.list_item_conversation_msg_in:
- return new ConversationMessageViewHolder(v, listener, true,
- imageViewPool, imageItemDecoration);
- case R.layout.list_item_conversation_msg_out:
- return new ConversationMessageViewHolder(v, listener, false,
- imageViewPool, imageItemDecoration);
- case R.layout.list_item_conversation_notice_in:
- return new ConversationNoticeViewHolder(v, listener, true);
- case R.layout.list_item_conversation_notice_out:
- return new ConversationNoticeViewHolder(v, listener, false);
- case R.layout.list_item_conversation_request:
- return new ConversationRequestViewHolder(v, listener, true);
- default:
- throw new IllegalArgumentException("Unknown ConversationItem");
+ if (type == R.layout.list_item_conversation_msg_in) {
+ return new ConversationMessageViewHolder(v, listener, true,
+ imageViewPool, imageItemDecoration);
+ } else if (type == R.layout.list_item_conversation_msg_out) {
+ return new ConversationMessageViewHolder(v, listener, false,
+ imageViewPool, imageItemDecoration);
+ } else if (type == R.layout.list_item_conversation_notice_in) {
+ return new ConversationNoticeViewHolder(v, listener, true);
+ } else if (type == R.layout.list_item_conversation_notice_out) {
+ return new ConversationNoticeViewHolder(v, listener, false);
+ } else if (type == R.layout.list_item_conversation_request) {
+ return new ConversationRequestViewHolder(v, listener, true);
}
+ throw new IllegalArgumentException("Unknown ConversationItem");
}
@Override
@@ -107,22 +111,65 @@ class ConversationAdapter
return c1.equals(c2);
}
+ @Override
+ public void add(ConversationItem item) {
+ items.beginBatchedUpdates();
+ items.add(item);
+ updateTimersInBatch();
+ items.endBatchedUpdates();
+ }
+
+ @Override
+ public void replaceAll(Collection itemsToReplace) {
+ items.beginBatchedUpdates();
+ // there can be items already in the adapter
+ // SortedList takes care of duplicates and detecting changed items
+ items.replaceAll(itemsToReplace);
+ updateTimersInBatch();
+ items.endBatchedUpdates();
+ }
+
+ @UiThread
+ void removeItems(Collection messageIds) {
+ // Collect all items to be deleted first
+ // and then delete them in one batched update.
+ // Deleting them right away would cause issues
+ // due to changing list positions.
+ List toRemove = new ArrayList<>(messageIds.size());
+ for (int i = 0; i < items.size(); i++) {
+ ConversationItem item = items.get(i);
+ if (messageIds.contains(item.getId())) toRemove.add(item);
+ }
+ items.beginBatchedUpdates();
+ for (ConversationItem item : toRemove) items.remove(item);
+ items.endBatchedUpdates();
+ }
+
+ private void updateTimersInBatch() {
+ long lastTimerIncoming = NO_AUTO_DELETE_TIMER;
+ long lastTimerOutgoing = NO_AUTO_DELETE_TIMER;
+ for (int i = 0; i < items.size(); i++) {
+ ConversationItem c = items.get(i);
+ boolean itemChanged;
+ boolean timerChanged;
+ if (c.isIncoming()) {
+ timerChanged = lastTimerIncoming != c.getAutoDeleteTimer();
+ lastTimerIncoming = c.getAutoDeleteTimer();
+ } else {
+ timerChanged = lastTimerOutgoing != c.getAutoDeleteTimer();
+ lastTimerOutgoing = c.getAutoDeleteTimer();
+ }
+ itemChanged = c.setTimerNoticeVisible(timerChanged);
+ if (itemChanged) items.updateItemAt(i, c);
+ }
+ }
+
void setSelectionTracker(SelectionTracker tracker) {
this.tracker = tracker;
}
- @Nullable
- ConversationItem getLastItem() {
- if (items.size() > 0) {
- return items.get(items.size() - 1);
- } else {
- return null;
- }
- }
-
SparseArray getOutgoingMessages() {
SparseArray messages = new SparseArray<>();
-
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (!item.isIncoming()) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java
index a0cd5d54b..c3d96e462 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java
@@ -9,6 +9,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
+import androidx.lifecycle.LiveData;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -22,20 +23,25 @@ abstract class ConversationItem {
protected String text;
private final MessageId id;
private final GroupId groupId;
- private final long time;
+ private final long time, autoDeleteTimer;
private final boolean isIncoming;
- private boolean read, sent, seen;
+ private final LiveData contactName;
+ private boolean read, sent, seen, showTimerNotice;
- ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h) {
+ ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h,
+ LiveData contactName) {
this.layoutRes = layoutRes;
this.text = null;
this.id = h.getId();
this.groupId = h.getGroupId();
this.time = h.getTimestamp();
+ this.autoDeleteTimer = h.getAutoDeleteTimer();
this.read = h.isRead();
this.sent = h.isSent();
this.seen = h.isSeen();
this.isIncoming = !h.isLocal();
+ this.contactName = contactName;
+ this.showTimerNotice = false;
}
@LayoutRes
@@ -68,6 +74,10 @@ abstract class ConversationItem {
return time;
}
+ public long getAutoDeleteTimer() {
+ return autoDeleteTimer;
+ }
+
/**
* Only useful for incoming messages.
*/
@@ -111,4 +121,25 @@ abstract class ConversationItem {
return isIncoming;
}
+ public LiveData getContactName() {
+ return contactName;
+ }
+
+ /**
+ * Set this to true when {@link #getAutoDeleteTimer()} has changed
+ * since the last message from the same peer.
+ *
+ * @return true if the value was set, false if it was already set.
+ */
+ boolean setTimerNoticeVisible(boolean visible) {
+ if (this.showTimerNotice != visible) {
+ this.showTimerNotice = visible;
+ return true;
+ }
+ return false;
+ }
+
+ boolean isTimerNoticeVisible() {
+ return showTimerNotice;
+ }
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java
index 6a54584b4..c09a2cbf7 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java
@@ -1,6 +1,8 @@
package org.briarproject.briar.android.conversation;
+import android.content.Context;
import android.view.View;
+import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -12,8 +14,12 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static org.briarproject.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
+import static org.briarproject.briar.android.util.UiUtils.formatDuration;
+import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread
@NotNullByDefault
@@ -24,8 +30,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConstraintLayout layout;
@Nullable
private final OutItemViewHolder outViewHolder;
- private final TextView text;
+ private final TextView topNotice, text;
protected final TextView time;
+ protected final ImageView bomb;
@Nullable
private String itemKey = null;
@@ -33,11 +40,13 @@ abstract class ConversationItemViewHolder extends ViewHolder {
boolean isIncoming) {
super(v);
this.listener = listener;
- this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
+ outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
root = v;
+ topNotice = v.findViewById(R.id.topNotice);
layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text);
time = v.findViewById(R.id.time);
+ bomb = v.findViewById(R.id.bomb);
}
@CallSuper
@@ -45,6 +54,8 @@ abstract class ConversationItemViewHolder extends ViewHolder {
itemKey = item.getKey();
root.setActivated(selected);
+ setTopNotice(item);
+
if (item.getText() != null) {
text.setText(trim(item.getText()));
}
@@ -52,6 +63,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
long timestamp = item.getTime();
time.setText(formatDate(time.getContext(), timestamp));
+ boolean showBomb = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
+ bomb.setVisibility(showBomb ? VISIBLE : GONE);
+
if (outViewHolder != null) outViewHolder.bind(item);
}
@@ -64,4 +78,35 @@ abstract class ConversationItemViewHolder extends ViewHolder {
return itemKey;
}
+ private void setTopNotice(ConversationItem item) {
+ if (item.isTimerNoticeVisible()) {
+ Context ctx = itemView.getContext();
+ topNotice.setVisibility(VISIBLE);
+ boolean enabled = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
+ String duration = enabled ?
+ formatDuration(ctx, item.getAutoDeleteTimer()) : "";
+ String tapToLearnMore = ctx.getString(R.string.tap_to_learn_more);
+ String text;
+ if (item.isIncoming()) {
+ String name = item.getContactName().getValue();
+ text = enabled ?
+ ctx.getString(R.string.auto_delete_msg_contact_enabled,
+ name, duration, tapToLearnMore) :
+ ctx.getString(R.string.auto_delete_msg_contact_disabled,
+ name, tapToLearnMore);
+ } else {
+ text = enabled ?
+ ctx.getString(R.string.auto_delete_msg_you_enabled,
+ duration, tapToLearnMore) :
+ ctx.getString(R.string.auto_delete_msg_you_disabled,
+ tapToLearnMore);
+ }
+ topNotice.setText(text);
+ topNotice.setOnClickListener(
+ v -> listener.onAutoDeleteTimerNoticeClicked());
+ } else {
+ topNotice.setVisibility(GONE);
+ }
+ }
+
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java
index 700608e3a..65a25e343 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java
@@ -18,4 +18,6 @@ interface ConversationListener {
void onAttachmentClicked(View view, ConversationMessageItem messageItem,
AttachmentItem attachmentItem);
+ void onAutoDeleteTimerNoticeClicked();
+
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java
index 8fc50d299..b9185c1ce 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java
@@ -10,6 +10,7 @@ import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread;
+import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -18,8 +19,8 @@ class ConversationMessageItem extends ConversationItem {
private final List attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
- List attachments) {
- super(layoutRes, h);
+ LiveData contactName, List attachments) {
+ super(layoutRes, h, contactName);
this.attachments = attachments;
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java
index acee25a9f..5baed8f1c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java
@@ -1,5 +1,6 @@
package org.briarproject.briar.android.conversation;
+import android.content.res.ColorStateList;
import android.view.View;
import android.view.ViewGroup;
@@ -14,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
import static androidx.core.content.ContextCompat.getColor;
+import static androidx.core.widget.ImageViewCompat.setImageTintList;
@UiThread
@NotNullByDefault
@@ -84,6 +86,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
if (item.getText() == null) {
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
+ setImageTintList(bomb, ColorStateList.valueOf(timeColorBubble));
constraintSet = imageConstraints;
} else {
resetStatusLayoutForText();
@@ -111,6 +114,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
// also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor);
+ setImageTintList(bomb, ColorStateList.valueOf(timeColor));
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeItem.java
index 0694a0762..50400017a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeItem.java
@@ -8,6 +8,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
+import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -17,15 +18,15 @@ class ConversationNoticeItem extends ConversationItem {
private final String msgText;
ConversationNoticeItem(@LayoutRes int layoutRes, String text,
- ConversationRequest r) {
- super(layoutRes, r);
+ LiveData contactName, ConversationRequest> r) {
+ super(layoutRes, r, contactName);
this.text = text;
this.msgText = r.getText();
}
ConversationNoticeItem(@LayoutRes int layoutRes, String text,
- ConversationResponse r) {
- super(layoutRes, r);
+ LiveData contactName, ConversationResponse r) {
+ super(layoutRes, r, contactName);
this.text = text;
this.msgText = null;
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestItem.java
index 71984db65..5f7027d63 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestItem.java
@@ -11,6 +11,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
+import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -26,14 +27,15 @@ class ConversationRequestItem extends ConversationNoticeItem {
private boolean answered;
ConversationRequestItem(@LayoutRes int layoutRes, String text,
- RequestType type, ConversationRequest r) {
- super(layoutRes, text, r);
+ LiveData contactName, RequestType type,
+ ConversationRequest> r) {
+ super(layoutRes, text, contactName, r);
this.requestType = type;
this.sessionId = r.getSessionId();
this.answered = r.wasAnswered();
if (r instanceof InvitationRequest) {
this.requestedGroupId = ((Shareable) r.getNameable()).getId();
- this.canBeOpened = ((InvitationRequest) r).canBeOpened();
+ this.canBeOpened = ((InvitationRequest>) r).canBeOpened();
} else {
this.requestedGroupId = null;
this.canBeOpened = false;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationSettingsDialog.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationSettingsDialog.java
new file mode 100644
index 000000000..f55d4c6d9
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationSettingsDialog.java
@@ -0,0 +1,126 @@
+package org.briarproject.briar.android.conversation;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.activity.ActivityComponent;
+import org.briarproject.briar.android.fragment.BaseFragment;
+import org.briarproject.briar.android.widget.OnboardingFullDialogFragment;
+
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import static java.util.logging.Level.INFO;
+import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
+import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class ConversationSettingsDialog extends DialogFragment {
+
+ final static String TAG = ConversationSettingsDialog.class.getName();
+
+ private static final Logger LOG = Logger.getLogger(TAG);
+
+ @Inject
+ ViewModelProvider.Factory viewModelFactory;
+
+ private ConversationViewModel viewModel;
+
+ static ConversationSettingsDialog newInstance(ContactId contactId) {
+ Bundle args = new Bundle();
+ args.putInt(CONTACT_ID, contactId.getInt());
+ ConversationSettingsDialog dialog = new ConversationSettingsDialog();
+ dialog.setArguments(args);
+ return dialog;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ injectFragment(((BaseFragment.BaseFragmentListener) context)
+ .getActivityComponent());
+ }
+
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
+ .get(ConversationViewModel.class);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(DialogFragment.STYLE_NO_FRAME,
+ R.style.BriarFullScreenDialogTheme);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_conversation_settings,
+ container, false);
+
+ Bundle args = requireArguments();
+ int id = args.getInt(CONTACT_ID, -1);
+ if (id == -1) throw new IllegalStateException();
+ ContactId contactId = new ContactId(id);
+
+ FragmentActivity activity = requireActivity();
+ viewModel = new ViewModelProvider(activity, viewModelFactory)
+ .get(ConversationViewModel.class);
+ viewModel.setContactId(contactId);
+
+ Toolbar toolbar = view.findViewById(R.id.toolbar);
+ toolbar.setNavigationOnClickListener(v -> dismiss());
+
+ SwitchCompat switchDisappearingMessages = view.findViewById(
+ R.id.switchDisappearingMessages);
+ switchDisappearingMessages.setOnCheckedChangeListener(
+ (button, value) -> viewModel.setAutoDeleteTimerEnabled(value));
+
+ Button buttonLearnMore =
+ view.findViewById(R.id.buttonLearnMore);
+ buttonLearnMore.setOnClickListener(e -> showLearnMoreDialog());
+
+ viewModel.getAutoDeleteTimer()
+ .observe(getViewLifecycleOwner(), timer -> {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("Received auto delete timer: " + timer);
+ }
+ boolean disappearingMessages =
+ timer != NO_AUTO_DELETE_TIMER;
+ switchDisappearingMessages
+ .setChecked(disappearingMessages);
+ switchDisappearingMessages.setEnabled(true);
+ });
+
+ return view;
+ }
+
+ private void showLearnMoreDialog() {
+ OnboardingFullDialogFragment.newInstance(
+ R.string.disappearing_messages_title,
+ R.string.disappearing_messages_explanation_long
+ ).show(getChildFragmentManager(), OnboardingFullDialogFragment.TAG);
+ }
+
+}
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 2a7e23dc7..1d477da11 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
@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
+import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
@@ -28,16 +29,22 @@ import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.attachment.AttachmentRetriever;
import org.briarproject.briar.android.contact.ContactItem;
import org.briarproject.briar.android.util.UiUtils;
+import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.attachment.AttachmentHeader;
+import org.briarproject.briar.api.autodelete.AutoDeleteManager;
+import org.briarproject.briar.api.autodelete.UnexpectedTimerException;
+import org.briarproject.briar.api.autodelete.event.AutoDeleteTimerMirroredEvent;
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
+import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager;
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;
@@ -55,6 +62,7 @@ import androidx.lifecycle.MutableLiveData;
import static androidx.lifecycle.Transformations.map;
import static java.util.Objects.requireNonNull;
+import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -62,6 +70,13 @@ 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.android.view.TextSendController.SendState.ERROR;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
+import static org.briarproject.briar.android.view.TextSendController.SendState.UNEXPECTED_TIMER;
+import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
+import static org.briarproject.briar.api.autodelete.AutoDeleteManager.DEFAULT_TIMER_DURATION;
+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 DbViewModel
@@ -84,6 +99,8 @@ public class ConversationViewModel extends DbViewModel
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentRetriever attachmentRetriever;
private final AttachmentCreator attachmentCreator;
+ private final AutoDeleteManager autoDeleteManager;
+ private final ConversationManager conversationManager;
@Nullable
private ContactId contactId = null;
@@ -92,7 +109,7 @@ public class ConversationViewModel extends DbViewModel
private final LiveData contactName = map(contactItem, c ->
UiUtils.getContactDisplayName(c.getContact()));
private final LiveData messagingGroupId;
- private final MutableLiveData imageSupport =
+ private final MutableLiveData privateMessageFormat =
new MutableLiveData<>();
private final MutableLiveEvent showImageOnboarding =
new MutableLiveEvent<>();
@@ -100,8 +117,10 @@ public class ConversationViewModel extends DbViewModel
new MutableLiveEvent<>();
private final MutableLiveData showIntroductionAction =
new MutableLiveData<>();
- private final MutableLiveData contactDeleted =
+ private final MutableLiveData autoDeleteTimer =
new MutableLiveData<>();
+ private final MutableLiveData contactDeleted =
+ new MutableLiveData<>(false);
private final MutableLiveEvent addedHeader =
new MutableLiveEvent<>();
@@ -118,7 +137,9 @@ public class ConversationViewModel extends DbViewModel
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
- AttachmentCreator attachmentCreator) {
+ AttachmentCreator attachmentCreator,
+ AutoDeleteManager autoDeleteManager,
+ ConversationManager conversationManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db;
this.eventBus = eventBus;
@@ -129,10 +150,10 @@ public class ConversationViewModel extends DbViewModel
this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator;
+ this.autoDeleteManager = autoDeleteManager;
+ this.conversationManager = conversationManager;
messagingGroupId = map(contactItem, c ->
messagingManager.getContactGroup(c.getContact()).getId());
- contactDeleted.setValue(false);
-
eventBus.addListener(this);
}
@@ -152,6 +173,11 @@ public class ConversationViewModel extends DbViewModel
runOnDbThread(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId()));
}
+ } else if (e instanceof AutoDeleteTimerMirroredEvent) {
+ AutoDeleteTimerMirroredEvent a = (AutoDeleteTimerMirroredEvent) e;
+ if (a.getContactId().equals(contactId)) {
+ autoDeleteTimer.setValue(a.getNewTimer());
+ }
} else if (e instanceof AvatarUpdatedEvent) {
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
if (a.getContactId().equals(contactId)) {
@@ -201,6 +227,11 @@ public class ConversationViewModel extends DbViewModel
contactItem.postValue(new ContactItem(c, authorInfo));
logDuration(LOG, "Loading contact", start);
start = now();
+ long timer = db.transactionWithResult(true, txn ->
+ autoDeleteManager.getAutoDeleteTimer(txn, contactId));
+ autoDeleteTimer.postValue(timer);
+ logDuration(LOG, "Getting auto-delete timer", start);
+ start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) {
@@ -215,7 +246,7 @@ public class ConversationViewModel extends DbViewModel
runOnDbThread(() -> {
try {
long start = now();
- messagingManager.setReadFlag(g, m, true);
+ conversationManager.setReadFlag(g, m, true);
logDuration(LOG, "Marking read", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
@@ -235,20 +266,6 @@ public class ConversationViewModel extends DbViewModel
});
}
- @UiThread
- void sendMessage(@Nullable String text,
- List headers, long timestamp) {
- // messagingGroupId is loaded with the contact
- observeForeverOnce(messagingGroupId, groupId -> {
- requireNonNull(groupId);
- observeForeverOnce(imageSupport, hasImageSupport -> {
- requireNonNull(hasImageSupport);
- createMessage(groupId, text, headers, timestamp,
- hasImageSupport);
- });
- });
- }
-
@Override
@UiThread
public LiveData storeAttachments(Collection uris,
@@ -275,10 +292,12 @@ public class ConversationViewModel extends DbViewModel
@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));
+ if (LOG.isLoggable(INFO))
+ LOG.info("PrivateMessageFormat loaded: " + format.name());
+ privateMessageFormat.postValue(format);
// check if introductions are supported
Collection contacts = contactManager.getContacts();
@@ -287,7 +306,7 @@ public class ConversationViewModel extends DbViewModel
// 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);
@@ -306,39 +325,82 @@ public class ConversationViewModel extends DbViewModel
}
@UiThread
- private void createMessage(GroupId groupId, @Nullable String text,
- List headers, long timestamp,
- boolean hasImageSupport) {
+ LiveData sendMessage(@Nullable String text,
+ List headers, long expectedTimer) {
+ MutableLiveData liveData = new MutableLiveData<>();
+ runOnDbThread(() -> {
+ try {
+ db.transaction(false, txn -> {
+ long start = now();
+ PrivateMessage m = createMessage(txn, text, headers,
+ expectedTimer);
+ messagingManager.addLocalMessage(txn, m);
+ logDuration(LOG, "Storing message", start);
+ Message message = m.getMessage();
+ PrivateMessageHeader h = new PrivateMessageHeader(
+ message.getId(), message.getGroupId(),
+ message.getTimestamp(), true, true, false, false,
+ m.hasText(), m.getAttachmentHeaders(),
+ m.getAutoDeleteTimer());
+ // TODO add text to cache when available here
+ MessageId id = message.getId();
+ txn.attach(() -> {
+ attachmentCreator.onAttachmentsSent(id);
+ liveData.setValue(SENT);
+ addedHeader.setEvent(h);
+ });
+ });
+ } catch (UnexpectedTimerException e) {
+ liveData.postValue(UNEXPECTED_TIMER);
+ } catch (DbException e) {
+ logException(LOG, WARNING, e);
+ liveData.postValue(ERROR);
+ }
+ });
+ return liveData;
+ }
+
+ private PrivateMessage createMessage(Transaction txn, @Nullable String text,
+ List headers, long expectedTimer)
+ throws DbException {
+ // Sending is only possible (setReady(true)) after loading all messages
+ // which happens after the contact has been loaded.
+ // privateMessageFormat is loaded together with contact
+ Contact contact = requireNonNull(contactItem.getValue()).getContact();
+ GroupId groupId = messagingManager.getContactGroup(contact).getId();
+ PrivateMessageFormat format =
+ requireNonNull(privateMessageFormat.getValue());
+ long timestamp = conversationManager
+ .getTimestampForOutgoingMessage(txn, requireNonNull(contactId));
try {
- PrivateMessage pm;
- if (hasImageSupport) {
- pm = privateMessageFactory.createPrivateMessage(groupId,
+ if (format == TEXT_ONLY) {
+ return privateMessageFactory.createLegacyPrivateMessage(
+ groupId, timestamp, requireNonNull(text));
+ } else if (format == TEXT_IMAGES) {
+ return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers);
} else {
- pm = privateMessageFactory.createLegacyPrivateMessage(
- groupId, timestamp, requireNonNull(text));
+ long timer = autoDeleteManager
+ .getAutoDeleteTimer(txn, contactId, timestamp);
+ if (timer != expectedTimer)
+ throw new UnexpectedTimerException();
+ return privateMessageFactory.createPrivateMessage(groupId,
+ timestamp, text, headers, timer);
}
- storeMessage(pm);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
- @UiThread
- private void storeMessage(PrivateMessage m) {
- attachmentCreator.onAttachmentsSent(m.getMessage().getId());
+ void setAutoDeleteTimerEnabled(boolean enabled) {
+ long timer = enabled ? DEFAULT_TIMER_DURATION : NO_AUTO_DELETE_TIMER;
+ // ContactId is set before menu gets inflated and UI interaction
+ final ContactId c = requireNonNull(contactId);
runOnDbThread(() -> {
try {
- long start = now();
- messagingManager.addLocalMessage(m);
- logDuration(LOG, "Storing message", start);
- Message message = m.getMessage();
- PrivateMessageHeader h = new PrivateMessageHeader(
- message.getId(), message.getGroupId(),
- message.getTimestamp(), true, true, false, false,
- m.hasText(), m.getAttachmentHeaders());
- // TODO add text to cache when available here
- addedHeader.postEvent(h);
+ db.transaction(false, txn ->
+ autoDeleteManager.setAutoDeleteTimer(txn, c, timer));
+ autoDeleteTimer.postValue(timer);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -357,8 +419,8 @@ public class ConversationViewModel extends DbViewModel
return contactName;
}
- LiveData hasImageSupport() {
- return imageSupport;
+ LiveData getPrivateMessageFormat() {
+ return privateMessageFormat;
}
LiveEvent showImageOnboarding() {
@@ -373,6 +435,10 @@ public class ConversationViewModel extends DbViewModel
return showIntroductionAction;
}
+ LiveData getAutoDeleteTimer() {
+ return autoDeleteTimer;
+ }
+
LiveData isContactDeleted() {
return contactDeleted;
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java
index 46468c030..061a57ccb 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java
@@ -60,10 +60,12 @@ class ConversationVisitor implements
}
if (h.isLocal()) {
item = new ConversationMessageItem(
- R.layout.list_item_conversation_msg_out, h, attachments);
+ R.layout.list_item_conversation_msg_out, h, contactName,
+ attachments);
} else {
item = new ConversationMessageItem(
- R.layout.list_item_conversation_msg_in, h, attachments);
+ R.layout.list_item_conversation_msg_in, h, contactName,
+ attachments);
}
if (h.hasText()) {
String text = textCache.getText(h.getId());
@@ -79,13 +81,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.blogs_sharing_invitation_sent,
r.getName(), contactName.getValue());
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text = ctx.getString(
R.string.blogs_sharing_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
- R.layout.list_item_conversation_request, text, BLOG, r);
+ R.layout.list_item_conversation_request, text, contactName,
+ BLOG, r);
}
}
@@ -98,13 +102,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.blogs_sharing_response_accepted_sent,
contactName.getValue());
+ } else if (r.isAutoDecline()) {
+ text = ctx.getString(
+ R.string.blogs_sharing_response_declined_auto,
+ contactName.getValue());
} else {
text = ctx.getString(
R.string.blogs_sharing_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -117,7 +126,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_in, text, r);
+ R.layout.list_item_conversation_notice_in, text,
+ contactName, r);
}
}
@@ -128,13 +138,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.forum_invitation_sent,
r.getName(), contactName.getValue());
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text = ctx.getString(
R.string.forum_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
- R.layout.list_item_conversation_request, text, FORUM, r);
+ R.layout.list_item_conversation_request, text, contactName,
+ FORUM, r);
}
}
@@ -147,13 +159,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.forum_invitation_response_accepted_sent,
contactName.getValue());
+ } else if (r.isAutoDecline()) {
+ text = ctx.getString(
+ R.string.forum_invitation_response_declined_auto,
+ contactName.getValue());
} else {
text = ctx.getString(
R.string.forum_invitation_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -166,7 +183,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_in, text, r);
+ R.layout.list_item_conversation_notice_in, text,
+ contactName, r);
}
}
@@ -178,13 +196,15 @@ class ConversationVisitor implements
R.string.groups_invitations_invitation_sent,
contactName.getValue(), r.getName());
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text = ctx.getString(
R.string.groups_invitations_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
- R.layout.list_item_conversation_request, text, GROUP, r);
+ R.layout.list_item_conversation_request, text, contactName,
+ GROUP, r);
}
}
@@ -197,13 +217,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.groups_invitations_response_accepted_sent,
contactName.getValue());
+ } else if (r.isAutoDecline()) {
+ text = ctx.getString(
+ R.string.groups_invitations_response_declined_auto,
+ contactName.getValue());
} else {
text = ctx.getString(
R.string.groups_invitations_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -216,7 +241,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_in, text, r);
+ R.layout.list_item_conversation_notice_in, text,
+ contactName, r);
}
}
@@ -227,7 +253,8 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.introduction_request_sent,
contactName.getValue(), name);
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text;
if (r.wasAnswered()) {
@@ -243,7 +270,7 @@ class ConversationVisitor implements
contactName.getValue(), name);
}
return new ConversationRequestItem(
- R.layout.list_item_conversation_request, text,
+ R.layout.list_item_conversation_request, text, contactName,
INTRODUCTION, r);
}
}
@@ -262,13 +289,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.introduction_response_accepted_sent,
introducedAuthor) + suffix;
+ } else if (r.isAutoDecline()) {
+ text = ctx.getString(
+ R.string.introduction_response_declined_auto,
+ introducedAuthor);
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,
introducedAuthor);
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_out, text, r);
+ R.layout.list_item_conversation_notice_out, text,
+ contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -288,7 +320,8 @@ class ConversationVisitor implements
introducedAuthor);
}
return new ConversationNoticeItem(
- R.layout.list_item_conversation_notice_in, text, r);
+ R.layout.list_item_conversation_notice_in, text,
+ contactName, r);
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java
index 8c2efe46f..d5ed65d33 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java
@@ -25,6 +25,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -34,6 +36,8 @@ import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
+import static org.briarproject.briar.android.view.TextSendController.SendState;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -129,8 +133,8 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
- public void onSendClick(@Nullable String text,
- List headers) {
+ public LiveData onSendClick(@Nullable String text,
+ List headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double invitations
ui.message.setReady(false);
@@ -141,6 +145,7 @@ public class IntroductionMessageFragment extends BaseFragment
FragmentActivity activity = requireActivity();
activity.setResult(RESULT_OK);
activity.supportFinishAfterTransition();
+ return new MutableLiveData<>(SENT);
}
private static class ViewHolder {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionViewModel.java
index 94ea3a4f3..399c4f64d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionViewModel.java
@@ -165,10 +165,9 @@ class IntroductionViewModel extends ContactsViewModel {
runOnDbThread(() -> {
// actually make the introduction
try {
- long timestamp = System.currentTimeMillis();
introductionManager.makeIntroduction(
info.getContact1().getContact(),
- info.getContact2().getContact(), text, timestamp);
+ info.getContact2().getContact(), text);
} catch (DbException e) {
logException(LOG, WARNING, e);
androidExecutor.runOnUiThread(() -> Toast.makeText(
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/CreateGroupControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/CreateGroupControllerImpl.java
index 809f224e6..4bae607e8 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/CreateGroupControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/creation/CreateGroupControllerImpl.java
@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -15,6 +17,8 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.briar.api.autodelete.AutoDeleteManager;
+import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -36,6 +40,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -44,9 +50,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
implements CreateGroupController {
private static final Logger LOG =
- Logger.getLogger(CreateGroupControllerImpl.class.getName());
+ getLogger(CreateGroupControllerImpl.class.getName());
private final Executor cryptoExecutor;
+ private final TransactionManager db;
+ private final AutoDeleteManager autoDeleteManager;
+ private final ConversationManager conversationManager;
private final ContactManager contactManager;
private final IdentityManager identityManager;
private final PrivateGroupFactory groupFactory;
@@ -57,17 +66,27 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private final Clock clock;
@Inject
- CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
+ CreateGroupControllerImpl(
+ @DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor,
- LifecycleManager lifecycleManager, ContactManager contactManager,
- AuthorManager authorManager, IdentityManager identityManager,
+ TransactionManager db,
+ AutoDeleteManager autoDeleteManager,
+ ConversationManager conversationManager,
+ LifecycleManager lifecycleManager,
+ ContactManager contactManager,
+ AuthorManager authorManager,
+ IdentityManager identityManager,
PrivateGroupFactory groupFactory,
GroupMessageFactory groupMessageFactory,
PrivateGroupManager groupManager,
GroupInvitationFactory groupInvitationFactory,
- GroupInvitationManager groupInvitationManager, Clock clock) {
+ GroupInvitationManager groupInvitationManager,
+ Clock clock) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.cryptoExecutor = cryptoExecutor;
+ this.db = db;
+ this.autoDeleteManager = autoDeleteManager;
+ this.conversationManager = conversationManager;
this.contactManager = contactManager;
this.identityManager = identityManager;
this.groupFactory = groupFactory;
@@ -131,16 +150,14 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler handler) {
runOnDbThread(() -> {
try {
- LocalAuthor localAuthor = identityManager.getLocalAuthor();
- List contacts = new ArrayList<>();
- for (ContactId c : contactIds) {
- try {
- contacts.add(contactManager.getContact(c));
- } catch (NoSuchContactException e) {
- // Continue
- }
- }
- signInvitations(g, localAuthor, contacts, text, handler);
+ db.transaction(false, txn -> {
+ LocalAuthor localAuthor =
+ identityManager.getLocalAuthor(txn);
+ List contexts =
+ createInvitationContexts(txn, contactIds);
+ txn.attach(() -> signInvitations(g, localAuthor, contexts,
+ text, handler));
+ });
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
@@ -148,17 +165,32 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
});
}
+ private List createInvitationContexts(Transaction txn,
+ Collection contactIds) throws DbException {
+ List contexts = new ArrayList<>();
+ for (ContactId c : contactIds) {
+ try {
+ Contact contact = contactManager.getContact(txn, c);
+ long timestamp = conversationManager
+ .getTimestampForOutgoingMessage(txn, c);
+ long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
+ timestamp);
+ contexts.add(new InvitationContext(contact, timestamp, timer));
+ } catch (NoSuchContactException e) {
+ // Continue
+ }
+ }
+ return contexts;
+ }
+
private void signInvitations(GroupId g, LocalAuthor localAuthor,
- Collection contacts, @Nullable String text,
+ List contexts, @Nullable String text,
ResultExceptionHandler handler) {
cryptoExecutor.execute(() -> {
- long timestamp = clock.currentTimeMillis();
- List contexts = new ArrayList<>();
- for (Contact c : contacts) {
- byte[] signature = groupInvitationFactory.signInvitation(c, g,
- timestamp, localAuthor.getPrivateKey());
- contexts.add(new InvitationContext(c.getId(), timestamp,
- signature));
+ for (InvitationContext ctx : contexts) {
+ ctx.signature = groupInvitationFactory.signInvitation(
+ ctx.contact, g, ctx.timestamp,
+ localAuthor.getPrivateKey());
}
sendInvitations(g, contexts, text, handler);
});
@@ -169,11 +201,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler handler) {
runOnDbThread(() -> {
try {
- for (InvitationContext context : contexts) {
+ for (InvitationContext ctx : contexts) {
try {
groupInvitationManager.sendInvitation(g,
- context.contactId, text, context.timestamp,
- context.signature);
+ ctx.contact.getId(), text, ctx.timestamp,
+ requireNonNull(ctx.signature),
+ ctx.autoDeleteTimer);
} catch (NoSuchContactException e) {
// Continue
}
@@ -188,15 +221,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private static class InvitationContext {
- private final ContactId contactId;
- private final long timestamp;
- private final byte[] signature;
+ private final Contact contact;
+ private final long timestamp, autoDeleteTimer;
+ @Nullable
+ private byte[] signature = null;
- private InvitationContext(ContactId contactId, long timestamp,
- byte[] signature) {
- this.contactId = contactId;
+ private InvitationContext(Contact contact, long timestamp,
+ long autoDeleteTimer) {
+ this.contact = contact;
this.timestamp = timestamp;
- this.signature = signature;
+ this.autoDeleteTimer = autoDeleteTimer;
}
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java
index ed1750c27..66adce301 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java
@@ -15,6 +15,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.LargeTextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
+import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List;
@@ -22,6 +23,10 @@ import java.util.List;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -79,13 +84,14 @@ public abstract class BaseMessageFragment extends BaseFragment
}
@Override
- public void onSendClick(@Nullable String text,
- List headers) {
+ public LiveData onSendClick(@Nullable String text,
+ List headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double actions
sendController.setReady(false);
message.hideSoftKeyboard();
listener.onButtonClick(text);
+ return new MutableLiveData<>(SENT);
}
@UiThread
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java
index abff0f41d..f07cdd130 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareBlogControllerImpl.java
@@ -10,11 +10,9 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.blog.BlogSharingManager;
-import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorManager;
import java.util.Collection;
@@ -26,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -34,22 +33,17 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
implements ShareBlogController {
private final static Logger LOG =
- Logger.getLogger(ShareBlogControllerImpl.class.getName());
+ getLogger(ShareBlogControllerImpl.class.getName());
- private final ConversationManager conversationManager;
private final BlogSharingManager blogSharingManager;
- private final Clock clock;
@Inject
ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager,
- ConversationManager conversationManager,
- BlogSharingManager blogSharingManager, Clock clock) {
+ BlogSharingManager blogSharingManager) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
- this.conversationManager = conversationManager;
this.blogSharingManager = blogSharingManager;
- this.clock = clock;
}
@Override
@@ -64,10 +58,7 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
try {
for (ContactId c : contacts) {
try {
- long time = Math.max(clock.currentTimeMillis(),
- conversationManager.getGroupCount(c)
- .getLatestMsgTime() + 1);
- blogSharingManager.sendInvitation(g, c, text, time);
+ blogSharingManager.sendInvitation(g, c, text);
} catch (NoSuchContactException | NoSuchGroupException e) {
logException(LOG, WARNING, e);
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java
index 1ab210d2e..dcbf5af91 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/ShareForumControllerImpl.java
@@ -10,10 +10,8 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
-import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.identity.AuthorManager;
@@ -26,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -34,22 +33,17 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
implements ShareForumController {
private final static Logger LOG =
- Logger.getLogger(ShareForumControllerImpl.class.getName());
+ getLogger(ShareForumControllerImpl.class.getName());
- private final ConversationManager conversationManager;
private final ForumSharingManager forumSharingManager;
- private final Clock clock;
@Inject
ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager,
- ConversationManager conversationManager,
- ForumSharingManager forumSharingManager, Clock clock) {
+ ForumSharingManager forumSharingManager) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
- this.conversationManager = conversationManager;
this.forumSharingManager = forumSharingManager;
- this.clock = clock;
}
@Override
@@ -64,10 +58,7 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
try {
for (ContactId c : contacts) {
try {
- long time = Math.max(clock.currentTimeMillis(),
- conversationManager.getGroupCount(c)
- .getLatestMsgTime() + 1);
- forumSharingManager.sendInvitation(g, c, text, time);
+ forumSharingManager.sendInvitation(g, c, text);
} catch (NoSuchContactException | NoSuchGroupException e) {
logException(LOG, WARNING, e);
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java
index ac045316b..40933e20f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java
@@ -19,6 +19,7 @@ import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
+import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.attachment.AttachmentHeader;
@@ -29,10 +30,13 @@ import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -231,8 +235,8 @@ public abstract class ThreadListActivity headers) {
+ public LiveData onSendClick(@Nullable String text,
+ List headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError();
MessageId replyId = getViewModel().getReplyId();
@@ -241,6 +245,7 @@ public abstract class ThreadListActivity(SENT);
}
protected abstract int getMaxTextLength();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
index 50459bb78..5193caa21 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
@@ -6,6 +6,7 @@ import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
+import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.PowerManager;
@@ -75,6 +76,7 @@ import static android.text.format.DateUtils.FORMAT_ABBREV_TIME;
import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
import static android.text.format.DateUtils.FORMAT_SHOW_TIME;
import static android.text.format.DateUtils.FORMAT_SHOW_YEAR;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
@@ -166,6 +168,36 @@ public class UiUtils {
return DateUtils.formatDateTime(ctx, time, flags);
}
+ /**
+ * Returns the given duration in a human-friendly format. For example,
+ * "7 days" or "1 hour 3 minutes".
+ */
+ public static String formatDuration(Context ctx, long millis) {
+ Resources r = ctx.getResources();
+ if (millis >= DAY_IN_MILLIS) {
+ int days = (int) (millis / DAY_IN_MILLIS);
+ int rest = (int) (millis % DAY_IN_MILLIS);
+ String dayStr =
+ r.getQuantityString(R.plurals.duration_days, days, days);
+ if (rest < HOUR_IN_MILLIS / 2) return dayStr;
+ else return dayStr + " " + formatDuration(ctx, rest);
+ } else if (millis >= HOUR_IN_MILLIS) {
+ int hours = (int) (millis / HOUR_IN_MILLIS);
+ int rest = (int) (millis % HOUR_IN_MILLIS);
+ String hourStr =
+ r.getQuantityString(R.plurals.duration_hours, hours, hours);
+ if (rest < MINUTE_IN_MILLIS / 2) return hourStr;
+ else return hourStr + " " + formatDuration(ctx, rest);
+ } else {
+ int minutes =
+ (int) ((millis + MINUTE_IN_MILLIS / 2) / MINUTE_IN_MILLIS);
+ // anything less than one minute is shown as one minute
+ if (minutes < 1) minutes = 1;
+ return r.getQuantityString(R.plurals.duration_minutes, minutes,
+ minutes);
+ }
+ }
+
public static long getDaysUntilExpiry() {
long now = System.currentTimeMillis();
return (EXPIRY_DATE - now) / DAYS.toMillis(1);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/CompositeSendButton.java b/briar-android/src/main/java/org/briarproject/briar/android/view/CompositeSendButton.java
index 273a250c5..095130fb7 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/view/CompositeSendButton.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/view/CompositeSendButton.java
@@ -5,6 +5,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
+import android.widget.ImageView;
import android.widget.ProgressBar;
import org.briarproject.briar.R;
@@ -19,6 +20,7 @@ import static java.util.Objects.requireNonNull;
public class CompositeSendButton extends FrameLayout {
private final AppCompatImageButton sendButton, imageButton;
+ private final ImageView bombBadge;
private final ProgressBar progressBar;
private boolean hasImageSupport = false;
@@ -32,6 +34,7 @@ public class CompositeSendButton extends FrameLayout {
sendButton = findViewById(R.id.sendButton);
imageButton = findViewById(R.id.imageButton);
+ bombBadge = findViewById(R.id.bombBadge);
progressBar = findViewById(R.id.progressBar);
}
@@ -71,6 +74,10 @@ public class CompositeSendButton extends FrameLayout {
return hasImageSupport;
}
+ public void setBombVisible(boolean visible) {
+ bombBadge.setVisibility(visible ? VISIBLE : INVISIBLE);
+ }
+
public void showImageButton(boolean showImageButton, boolean sendEnabled) {
if (showImageButton) {
imageButton.setVisibility(VISIBLE);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java
index a048d43e2..9cb3210f3 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java
@@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.customview.view.AbsSavedState;
-import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
@@ -39,6 +38,7 @@ import static androidx.core.content.ContextCompat.getColor;
import static androidx.customview.view.AbsSavedState.EMPTY_STATE;
import static androidx.lifecycle.Lifecycle.State.DESTROYED;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
@UiThread
@@ -52,7 +52,6 @@ public class TextAttachmentController extends TextSendController
private final AttachmentManager attachmentManager;
private final List imageUris = new ArrayList<>();
- private final CharSequence textHint;
private boolean loadingUris = false;
public TextAttachmentController(TextInputView v, ImagePreview imagePreview,
@@ -66,23 +65,44 @@ public class TextAttachmentController extends TextSendController
sendButton = (CompositeSendButton) compositeSendButton;
sendButton.setOnImageClickListener(view -> onImageButtonClicked());
-
- textHint = textInput.getHint();
}
@Override
protected void updateViewState() {
- textInput.setEnabled(ready && !loadingUris);
- boolean sendEnabled = ready && !loadingUris &&
- (!textIsEmpty || canSendEmptyText());
+ super.updateViewState();
if (loadingUris) {
sendButton.showProgress(true);
} else if (imageUris.isEmpty()) {
sendButton.showProgress(false);
- sendButton.showImageButton(textIsEmpty, sendEnabled);
+ sendButton.showImageButton(textIsEmpty, isSendButtonEnabled());
} else {
sendButton.showProgress(false);
- sendButton.showImageButton(false, sendEnabled);
+ sendButton.showImageButton(false, isSendButtonEnabled());
+ }
+ }
+
+ @Override
+ protected boolean isTextInputEnabled() {
+ return super.isTextInputEnabled() && !loadingUris;
+ }
+
+ @Override
+ protected boolean isSendButtonEnabled() {
+ return super.isSendButtonEnabled() && !loadingUris;
+ }
+
+ @Override
+ protected boolean isBombVisible() {
+ return super.isBombVisible() && (!textIsEmpty || !imageUris.isEmpty());
+ }
+
+ @Override
+ protected CharSequence getCurrentTextHint() {
+ if (imageUris.isEmpty()) {
+ return super.getCurrentTextHint();
+ } else {
+ Context ctx = textInput.getContext();
+ return ctx.getString(R.string.image_caption_hint);
}
}
@@ -91,11 +111,17 @@ public class TextAttachmentController extends TextSendController
if (canSend()) {
if (loadingUris) throw new AssertionError();
listener.onSendClick(textInput.getText(),
- attachmentManager.getAttachmentHeadersForSending());
- reset();
+ attachmentManager.getAttachmentHeadersForSending(),
+ expectedTimer).observe(listener, this::onSendStateChanged);
}
}
+ @Override
+ protected void onSendStateChanged(SendState sendState) {
+ super.onSendStateChanged(sendState);
+ if (sendState == SENT) reset();
+ }
+
@Override
protected boolean canSendEmptyText() {
return !imageUris.isEmpty();
@@ -154,6 +180,7 @@ public class TextAttachmentController extends TextSendController
private void onNewUris(boolean restart, List newUris) {
if (newUris.isEmpty()) return;
if (loadingUris) throw new AssertionError();
+ if (textIsEmpty) onStartingMessage();
loadingUris = true;
if (newUris.size() > MAX_ATTACHMENTS_PER_MESSAGE) {
newUris = newUris.subList(0, MAX_ATTACHMENTS_PER_MESSAGE);
@@ -161,7 +188,6 @@ public class TextAttachmentController extends TextSendController
}
imageUris.addAll(newUris);
updateViewState();
- textInput.setHint(R.string.image_caption_hint);
List items = ImagePreviewItem.fromUris(imageUris);
imagePreview.showPreview(items);
// store attachments and show preview when successful
@@ -207,8 +233,6 @@ public class TextAttachmentController extends TextSendController
}
private void reset() {
- // restore hint
- textInput.setHint(textHint);
// hide image layout
imagePreview.setVisibility(GONE);
// reset image URIs
@@ -303,7 +327,7 @@ public class TextAttachmentController extends TextSendController
}
@UiThread
- public interface AttachmentListener extends SendListener, LifecycleOwner {
+ public interface AttachmentListener extends SendListener {
void onAttachImage(Intent intent);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java
index e070a3dd4..0d6c432c7 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java
@@ -1,7 +1,9 @@
package org.briarproject.briar.android.view;
+import android.content.Context;
import android.os.Parcelable;
import android.view.View;
+import android.widget.Toast;
import com.google.android.material.snackbar.Snackbar;
@@ -12,11 +14,20 @@ import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List;
+import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import androidx.appcompat.app.AlertDialog;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import static android.widget.Toast.LENGTH_LONG;
import static com.google.android.material.snackbar.Snackbar.LENGTH_SHORT;
import static java.util.Collections.emptyList;
+import static org.briarproject.briar.android.view.TextSendController.SendState.ERROR;
+import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
+import static org.briarproject.briar.android.view.TextSendController.SendState.UNEXPECTED_TIMER;
+import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread
@NotNullByDefault
@@ -26,8 +37,12 @@ public class TextSendController implements TextInputListener {
protected final View compositeSendButton;
protected final SendListener listener;
- protected boolean ready = true, textIsEmpty = true;
+ protected boolean textIsEmpty = true;
+ private boolean ready = true;
+ private long currentTimer = NO_AUTO_DELETE_TIMER;
+ protected long expectedTimer = NO_AUTO_DELETE_TIMER;
+ private final CharSequence defaultHint;
private final boolean allowEmptyText;
public TextSendController(TextInputView v, SendListener listener,
@@ -36,31 +51,91 @@ public class TextSendController implements TextInputListener {
this.compositeSendButton.setOnClickListener(view -> onSendEvent());
this.listener = listener;
this.textInput = v.getEmojiTextInputView();
+ this.defaultHint = textInput.getHint();
this.allowEmptyText = allowEmptyText;
}
@Override
public void onTextIsEmptyChanged(boolean isEmpty) {
textIsEmpty = isEmpty;
+ if (!isEmpty) onStartingMessage();
updateViewState();
}
@Override
public void onSendEvent() {
if (canSend()) {
- listener.onSendClick(textInput.getText(), emptyList());
+ listener.onSendClick(textInput.getText(), emptyList(),
+ expectedTimer).observe(listener, this::onSendStateChanged);
}
}
+ @CallSuper
+ protected void onSendStateChanged(SendState sendState) {
+ if (sendState == SENT) {
+ textInput.clearText();
+ } else if (sendState == UNEXPECTED_TIMER) {
+ boolean enabled = expectedTimer == NO_AUTO_DELETE_TIMER;
+ showTimerChangedDialog(enabled);
+ } else if (sendState == ERROR) {
+ Toast.makeText(textInput.getContext(), R.string.message_error,
+ LENGTH_LONG).show();
+ }
+ }
+
+ /**
+ * Call whenever the user starts a new message,
+ * either by entering text or adding an attachment.
+ * This updates the expected auto-delete timer to the current value.
+ */
+ protected void onStartingMessage() {
+ expectedTimer = currentTimer;
+ }
+
public void setReady(boolean ready) {
this.ready = ready;
updateViewState();
}
+ /**
+ * Sets the current auto delete timer and updates the UI accordingly.
+ */
+ public void setAutoDeleteTimer(long timer) {
+ currentTimer = timer;
+ updateViewState();
+ }
+
+ @CallSuper
protected void updateViewState() {
- textInput.setEnabled(ready);
- compositeSendButton
- .setEnabled(ready && (!textIsEmpty || canSendEmptyText()));
+ textInput.setEnabled(isTextInputEnabled());
+ textInput.setHint(getCurrentTextHint());
+ compositeSendButton.setEnabled(isSendButtonEnabled());
+ if (compositeSendButton instanceof CompositeSendButton) {
+ CompositeSendButton sendButton =
+ (CompositeSendButton) compositeSendButton;
+ sendButton.setBombVisible(isBombVisible());
+ }
+ }
+
+ protected boolean isTextInputEnabled() {
+ return ready;
+ }
+
+ protected boolean isSendButtonEnabled() {
+ return ready && (!textIsEmpty || canSendEmptyText());
+ }
+
+ protected boolean isBombVisible() {
+ return currentTimer != NO_AUTO_DELETE_TIMER;
+ }
+
+ protected CharSequence getCurrentTextHint() {
+ if (currentTimer == NO_AUTO_DELETE_TIMER) {
+ return defaultHint;
+ } else {
+ Context ctx = textInput.getContext();
+ return ctx.getString(R.string.message_hint_auto_delete);
+ }
}
protected final boolean canSend() {
@@ -76,6 +151,23 @@ public class TextSendController implements TextInputListener {
return allowEmptyText;
}
+ private void showTimerChangedDialog(boolean enabled) {
+ Context ctx = textInput.getContext();
+ int message =
+ enabled ? R.string.auto_delete_changed_warning_message_enabled :
+ R.string.auto_delete_changed_warning_message_disabled;
+ new AlertDialog.Builder(ctx, R.style.BriarDialogTheme)
+ .setTitle(R.string.auto_delete_changed_warning_title)
+ .setMessage(message)
+ .setPositiveButton(R.string.auto_delete_changed_warning_send,
+ (dialog, which) -> {
+ expectedTimer = currentTimer;
+ onSendEvent();
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
@Nullable
public Parcelable onSaveInstanceState(@Nullable Parcelable superState) {
return superState;
@@ -86,9 +178,11 @@ public class TextSendController implements TextInputListener {
return state;
}
- @UiThread
- public interface SendListener {
- void onSendClick(@Nullable String text, List headers);
+ public enum SendState {SENT, ERROR, UNEXPECTED_TIMER}
+
+ public interface SendListener extends LifecycleOwner {
+ LiveData onSendClick(@Nullable String text,
+ List headers, long expectedAutoDeleteTimer);
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/widget/OnboardingFullDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/widget/OnboardingFullDialogFragment.java
new file mode 100644
index 000000000..cf2fd2d72
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/widget/OnboardingFullDialogFragment.java
@@ -0,0 +1,66 @@
+package org.briarproject.briar.android.widget;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.R;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.DialogFragment;
+
+@NotNullByDefault
+public class OnboardingFullDialogFragment extends DialogFragment {
+
+ public final static String TAG =
+ OnboardingFullDialogFragment.class.getName();
+
+ private final static String RES_TITLE = "resTitle";
+ private final static String RES_CONTENT = "resContent";
+
+ public static OnboardingFullDialogFragment newInstance(@StringRes int title,
+ @StringRes int content) {
+ Bundle args = new Bundle();
+ args.putInt(RES_TITLE, title);
+ args.putInt(RES_CONTENT, content);
+ OnboardingFullDialogFragment f = new OnboardingFullDialogFragment();
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(DialogFragment.STYLE_NORMAL,
+ R.style.BriarFullScreenDialogTheme);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_onboarding_full,
+ container, false);
+
+ Bundle args = requireArguments();
+
+ Toolbar toolbar = view.findViewById(R.id.toolbar);
+ toolbar.setNavigationOnClickListener(v -> dismiss());
+ toolbar.setTitle(args.getInt(RES_TITLE));
+
+ TextView contentView = view.findViewById(R.id.contentView);
+ contentView.setText(args.getInt(RES_CONTENT));
+
+ view.findViewById(R.id.button).setOnClickListener(v -> dismiss());
+
+ return view;
+ }
+
+}
diff --git a/briar-android/src/main/res/drawable/ic_bomb.xml b/briar-android/src/main/res/drawable/ic_bomb.xml
new file mode 100644
index 000000000..c874afd02
--- /dev/null
+++ b/briar-android/src/main/res/drawable/ic_bomb.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/briar-android/src/main/res/layout/fragment_conversation_settings.xml b/briar-android/src/main/res/layout/fragment_conversation_settings.xml
new file mode 100644
index 000000000..698840626
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_conversation_settings.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/fragment_onboarding_full.xml b/briar-android/src/main/res/layout/fragment_onboarding_full.xml
new file mode 100644
index 000000000..82ae00f69
--- /dev/null
+++ b/briar-android/src/main/res/layout/fragment_onboarding_full.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml
index 020bca4b3..cfc78c4f8 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml
@@ -1,4 +1,6 @@
+
+
+
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml
index 27056a3da..17fc59cf7 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml
@@ -1,4 +1,6 @@
+
+
+ app:layout_constraintTop_toBottomOf="@+id/text"
+ tools:ignore="UseCompoundDrawables">
+
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml
index d946046e4..34832689d 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml
@@ -1,14 +1,19 @@
-
+ android:background="@drawable/list_item_background_selectable"
+ android:orientation="vertical">
+
+
+
-
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml
index a92cf249c..63b679572 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml
@@ -1,4 +1,6 @@
+
+
+
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml
index 6ac5ef61a..7e03d0a7f 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml
@@ -1,10 +1,14 @@
-
+ android:background="@drawable/list_item_background_selectable"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/message_bubble_margin">
+
+
+
+
@@ -89,4 +104,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/briar-android/src/main/res/layout/list_item_conversation_notice_in.xml b/briar-android/src/main/res/layout/list_item_conversation_notice_in.xml
index 1725f4315..0a6a2fcd1 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_notice_in.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_notice_in.xml
@@ -5,8 +5,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable"
- android:orientation="vertical"
- android:paddingTop="@dimen/message_bubble_margin">
+ android:orientation="vertical">
+
+
+ tools:text="Short message"
+ tools:visibility="visible" />
+
+
-
\ No newline at end of file
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_notice_out.xml b/briar-android/src/main/res/layout/list_item_conversation_notice_out.xml
index d99ae0ce6..ff473129c 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_notice_out.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_notice_out.xml
@@ -8,6 +8,8 @@
android:orientation="vertical"
android:paddingTop="@dimen/message_bubble_margin">
+
+
+ tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."
+ tools:visibility="visible" />
+
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_request.xml b/briar-android/src/main/res/layout/list_item_conversation_request.xml
index 902d10fc3..9ac5087a0 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_request.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_request.xml
@@ -5,8 +5,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable"
- android:orientation="vertical"
- android:paddingTop="@dimen/message_bubble_margin">
+ android:orientation="vertical">
+
+
+
+
-
\ No newline at end of file
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_top_notice_in.xml b/briar-android/src/main/res/layout/list_item_conversation_top_notice_in.xml
new file mode 100644
index 000000000..1d53bc8de
--- /dev/null
+++ b/briar-android/src/main/res/layout/list_item_conversation_top_notice_in.xml
@@ -0,0 +1,20 @@
+
+
diff --git a/briar-android/src/main/res/layout/list_item_conversation_top_notice_out.xml b/briar-android/src/main/res/layout/list_item_conversation_top_notice_out.xml
new file mode 100644
index 000000000..ca479733d
--- /dev/null
+++ b/briar-android/src/main/res/layout/list_item_conversation_top_notice_out.xml
@@ -0,0 +1,22 @@
+
+
diff --git a/briar-android/src/main/res/layout/view_composite_send_button.xml b/briar-android/src/main/res/layout/view_composite_send_button.xml
index b72677e71..062e36740 100644
--- a/briar-android/src/main/res/layout/view_composite_send_button.xml
+++ b/briar-android/src/main/res/layout/view_composite_send_button.xml
@@ -37,6 +37,18 @@
app:srcCompat="@drawable/social_send_now_white"
app:tint="@color/briar_accent" />
+
+
-
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 34314f7fc..67431f249 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -157,7 +157,9 @@
Tap the + icon to add a contactNo messages.No messages to show
- Type message
+ New message
+ New disappearing message
+ Error sending messageAdd a caption (optional)Attach imageCould not attach image(s)
@@ -165,6 +167,32 @@
Image format unsupported: %sChange contact nameContact name
+ Disappearing messages
+
+ Your messages will disappear after %1$s. %2$s
+
+ Your messages will not disappear. %1$s
+
+ %1$s\'s messages will disappear after %2$s. %3$s
+
+ %d minute
+ %d minutes
+
+
+ %d hour
+ %d hours
+
+
+ %d day
+ %d days
+
+
+ %1$s\'s messages will not disappear. %2$s
+ Tap to learn more.
+ Disappearing messages changed
+ Since you started composing your message, disappearing messages have been enabled.
+ Since you started composing your message, disappearing messages have been disabled.
+ Send anywayDelete all messagesConfirm Message DeletionAre you sure that you want to delete all messages?
@@ -172,7 +200,6 @@
Messages related to ongoing invitations and introductions cannot be deleted until they conclude.Messages related to ongoing introductions cannot be deleted until they conclude.Messages related to ongoing invitations cannot be deleted until they conclude.
- Partly downloaded messages cannot be deleted until they have finished downloading.To delete an invitation or introduction, you need to select the request and the response.To delete an introduction, you need to select the request and the response.To delete an invitation, you need to select the request and the response.
@@ -290,6 +317,7 @@
You accepted the introduction to %1$s.Before %1$s gets added to your contacts, they need to accept the introduction as well. This might take some time.You declined the introduction to %1$s.
+ The introduction to %1$s was automatically declined.%1$s accepted the introduction to %2$s.%1$s declined the introduction to %2$s.%1$s says that %2$s declined the introduction.
@@ -338,6 +366,7 @@
You accepted the group invitation from %s.You declined the group invitation from %s.
+ The group invitation from %s was automatically declined.%s accepted the group invitation.%s declined the group invitation.Only the creator can invite new members to the group. Below are all current members of the group.
@@ -391,6 +420,7 @@
Already sharingYou accepted the forum invitation from %s.You declined the forum invitation from %s.
+ The forum invitation from %s was automatically declined.%s accepted the forum invitation.%s declined the forum invitation.
@@ -428,6 +458,7 @@
Blog shared with chosen contactsYou accepted the blog invitation from %s.You declined the blog invitation from %s.
+ The blog invitation from %s was automatically declined.%s accepted the blog invitation.%s declined the blog invitation.%1$s has shared the blog \"%2$s\" with you.
@@ -551,6 +582,20 @@
Choose ringtoneCannot load ringtone
+
+ Disappearing messages
+ Turning on this setting will make new
+ messages in this conversation automatically disappear after 7\u00A0days.
+ \n\nThe countdown for the sender\'s copy of the message starts after it has been delivered.
+ The countdown starts for the recipient after they have read the message.
+ \n\nMessages that will disappear are marked with a bomb icon.
+ \n\nKeep in mind that recipients can still make copies of the messages you send.
+ \n\nIf you change this setting, it will apply to your new messages immediately and to your
+ contact\'s messages once they receive your next message.
+ Your contact can also change this setting for the both of you.
+ Learn more
+ Make future messages in this conversation automatically disappear after 7\u00A0days.
+
Send feedback
diff --git a/briar-android/src/main/res/values/themes.xml b/briar-android/src/main/res/values/themes.xml
index 211fe6ebc..72eac331f 100644
--- a/briar-android/src/main/res/values/themes.xml
+++ b/briar-android/src/main/res/values/themes.xml
@@ -43,6 +43,16 @@
true
+
+
+
+