Compare commits

...

64 Commits

Author SHA1 Message Date
akwizgran
e3e0d3bf59 Disable image attachments and profile pictures for user testing. 2021-02-05 14:02:43 +00:00
akwizgran
761647dde4 Merge branch 'incoming-bombs' into '804-self-destructing-messages'
Fix bomb icon color in incoming image messages without text

See merge request briar/briar!1358
2021-02-05 13:50:04 +00:00
akwizgran
51ddd47502 Merge branch '1864-warning-when-timer-changed' into '804-self-destructing-messages'
Show warning dialog when the expected timer differs from the current timer

See merge request briar/briar!1328
2021-02-05 13:14:08 +00:00
Torsten Grote
eb72754d8d Get rid of SENDING state and publish new live data in order on UiThread 2021-02-05 10:04:02 -03:00
Torsten Grote
aeaa549d6f Show outgoing message status icon in same color as time 2021-02-04 17:12:25 -03:00
Torsten Grote
22fb2df3dc Fix bomb icon color
in incoming image messages without text (on old phones)
2021-02-04 16:58:28 -03:00
Torsten Grote
906ee6c735 Return LiveData when sending message 2021-02-04 12:21:51 +00:00
Torsten Grote
6a81e805cc Show warning dialog when auto-delete timer has changed since starting to compose message 2021-02-04 12:14:11 +00:00
Torsten Grote
f6ccf885e6 Add "Tap to learn more" to message bubbles for timer changes 2021-02-03 15:08:38 +00:00
akwizgran
d821696c6a Provide clock for UI tests. 2021-02-03 15:08:38 +00:00
akwizgran
cf9162b694 Add some comments. 2021-02-03 15:08:38 +00:00
akwizgran
48f0fd0dea Sync acks for initial messages when setting up integration tests. 2021-02-03 15:08:38 +00:00
akwizgran
73bbfe3993 Allow time travel in integration tests. 2021-02-03 15:08:37 +00:00
akwizgran
096249ad32 Inject DefaultTaskSchedulerModule.EagerSingletons at startup in headless app. 2021-02-03 15:08:37 +00:00
akwizgran
ebcc789977 Refactor integration tests to allow clock to be replaced. 2021-02-03 15:08:37 +00:00
Sebastian Kürten
4d7bc18155 Introduce conversation settings screen 2021-02-03 15:08:35 +00:00
Torsten Grote
7682bf9553 Create group invitation with read-write transaction
because the AutoDeleteManager needs to change the DB
and otherwise crashes.

Closes #1863
2021-02-03 15:07:39 +00:00
Torsten Grote
e8428df700 Make view state of text send UI easier to reason about
and fix bugs with bomb badge and hint display
2021-02-03 15:07:39 +00:00
Torsten Grote
a7cec213b1 Show bomb badge in same style as send button 2021-02-03 15:07:37 +00:00
Torsten Grote
dd4afd7c39 Show a bomb badge on the send button when disappearing messages is active 2021-02-03 15:07:00 +00:00
Torsten Grote
838fc46af4 Use a different hint in conversation when message will disappear
and keep the hint updated when the auto-delete timer changes
2021-02-03 15:06:34 +00:00
Torsten Grote
d29ade44fb Broadcast event when auto delete timer is mirrored 2021-02-03 15:06:34 +00:00
Torsten Grote
76d29d4a18 Remove mirrored timer texts
as we can't detect reliably if a timer setting was mirrored or manually changed.

Also remove item update optimization from adapter as this can cause issues when items already exist.
2021-02-03 15:06:34 +00:00
Torsten Grote
04ef837307 Show timer change notices in private conversations 2021-02-03 15:06:34 +00:00
Torsten Grote
4a73daa214 Allow setting a self-destruct timer
This is a rough prototype of #1837 meant to make testing the UI easier.
2021-02-03 15:06:34 +00:00
akwizgran
9f9d5642c2 Use Collections.sort() to satisfy Animal Sniffer. 2021-02-03 15:06:34 +00:00
akwizgran
707e7b06df Add integration tests for timer mirroring. 2021-02-03 15:06:34 +00:00
akwizgran
b5f69b5212 Add method for UI and tests to get current timer. 2021-02-03 15:06:34 +00:00
akwizgran
a7e5924137 Update integration tests. 2021-02-03 15:06:34 +00:00
akwizgran
105dc08121 Don't receive auto-delete timer from remote accept message as introducee. 2021-02-03 15:06:34 +00:00
akwizgran
c49db25e96 Hook up incoming messages to the auto-delete manager. 2021-02-03 15:06:33 +00:00
akwizgran
34c1490a8b Mirror the remote auto-delete timer. 2021-02-03 15:06:33 +00:00
akwizgran
d7b60c5d5b Add integration tests for auto-delete timer. 2021-02-03 15:06:33 +00:00
akwizgran
a4a8fea29d Forwarded accept messages aren't visible to the introducee. 2021-02-03 15:06:33 +00:00
akwizgran
396b433030 Only use conversation timestamp for messages that will be visible in conversation. 2021-02-03 15:06:33 +00:00
akwizgran
8b1badc715 Get timestamp for abort message in same way as other messages. 2021-02-03 15:06:33 +00:00
akwizgran
b1d6e81c73 Look up auto-delete timer when creating private group invitation. 2021-02-03 15:06:33 +00:00
akwizgran
7204f8ea0b Use the right timestamp when signing private group invitation. 2021-02-03 15:06:33 +00:00
akwizgran
4ab0d4b24b Provide TransactionManager. 2021-02-03 15:06:33 +00:00
akwizgran
a8a905fb87 Look up conversation timestamp when creating group invitation messages. 2021-02-03 15:06:33 +00:00
akwizgran
73b0e0356f Move lookup of latest conversation timestamp to core for blog and forum sharing. 2021-02-03 15:06:33 +00:00
akwizgran
952cc9265f Move lookup of latest conversation timestamp to core. 2021-02-03 15:06:33 +00:00
akwizgran
104587838c Add transactional variant of getGroupCount(). 2021-02-03 15:06:33 +00:00
akwizgran
1a91be403b Send current minor version of messaging client to contacts. 2021-02-03 15:06:32 +00:00
Torsten Grote
f2e1a1bf73 Show bomb icon for messages with auto-destruct timer 2021-02-03 15:06:30 +00:00
akwizgran
fe360f28fd Check that timer argument is legal before storing. 2021-02-02 16:57:13 +00:00
akwizgran
8247e10e82 Add unit tests for AutoDeleteManagerImpl. 2021-02-02 16:57:13 +00:00
akwizgran
e8c029a7b4 Implement AutoDeleteManager. 2021-02-02 16:57:13 +00:00
akwizgran
5cdbd58ac3 Add dummy implementation of AutoDeleteManager. 2021-02-02 16:57:13 +00:00
akwizgran
51a308c6f4 Refactor auto-delete code from Bramble to Briar. 2021-02-02 16:57:11 +00:00
akwizgran
62215be369 Rewrap lines. 2021-02-02 16:56:15 +00:00
akwizgran
7a8f07a8c6 Factor out methods for storing and retrieving contact ID. 2021-02-02 16:56:14 +00:00
akwizgran
c3201590de Factor out method for validating auto-delete timers. 2021-02-02 16:56:14 +00:00
akwizgran
f002378bbf Update comments. 2021-02-02 16:56:14 +00:00
akwizgran
31ca3e2cb5 Add unit tests for validating auto-delete timer. 2021-02-02 16:56:14 +00:00
akwizgran
3a11cb32c4 Update private group invitation client to include self-destruct timers. 2021-02-02 16:56:14 +00:00
akwizgran
aee663fcc0 Update blog and forum sharing clients to include self-destruct timers. 2021-02-02 16:56:14 +00:00
akwizgran
b266b78a49 Update message parsing and encoding to include auto-delete timer. 2021-02-02 16:56:14 +00:00
akwizgran
02f1385ed2 Update introduction validator to support auto-delete timers. 2021-02-02 16:56:14 +00:00
akwizgran
c683038343 Add constant for NO_AUTO_DELETE_TIMER, address review comments. 2021-02-02 16:56:11 +00:00
akwizgran
336dd8de5b Add unit tests for private message validation. 2021-02-02 16:43:48 +00:00
akwizgran
7e4460b4ea Fix comments in PrivateMessageValidator. 2021-02-02 16:37:46 +00:00
akwizgran
152b3f1967 Add integration test for auto-delete timer in private messages. 2021-02-02 16:37:46 +00:00
akwizgran
ae461b9878 Add auto-deletion timer to private messages. 2021-02-02 16:37:46 +00:00
195 changed files with 5811 additions and 1505 deletions

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.api.client; package org.briarproject.bramble.api.client;
import org.briarproject.bramble.api.FormatException; 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.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
@@ -119,4 +120,17 @@ public interface ClientHelper {
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap( Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
BdfDictionary properties) throws FormatException; 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;
} }

View File

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

View File

@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault @NotNullByDefault
public class ValidationUtils { public class ValidationUtils {
@@ -64,4 +66,9 @@ public class ValidationUtils {
if (dictionary != null && dictionary.size() != size) if (dictionary != null && dictionary.size() != size)
throw new FormatException(); 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();
}
} }

View File

@@ -0,0 +1,8 @@
package org.briarproject.bramble.test;
public interface TimeTravel {
void setCurrentTimeMillis(long now) throws InterruptedException;
void addCurrentTimeMillis(long add) throws InterruptedException;
}

View File

@@ -21,7 +21,6 @@ import org.briarproject.bramble.rendezvous.RendezvousModule;
import org.briarproject.bramble.settings.SettingsModule; import org.briarproject.bramble.settings.SettingsModule;
import org.briarproject.bramble.sync.SyncModule; import org.briarproject.bramble.sync.SyncModule;
import org.briarproject.bramble.sync.validation.ValidationModule; import org.briarproject.bramble.sync.validation.ValidationModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.bramble.transport.TransportModule; import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.bramble.versioning.VersioningModule; import org.briarproject.bramble.versioning.VersioningModule;
@@ -29,7 +28,6 @@ import dagger.Module;
@Module(includes = { @Module(includes = {
ClientModule.class, ClientModule.class,
ClockModule.class,
ConnectionModule.class, ConnectionModule.class,
ContactModule.class, ContactModule.class,
CryptoModule.class, CryptoModule.class,

View File

@@ -2,11 +2,13 @@ package org.briarproject.bramble.client;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper; 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.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyParser; import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey; import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary; 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.BdfList;
import org.briarproject.bramble.api.data.BdfReader; import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory; import org.briarproject.bramble.api.data.BdfReaderFactory;
@@ -39,6 +41,7 @@ import java.util.Map.Entry;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; 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.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
@@ -389,4 +392,27 @@ class ClientHelperImpl implements ClientHelper {
return tpMap; 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);
}
}
} }

View File

@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
// Metadata keys // Metadata keys
String MSG_KEY_UPDATE_VERSION = "version"; String MSG_KEY_UPDATE_VERSION = "version";
String MSG_KEY_LOCAL = "local"; String MSG_KEY_LOCAL = "local";
String GROUP_KEY_CONTACT_ID = "contactId";
} }

View File

@@ -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.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; 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.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_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
@@ -161,13 +160,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
db.addGroup(txn, g); db.addGroup(txn, g);
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
// Attach the contact ID to the group // Attach the contact ID to the group
BdfDictionary meta = new BdfDictionary(); clientHelper.setContactId(txn, g.getId(), c.getId());
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
try {
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new AssertionError(e);
}
// Create and store the first local update // Create and store the first local update
List<ClientVersion> versions = new ArrayList<>(clients); List<ClientVersion> versions = new ArrayList<>(clients);
Collections.sort(versions); Collections.sort(versions);
@@ -229,7 +222,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
Map<ClientMajorVersion, Visibility> after = Map<ClientMajorVersion, Visibility> after =
getVisibilities(newLocalStates, newRemoteStates); getVisibilities(newLocalStates, newRemoteStates);
// Call hooks for any visibilities that have changed // 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)) { if (!before.equals(after)) {
Contact contact = db.getContact(txn, c); Contact contact = db.getContact(txn, c);
callVisibilityHooks(txn, contact, before, after); callVisibilityHooks(txn, contact, before, after);
@@ -521,17 +514,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
storeUpdate(txn, g, states, 1); 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<ClientState> updateStatesFromRemoteStates( private List<ClientState> updateStatesFromRemoteStates(
List<ClientState> oldLocalStates, List<ClientState> remoteStates) { List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
Set<ClientMajorVersion> remoteSet = new HashSet<>(); Set<ClientMajorVersion> remoteSet = new HashSet<>();

View File

@@ -1,18 +1,18 @@
package org.briarproject.bramble; package org.briarproject.bramble;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule; import org.briarproject.bramble.system.TimeTravelModule;
public interface BrambleCoreIntegrationTestEagerSingletons public interface BrambleCoreIntegrationTestEagerSingletons
extends BrambleCoreEagerSingletons { extends BrambleCoreEagerSingletons {
void inject(DefaultTaskSchedulerModule.EagerSingletons init); void inject(TimeTravelModule.EagerSingletons init);
class Helper { class Helper {
public static void injectEagerSingletons( public static void injectEagerSingletons(
BrambleCoreIntegrationTestEagerSingletons c) { BrambleCoreIntegrationTestEagerSingletons c) {
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c); BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
c.inject(new DefaultTaskSchedulerModule.EagerSingletons()); c.inject(new TimeTravelModule.EagerSingletons());
} }
} }
} }

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ package org.briarproject.bramble.test;
import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule; import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule; import org.briarproject.bramble.event.DefaultEventExecutorModule;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule; import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@@ -12,11 +12,11 @@ import dagger.Provides;
@Module(includes = { @Module(includes = {
DefaultBatteryManagerModule.class, DefaultBatteryManagerModule.class,
DefaultEventExecutorModule.class, DefaultEventExecutorModule.class,
DefaultTaskSchedulerModule.class,
DefaultWakefulIoExecutorModule.class, DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class, TestDatabaseConfigModule.class,
TestPluginConfigModule.class, TestPluginConfigModule.class,
TestSecureRandomModule.class TestSecureRandomModule.class,
TimeTravelModule.class
}) })
public class BrambleCoreIntegrationTestModule { public class BrambleCoreIntegrationTestModule {

View File

@@ -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.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage; import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId; 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_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -60,8 +59,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
private final ClientId clientId = getClientId(); private final ClientId clientId = getClientId();
private final long now = System.currentTimeMillis(); private final long now = System.currentTimeMillis();
private final Transaction txn = new Transaction(null, false); 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() { private ClientVersioningManagerImpl createInstance() {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -123,8 +120,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).addGroup(txn, contactGroup); oneOf(db).addGroup(txn, contactGroup);
oneOf(db).setGroupVisibility(txn, contact.getId(), oneOf(db).setGroupVisibility(txn, contact.getId(),
contactGroup.getId(), SHARED); contactGroup.getId(), SHARED);
oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(), oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
groupMeta); contact.getId());
oneOf(clock).currentTimeMillis(); oneOf(clock).currentTimeMillis();
will(returnValue(now)); will(returnValue(now));
oneOf(clientHelper).createMessage(contactGroup.getId(), now, oneOf(clientHelper).createMessage(contactGroup.getId(), now,
@@ -460,9 +457,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).deleteMessage(txn, oldRemoteUpdateId); oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId); oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
// Get contact ID // Get contact ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
// No states or visibilities have changed // No states or visibilities have changed
}}); }});
@@ -492,10 +488,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Get client ID // Get contact ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
// No states or visibilities have changed // No states or visibilities have changed
}}); }});
@@ -546,8 +541,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of( BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L), new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true)); new BdfEntry(MSG_KEY_LOCAL, true));
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate); oneOf(clientHelper).toList(newRemoteUpdate);
@@ -577,9 +570,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
oneOf(db).getContact(txn, contact.getId()); oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact)); will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility); oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
@@ -619,8 +611,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of( BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L), new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true)); new BdfEntry(MSG_KEY_LOCAL, true));
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate); oneOf(clientHelper).toList(newRemoteUpdate);
@@ -650,9 +640,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
oneOf(db).getContact(txn, contact.getId()); oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact)); will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE); oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);

View File

@@ -8,9 +8,9 @@ import org.briarproject.bramble.system.JavaSystemModule;
import dagger.Module; import dagger.Module;
@Module(includes = { @Module(includes = {
CircumventionModule.class,
JavaNetworkModule.class, JavaNetworkModule.class,
JavaSystemModule.class, JavaSystemModule.class,
CircumventionModule.class,
SocksModule.class SocksModule.class
}) })
public class BrambleJavaModule { public class BrambleJavaModule {

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
@@ -16,6 +17,7 @@ import dagger.Component;
@Component(modules = { @Component(modules = {
AppModule.class, AppModule.class,
AttachmentModule.class, AttachmentModule.class,
ClockModule.class,
MediaModule.class, MediaModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
@@ -17,6 +18,7 @@ import dagger.Component;
@Component(modules = { @Component(modules = {
AppModule.class, AppModule.class,
AttachmentModule.class, AttachmentModule.class,
ClockModule.class,
MediaModule.class, MediaModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,

View File

@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
@@ -41,6 +42,7 @@ import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager; import org.briarproject.briar.api.android.LockManager;
import org.briarproject.briar.api.android.ScreenFilterMonitor; import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.attachment.AttachmentReader; 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.BlogManager;
import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -75,6 +77,7 @@ import dagger.Component;
BriarAccountModule.class, BriarAccountModule.class,
AppModule.class, AppModule.class,
AttachmentModule.class, AttachmentModule.class,
ClockModule.class,
MediaModule.class MediaModule.class
}) })
public interface AndroidComponent public interface AndroidComponent
@@ -179,6 +182,8 @@ public interface AndroidComponent
AndroidWakeLockManager wakeLockManager(); AndroidWakeLockManager wakeLockManager();
AutoDeleteManager autoDeleteManager();
void inject(SignInReminderReceiver briarService); void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService); void inject(BriarService briarService);

View File

@@ -36,9 +36,9 @@ import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule;
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule; import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.privategroup.list.GroupListModule; import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.reporting.DevReportModule; import org.briarproject.briar.android.reporting.DevReportModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.sharing.SharingModule; import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.test.TestAvatarCreatorImpl; import org.briarproject.briar.android.test.TestAvatarCreatorImpl;
import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.android.viewmodel.ViewModelModule;
@@ -68,7 +68,6 @@ import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = { @Module(includes = {
SetupModule.class, SetupModule.class,
@@ -269,12 +268,12 @@ public class AppModule {
@Override @Override
public boolean shouldEnableImageAttachments() { public boolean shouldEnableImageAttachments() {
return IS_DEBUG_BUILD; return false;
} }
@Override @Override
public boolean shouldEnableProfilePictures() { public boolean shouldEnableProfilePictures() {
return IS_DEBUG_BUILD; return false;
} }
}; };
} }

View File

@@ -27,6 +27,7 @@ import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity; import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.AliasDialogFragment; import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity; 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.ImageActivity;
import org.briarproject.briar.android.conversation.ImageFragment; import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.CreateForumActivity;
@@ -238,4 +239,6 @@ public interface ActivityComponent {
void inject(ConfirmAvatarDialogFragment fragment); void inject(ConfirmAvatarDialogFragment fragment);
void inject(ConversationSettingsDialog dialog);
} }

View File

@@ -20,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List; import java.util.List;
@@ -27,6 +28,9 @@ import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.view.View.FOCUS_DOWN; import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
@@ -34,6 +38,7 @@ import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID; import static org.briarproject.briar.android.blog.BasePostFragment.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; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -121,8 +126,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
ui.input.hideSoftKeyboard(); ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text, feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) { new UiExceptionHandler<DbException>(this) {
@@ -132,6 +137,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
} }
}); });
finish(); finish();
return new MutableLiveData<>(SENT);
} }
private void showProgressBar() { private void showProgressBar() {

View File

@@ -31,12 +31,16 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; 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; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -112,8 +116,8 @@ public class WriteBlogPostActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError(); if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar // hide publish button, show progress bar
@@ -122,6 +126,7 @@ public class WriteBlogPostActivity extends BriarActivity
progressBar.setVisibility(VISIBLE); progressBar.setVisibility(VISIBLE);
storePost(text); storePost(text);
return new MutableLiveData<>(SENT);
} }
private void storePost(String text) { private void storePost(String text) {

View File

@@ -31,6 +31,8 @@ import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
// TODO: we can probably switch to androidx DialogFragment here but need to
// test this properly
public class AliasDialogFragment extends AppCompatDialogFragment { public class AliasDialogFragment extends AppCompatDialogFragment {
final static String TAG = AliasDialogFragment.class.getName(); final static String TAG = AliasDialogFragment.class.getName();

View File

@@ -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.AttachmentCache;
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
import org.briarproject.briar.android.forum.ForumActivity; 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.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
@@ -59,6 +60,7 @@ import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener; import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; 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.android.AndroidNotificationManager;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -138,12 +140,14 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.android.view.AuthorView.setAvatar; 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_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; 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 @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextCache, implements BaseFragmentListener, EventListener, ConversationListener,
AttachmentCache, AttachmentListener, ActionMode.Callback { TextCache, AttachmentCache, AttachmentListener, ActionMode.Callback {
public static final String CONTACT_ID = "briar.CONTACT_ID"; public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -268,15 +272,11 @@ public class ConversationActivity extends BriarActivity
ImagePreview imagePreview = findViewById(R.id.imagePreview); ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView, sendController = new TextAttachmentController(textInputView,
imagePreview, this, viewModel); imagePreview, this, viewModel);
viewModel.hasImageSupport().observe(this, new Observer<Boolean>() { observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
@Override if (format != TEXT_ONLY) {
public void onChanged(@Nullable Boolean hasSupport) {
if (hasSupport != null && hasSupport) {
// TODO: remove cast when removing feature flag // TODO: remove cast when removing feature flag
((TextAttachmentController) sendController) ((TextAttachmentController) sendController)
.setImagesSupported(); .setImagesSupported();
viewModel.hasImageSupport().removeObserver(this);
}
} }
}); });
} else { } else {
@@ -286,6 +286,9 @@ public class ConversationActivity extends BriarActivity
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH); textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false); textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom); textInputView.setOnKeyboardShownListener(this::scrollToBottom);
viewModel.getAutoDeleteTimer().observe(this, timer ->
sendController.setAutoDeleteTimer(timer));
} }
private void scrollToBottom() { private void scrollToBottom() {
@@ -369,6 +372,12 @@ public class ConversationActivity extends BriarActivity
// enable alias action if available // enable alias action if available
observeOnce(viewModel.getContactItem(), this, contact -> observeOnce(viewModel.getContactItem(), this, contact ->
menu.findItem(R.id.action_set_alias).setEnabled(true)); menu.findItem(R.id.action_set_alias).setEnabled(true));
// show auto-delete timer setting only, if contacts supports it
observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
boolean visible = format == TEXT_IMAGES_AUTO_DELETE;
MenuItem item = menu.findItem(R.id.action_conversation_settings);
item.setVisible(visible);
});
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -390,6 +399,10 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show( AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG); getSupportFragmentManager(), AliasDialogFragment.TAG);
return true; return true;
case R.id.action_conversation_settings:
if (contactId == null) return false;
onAutoDeleteTimerNoticeClicked();
return true;
case R.id.action_delete_all_messages: case R.id.action_delete_all_messages:
askToDeleteAllMessages(); askToDeleteAllMessages();
return true; return true;
@@ -640,8 +653,8 @@ public class ConversationActivity extends BriarActivity
supportFinishAfterTransition(); supportFinishAfterTransition();
} }
} else if (e instanceof ConversationMessageReceivedEvent) { } else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent p = ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent) e; (ConversationMessageReceivedEvent<?>) e;
if (p.getContactId().equals(contactId)) { if (p.getContactId().equals(contactId)) {
LOG.info("Message received, adding"); LOG.info("Message received, adding");
onNewConversationMessage(p.getMessageHeader()); onNewConversationMessage(p.getMessageHeader());
@@ -735,20 +748,13 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) { List<AttachmentHeader> attachmentHeaders,
long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty()) if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError(); throw new AssertionError();
long timestamp = System.currentTimeMillis(); return viewModel
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); .sendMessage(text, attachmentHeaders, expectedAutoDeleteTimer);
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;
} }
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) { private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
@@ -958,13 +964,11 @@ public class ConversationActivity extends BriarActivity
adapter.notifyItemChanged(position, item); adapter.notifyItemChanged(position, item);
} }
runOnDbThread(() -> { runOnDbThread(() -> {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try { try {
switch (item.getRequestType()) { switch (item.getRequestType()) {
case INTRODUCTION: case INTRODUCTION:
respondToIntroductionRequest(item.getSessionId(), respondToIntroductionRequest(item.getSessionId(),
accept, timestamp); accept);
break; break;
case FORUM: case FORUM:
respondToForumRequest(item.getSessionId(), accept); respondToForumRequest(item.getSessionId(), accept);
@@ -1038,11 +1042,18 @@ public class ConversationActivity extends BriarActivity
ActivityCompat.startActivity(this, i, options.toBundle()); ActivityCompat.startActivity(this, i, options.toBundle());
} }
@Override
public void onAutoDeleteTimerNoticeClicked() {
ConversationSettingsDialog dialog =
ConversationSettingsDialog.newInstance(contactId);
dialog.show(getSupportFragmentManager(),
ConversationSettingsDialog.TAG);
}
@DatabaseExecutor @DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId, private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException { boolean accept) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, time, introductionManager.respondToIntroduction(contactId, sessionId, accept);
accept);
} }
@DatabaseExecutor @DatabaseExecutor

View File

@@ -13,6 +13,8 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter; import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.ItemReturningAdapter; import org.briarproject.briar.android.util.ItemReturningAdapter;
import java.util.Collection;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.selection.SelectionTracker; import androidx.recyclerview.selection.SelectionTracker;
@@ -20,13 +22,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool; import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@NotNullByDefault @NotNullByDefault
class ConversationAdapter class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder> extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
implements ItemReturningAdapter<ConversationItem> { implements ItemReturningAdapter<ConversationItem> {
private ConversationListener listener; private final ConversationListener listener;
private final RecycledViewPool imageViewPool; private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration; private final ImageItemDecoration imageItemDecoration;
@Nullable @Nullable
@@ -65,22 +68,20 @@ class ConversationAdapter
@LayoutRes int type) { @LayoutRes int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate( View v = LayoutInflater.from(viewGroup.getContext()).inflate(
type, viewGroup, false); type, viewGroup, false);
switch (type) { if (type == R.layout.list_item_conversation_msg_in) {
case R.layout.list_item_conversation_msg_in:
return new ConversationMessageViewHolder(v, listener, true, return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration); imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_msg_out: } else if (type == R.layout.list_item_conversation_msg_out) {
return new ConversationMessageViewHolder(v, listener, false, return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration); imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_notice_in: } else if (type == R.layout.list_item_conversation_notice_in) {
return new ConversationNoticeViewHolder(v, listener, true); return new ConversationNoticeViewHolder(v, listener, true);
case R.layout.list_item_conversation_notice_out: } else if (type == R.layout.list_item_conversation_notice_out) {
return new ConversationNoticeViewHolder(v, listener, false); return new ConversationNoticeViewHolder(v, listener, false);
case R.layout.list_item_conversation_request: } else if (type == R.layout.list_item_conversation_request) {
return new ConversationRequestViewHolder(v, listener, true); return new ConversationRequestViewHolder(v, listener, true);
default:
throw new IllegalArgumentException("Unknown ConversationItem");
} }
throw new IllegalArgumentException("Unknown ConversationItem");
} }
@Override @Override
@@ -107,22 +108,49 @@ class ConversationAdapter
return c1.equals(c2); return c1.equals(c2);
} }
@Override
public void add(ConversationItem item) {
items.beginBatchedUpdates();
items.add(item);
updateTimersInBatch();
items.endBatchedUpdates();
}
@Override
public void addAll(Collection<ConversationItem> itemsToAdd) {
items.beginBatchedUpdates();
// there can be items already in the adapter
// SortedList takes care of duplicates and detecting changed items
items.addAll(itemsToAdd);
updateTimersInBatch();
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<String> tracker) { void setSelectionTracker(SelectionTracker<String> tracker) {
this.tracker = tracker; this.tracker = tracker;
} }
@Nullable
ConversationItem getLastItem() {
if (items.size() > 0) {
return items.get(items.size() - 1);
} else {
return null;
}
}
SparseArray<ConversationItem> getOutgoingMessages() { SparseArray<ConversationItem> getOutgoingMessages() {
SparseArray<ConversationItem> messages = new SparseArray<>(); SparseArray<ConversationItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i); ConversationItem item = items.get(i);
if (!item.isIncoming()) { if (!item.isIncoming()) {

View File

@@ -9,6 +9,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
import static org.briarproject.bramble.util.StringUtils.toHexString; import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -22,20 +23,25 @@ abstract class ConversationItem {
protected String text; protected String text;
private final MessageId id; private final MessageId id;
private final GroupId groupId; private final GroupId groupId;
private final long time; private final long time, autoDeleteTimer;
private final boolean isIncoming; private final boolean isIncoming;
private boolean read, sent, seen; private final LiveData<String> contactName;
private boolean read, sent, seen, showTimerNotice;
ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h) { ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h,
LiveData<String> contactName) {
this.layoutRes = layoutRes; this.layoutRes = layoutRes;
this.text = null; this.text = null;
this.id = h.getId(); this.id = h.getId();
this.groupId = h.getGroupId(); this.groupId = h.getGroupId();
this.time = h.getTimestamp(); this.time = h.getTimestamp();
this.autoDeleteTimer = h.getAutoDeleteTimer();
this.read = h.isRead(); this.read = h.isRead();
this.sent = h.isSent(); this.sent = h.isSent();
this.seen = h.isSeen(); this.seen = h.isSeen();
this.isIncoming = !h.isLocal(); this.isIncoming = !h.isLocal();
this.contactName = contactName;
this.showTimerNotice = false;
} }
@LayoutRes @LayoutRes
@@ -68,6 +74,10 @@ abstract class ConversationItem {
return time; return time;
} }
public long getAutoDeleteTimer() {
return autoDeleteTimer;
}
/** /**
* Only useful for incoming messages. * Only useful for incoming messages.
*/ */
@@ -111,4 +121,25 @@ abstract class ConversationItem {
return isIncoming; return isIncoming;
} }
public LiveData<String> 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;
}
} }

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.view.View; import android.view.View;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -12,8 +14,11 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView.ViewHolder; 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.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.formatDate; import static org.briarproject.briar.android.util.UiUtils.formatDate;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
@@ -24,8 +29,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConstraintLayout layout; protected final ConstraintLayout layout;
@Nullable @Nullable
private final OutItemViewHolder outViewHolder; private final OutItemViewHolder outViewHolder;
private final TextView text; private final TextView topNotice, text;
protected final TextView time; protected final TextView time;
protected final ImageView bomb;
@Nullable @Nullable
private String itemKey = null; private String itemKey = null;
@@ -33,11 +39,13 @@ abstract class ConversationItemViewHolder extends ViewHolder {
boolean isIncoming) { boolean isIncoming) {
super(v); super(v);
this.listener = listener; this.listener = listener;
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v); outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
root = v; root = v;
topNotice = v.findViewById(R.id.topNotice);
layout = v.findViewById(R.id.layout); layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text); text = v.findViewById(R.id.text);
time = v.findViewById(R.id.time); time = v.findViewById(R.id.time);
bomb = v.findViewById(R.id.bomb);
} }
@CallSuper @CallSuper
@@ -45,6 +53,8 @@ abstract class ConversationItemViewHolder extends ViewHolder {
itemKey = item.getKey(); itemKey = item.getKey();
root.setActivated(selected); root.setActivated(selected);
setTopNotice(item);
if (item.getText() != null) { if (item.getText() != null) {
text.setText(trim(item.getText())); text.setText(trim(item.getText()));
} }
@@ -52,6 +62,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
long timestamp = item.getTime(); long timestamp = item.getTime();
time.setText(formatDate(time.getContext(), timestamp)); 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); if (outViewHolder != null) outViewHolder.bind(item);
} }
@@ -64,4 +77,31 @@ abstract class ConversationItemViewHolder extends ViewHolder {
return itemKey; 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 tapToLearnMore = ctx.getString(R.string.tap_to_learn_more);
String text;
if (item.isIncoming()) {
String name = item.getContactName().getValue();
int strRes = enabled ?
R.string.auto_delete_msg_contact_enabled :
R.string.auto_delete_msg_contact_disabled;
text = ctx.getString(strRes, name, tapToLearnMore);
} else {
int strRes = enabled ?
R.string.auto_delete_msg_you_enabled :
R.string.auto_delete_msg_you_disabled;
text = ctx.getString(strRes, tapToLearnMore);
}
topNotice.setText(text);
topNotice.setOnClickListener(
v -> listener.onAutoDeleteTimerNoticeClicked());
} else {
topNotice.setVisibility(GONE);
}
}
} }

View File

@@ -18,4 +18,6 @@ interface ConversationListener {
void onAttachmentClicked(View view, ConversationMessageItem messageItem, void onAttachmentClicked(View view, ConversationMessageItem messageItem,
AttachmentItem attachmentItem); AttachmentItem attachmentItem);
void onAutoDeleteTimerNoticeClicked();
} }

View File

@@ -10,6 +10,7 @@ import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
@NotThreadSafe @NotThreadSafe
@NotNullByDefault @NotNullByDefault
@@ -18,8 +19,8 @@ class ConversationMessageItem extends ConversationItem {
private final List<AttachmentItem> attachments; private final List<AttachmentItem> attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h, ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
List<AttachmentItem> attachments) { LiveData<String> contactName, List<AttachmentItem> attachments) {
super(layoutRes, h); super(layoutRes, h, contactName);
this.attachments = attachments; this.attachments = attachments;
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.conversation;
import android.content.res.ColorStateList;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
import static androidx.core.content.ContextCompat.getColor; import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.widget.ImageViewCompat.setImageTintList;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
@@ -84,6 +86,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
if (item.getText() == null) { if (item.getText() == null) {
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble); statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble); time.setTextColor(timeColorBubble);
setImageTintList(bomb, ColorStateList.valueOf(timeColorBubble));
constraintSet = imageConstraints; constraintSet = imageConstraints;
} else { } else {
resetStatusLayoutForText(); resetStatusLayoutForText();
@@ -111,6 +114,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
// also reset padding (the background drawable defines some) // also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0); statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor); time.setTextColor(timeColor);
setImageTintList(bomb, ColorStateList.valueOf(timeColor));
} }
} }

View File

@@ -8,6 +8,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
@NotThreadSafe @NotThreadSafe
@NotNullByDefault @NotNullByDefault
@@ -17,15 +18,15 @@ class ConversationNoticeItem extends ConversationItem {
private final String msgText; private final String msgText;
ConversationNoticeItem(@LayoutRes int layoutRes, String text, ConversationNoticeItem(@LayoutRes int layoutRes, String text,
ConversationRequest r) { LiveData<String> contactName, ConversationRequest<?> r) {
super(layoutRes, r); super(layoutRes, r, contactName);
this.text = text; this.text = text;
this.msgText = r.getText(); this.msgText = r.getText();
} }
ConversationNoticeItem(@LayoutRes int layoutRes, String text, ConversationNoticeItem(@LayoutRes int layoutRes, String text,
ConversationResponse r) { LiveData<String> contactName, ConversationResponse r) {
super(layoutRes, r); super(layoutRes, r, contactName);
this.text = text; this.text = text;
this.msgText = null; this.msgText = null;
} }

View File

@@ -11,6 +11,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
@NotThreadSafe @NotThreadSafe
@NotNullByDefault @NotNullByDefault
@@ -26,14 +27,15 @@ class ConversationRequestItem extends ConversationNoticeItem {
private boolean answered; private boolean answered;
ConversationRequestItem(@LayoutRes int layoutRes, String text, ConversationRequestItem(@LayoutRes int layoutRes, String text,
RequestType type, ConversationRequest r) { LiveData<String> contactName, RequestType type,
super(layoutRes, text, r); ConversationRequest<?> r) {
super(layoutRes, text, contactName, r);
this.requestType = type; this.requestType = type;
this.sessionId = r.getSessionId(); this.sessionId = r.getSessionId();
this.answered = r.wasAnswered(); this.answered = r.wasAnswered();
if (r instanceof InvitationRequest) { if (r instanceof InvitationRequest) {
this.requestedGroupId = ((Shareable) r.getNameable()).getId(); this.requestedGroupId = ((Shareable) r.getNameable()).getId();
this.canBeOpened = ((InvitationRequest) r).canBeOpened(); this.canBeOpened = ((InvitationRequest<?>) r).canBeOpened();
} else { } else {
this.requestedGroupId = null; this.requestedGroupId = null;
this.canBeOpened = false; this.canBeOpened = false;

View File

@@ -0,0 +1,122 @@
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.TextView;
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 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 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));
TextView buttonLearnMore =
view.findViewById(R.id.buttonLearnMore);
buttonLearnMore.setOnClickListener(e -> showLearnMoreDialog());
viewModel.getAutoDeleteTimer()
.observe(getViewLifecycleOwner(), timer -> {
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() {
ConversationSettingsLearnMoreDialog
dialog = new ConversationSettingsLearnMoreDialog();
dialog.show(getChildFragmentManager(),
ConversationSettingsLearnMoreDialog.TAG);
}
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.briar.android.conversation;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationSettingsLearnMoreDialog extends DialogFragment {
final static String TAG =
ConversationSettingsLearnMoreDialog.class.getName();
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
FragmentActivity activity = requireActivity();
AlertDialog.Builder builder = new AlertDialog.Builder(activity,
R.style.OnboardingDialogTheme);
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
View view = inflater.inflate(
R.layout.fragment_conversation_settings_learn_more, null);
builder.setView(view);
builder.setTitle(R.string.disappearing_messages_title);
builder.setPositiveButton(R.string.ok, null);
return builder.create();
}
}

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; 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.db.TransactionManager;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; 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.attachment.AttachmentRetriever;
import org.briarproject.briar.android.contact.ContactItem; import org.briarproject.briar.android.contact.ContactItem;
import org.briarproject.briar.android.util.UiUtils; 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.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.attachment.AttachmentHeader; 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.avatar.event.AvatarUpdatedEvent;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorInfo; import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager; import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory; 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.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent; import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
@@ -55,6 +62,8 @@ import androidx.lifecycle.MutableLiveData;
import static androidx.lifecycle.Transformations.map; import static androidx.lifecycle.Transformations.map;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -62,6 +71,12 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; 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.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.messaging.PrivateMessageFormat.TEXT_IMAGES;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends DbViewModel public class ConversationViewModel extends DbViewModel
@@ -84,6 +99,8 @@ public class ConversationViewModel extends DbViewModel
private final PrivateMessageFactory privateMessageFactory; private final PrivateMessageFactory privateMessageFactory;
private final AttachmentRetriever attachmentRetriever; private final AttachmentRetriever attachmentRetriever;
private final AttachmentCreator attachmentCreator; private final AttachmentCreator attachmentCreator;
private final AutoDeleteManager autoDeleteManager;
private final ConversationManager conversationManager;
@Nullable @Nullable
private ContactId contactId = null; private ContactId contactId = null;
@@ -92,7 +109,7 @@ public class ConversationViewModel extends DbViewModel
private final LiveData<String> contactName = map(contactItem, c -> private final LiveData<String> contactName = map(contactItem, c ->
UiUtils.getContactDisplayName(c.getContact())); UiUtils.getContactDisplayName(c.getContact()));
private final LiveData<GroupId> messagingGroupId; private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport = private final MutableLiveData<PrivateMessageFormat> privateMessageFormat =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding = private final MutableLiveEvent<Boolean> showImageOnboarding =
new MutableLiveEvent<>(); new MutableLiveEvent<>();
@@ -100,8 +117,10 @@ public class ConversationViewModel extends DbViewModel
new MutableLiveEvent<>(); new MutableLiveEvent<>();
private final MutableLiveData<Boolean> showIntroductionAction = private final MutableLiveData<Boolean> showIntroductionAction =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted = private final MutableLiveData<Long> autoDeleteTimer =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>(false);
private final MutableLiveEvent<PrivateMessageHeader> addedHeader = private final MutableLiveEvent<PrivateMessageHeader> addedHeader =
new MutableLiveEvent<>(); new MutableLiveEvent<>();
@@ -118,7 +137,9 @@ public class ConversationViewModel extends DbViewModel
SettingsManager settingsManager, SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory, PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever, AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) { AttachmentCreator attachmentCreator,
AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor); super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db; this.db = db;
this.eventBus = eventBus; this.eventBus = eventBus;
@@ -129,10 +150,10 @@ public class ConversationViewModel extends DbViewModel
this.privateMessageFactory = privateMessageFactory; this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = attachmentRetriever; this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator; this.attachmentCreator = attachmentCreator;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
messagingGroupId = map(contactItem, c -> messagingGroupId = map(contactItem, c ->
messagingManager.getContactGroup(c.getContact()).getId()); messagingManager.getContactGroup(c.getContact()).getId());
contactDeleted.setValue(false);
eventBus.addListener(this); eventBus.addListener(this);
} }
@@ -152,6 +173,11 @@ public class ConversationViewModel extends DbViewModel
runOnDbThread(() -> attachmentRetriever runOnDbThread(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId())); .loadAttachmentItem(a.getMessageId()));
} }
} else if (e instanceof AutoDeleteTimerMirroredEvent) {
AutoDeleteTimerMirroredEvent a = (AutoDeleteTimerMirroredEvent) e;
if (a.getContactId().equals(contactId)) {
autoDeleteTimer.postValue(a.getNewTimer());
}
} else if (e instanceof AvatarUpdatedEvent) { } else if (e instanceof AvatarUpdatedEvent) {
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e; AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
if (a.getContactId().equals(contactId)) { if (a.getContactId().equals(contactId)) {
@@ -201,6 +227,11 @@ public class ConversationViewModel extends DbViewModel
contactItem.postValue(new ContactItem(c, authorInfo)); contactItem.postValue(new ContactItem(c, authorInfo));
logDuration(LOG, "Loading contact", start); logDuration(LOG, "Loading contact", start);
start = now(); 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); checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start); logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
@@ -235,20 +266,6 @@ public class ConversationViewModel extends DbViewModel
}); });
} }
@UiThread
void sendMessage(@Nullable String text,
List<AttachmentHeader> 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 @Override
@UiThread @UiThread
public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris, public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
@@ -275,10 +292,12 @@ public class ConversationViewModel extends DbViewModel
@DatabaseExecutor @DatabaseExecutor
private void checkFeaturesAndOnboarding(ContactId c) throws DbException { private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
// check if images are supported // check if images and auto-deletion are supported
boolean imagesSupported = db.transactionWithResult(true, txn -> PrivateMessageFormat format = db.transactionWithResult(true, txn ->
messagingManager.contactSupportsImages(txn, c)); messagingManager.getContactMessageFormat(txn, c));
imageSupport.postValue(imagesSupported); if (LOG.isLoggable(INFO))
LOG.info("PrivateMessageFormat loaded: " + format.name());
privateMessageFormat.postValue(format);
// check if introductions are supported // check if introductions are supported
Collection<Contact> contacts = contactManager.getContacts(); Collection<Contact> contacts = contactManager.getContacts();
@@ -287,7 +306,7 @@ public class ConversationViewModel extends DbViewModel
// we only show one onboarding dialog at a time // we only show one onboarding dialog at a time
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE); Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
if (imagesSupported && if (format != TEXT_ONLY &&
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) { settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
onOnboardingShown(SHOW_ONBOARDING_IMAGE); onOnboardingShown(SHOW_ONBOARDING_IMAGE);
showImageOnboarding.postEvent(true); showImageOnboarding.postEvent(true);
@@ -306,39 +325,82 @@ public class ConversationViewModel extends DbViewModel
} }
@UiThread @UiThread
private void createMessage(GroupId groupId, @Nullable String text, LiveData<SendState> sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long timestamp, List<AttachmentHeader> headers, long expectedTimer) {
boolean hasImageSupport) { MutableLiveData<SendState> liveData = new MutableLiveData<>();
try {
PrivateMessage pm;
if (hasImageSupport) {
pm = privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers);
} else {
pm = privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
}
storeMessage(pm);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
@UiThread
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
db.transaction(false, txn -> {
long start = now(); long start = now();
messagingManager.addLocalMessage(m); PrivateMessage m = createMessage(txn, text, headers,
expectedTimer);
messagingManager.addLocalMessage(txn, m);
logDuration(LOG, "Storing message", start); logDuration(LOG, "Storing message", start);
Message message = m.getMessage(); Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader( PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(), message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false, message.getTimestamp(), true, true, false, false,
m.hasText(), m.getAttachmentHeaders()); m.hasText(), m.getAttachmentHeaders(),
m.getAutoDeleteTimer());
// TODO add text to cache when available here // TODO add text to cache when available here
addedHeader.postEvent(h); 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<AttachmentHeader> 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 {
if (format == TEXT_ONLY) {
return privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
} else if (format == TEXT_IMAGES) {
return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers);
} else {
long timer = autoDeleteManager
.getAutoDeleteTimer(txn, contactId, timestamp);
if (timer != expectedTimer)
throw new UnexpectedTimerException();
return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers, timer);
}
} catch (FormatException e) {
throw new AssertionError(e);
}
}
void setAutoDeleteTimerEnabled(boolean enabled) {
final long timer = enabled ? DAYS.toMillis(7) : NO_AUTO_DELETE_TIMER;
// ContactId is set before menu gets inflated and UI interaction
final ContactId c = requireNonNull(contactId);
runOnDbThread(() -> {
try {
db.transaction(false, txn ->
autoDeleteManager.setAutoDeleteTimer(txn, c, timer));
autoDeleteTimer.postValue(timer);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
@@ -357,8 +419,8 @@ public class ConversationViewModel extends DbViewModel
return contactName; return contactName;
} }
LiveData<Boolean> hasImageSupport() { LiveData<PrivateMessageFormat> getPrivateMessageFormat() {
return imageSupport; return privateMessageFormat;
} }
LiveEvent<Boolean> showImageOnboarding() { LiveEvent<Boolean> showImageOnboarding() {
@@ -373,6 +435,10 @@ public class ConversationViewModel extends DbViewModel
return showIntroductionAction; return showIntroductionAction;
} }
LiveData<Long> getAutoDeleteTimer() {
return autoDeleteTimer;
}
LiveData<Boolean> isContactDeleted() { LiveData<Boolean> isContactDeleted() {
return contactDeleted; return contactDeleted;
} }

View File

@@ -60,10 +60,12 @@ class ConversationVisitor implements
} }
if (h.isLocal()) { if (h.isLocal()) {
item = new ConversationMessageItem( item = new ConversationMessageItem(
R.layout.list_item_conversation_msg_out, h, attachments); R.layout.list_item_conversation_msg_out, h, contactName,
attachments);
} else { } else {
item = new ConversationMessageItem( 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()) { if (h.hasText()) {
String text = textCache.getText(h.getId()); String text = textCache.getText(h.getId());
@@ -79,13 +81,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.blogs_sharing_invitation_sent, String text = ctx.getString(R.string.blogs_sharing_invitation_sent,
r.getName(), contactName.getValue()); r.getName(), contactName.getValue());
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text = ctx.getString( String text = ctx.getString(
R.string.blogs_sharing_invitation_received, R.string.blogs_sharing_invitation_received,
contactName.getValue(), r.getName()); contactName.getValue(), r.getName());
return new ConversationRequestItem( return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, BLOG, r); R.layout.list_item_conversation_request, text, contactName,
BLOG, r);
} }
} }
@@ -104,7 +108,8 @@ class ConversationVisitor implements
contactName.getValue()); contactName.getValue());
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text; String text;
if (r.wasAccepted()) { if (r.wasAccepted()) {
@@ -117,7 +122,8 @@ class ConversationVisitor implements
contactName.getValue()); contactName.getValue());
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r); R.layout.list_item_conversation_notice_in, text,
contactName, r);
} }
} }
@@ -128,13 +134,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.forum_invitation_sent, String text = ctx.getString(R.string.forum_invitation_sent,
r.getName(), contactName.getValue()); r.getName(), contactName.getValue());
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text = ctx.getString( String text = ctx.getString(
R.string.forum_invitation_received, R.string.forum_invitation_received,
contactName.getValue(), r.getName()); contactName.getValue(), r.getName());
return new ConversationRequestItem( return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, FORUM, r); R.layout.list_item_conversation_request, text, contactName,
FORUM, r);
} }
} }
@@ -153,7 +161,8 @@ class ConversationVisitor implements
contactName.getValue()); contactName.getValue());
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text; String text;
if (r.wasAccepted()) { if (r.wasAccepted()) {
@@ -166,7 +175,8 @@ class ConversationVisitor implements
contactName.getValue()); contactName.getValue());
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r); R.layout.list_item_conversation_notice_in, text,
contactName, r);
} }
} }
@@ -178,13 +188,15 @@ class ConversationVisitor implements
R.string.groups_invitations_invitation_sent, R.string.groups_invitations_invitation_sent,
contactName.getValue(), r.getName()); contactName.getValue(), r.getName());
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text = ctx.getString( String text = ctx.getString(
R.string.groups_invitations_invitation_received, R.string.groups_invitations_invitation_received,
contactName.getValue(), r.getName()); contactName.getValue(), r.getName());
return new ConversationRequestItem( return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, GROUP, r); R.layout.list_item_conversation_request, text, contactName,
GROUP, r);
} }
} }
@@ -203,7 +215,8 @@ class ConversationVisitor implements
contactName.getValue()); contactName.getValue());
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text; String text;
if (r.wasAccepted()) { if (r.wasAccepted()) {
@@ -216,7 +229,8 @@ class ConversationVisitor implements
contactName.getValue()); contactName.getValue());
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r); R.layout.list_item_conversation_notice_in, text,
contactName, r);
} }
} }
@@ -227,7 +241,8 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.introduction_request_sent, String text = ctx.getString(R.string.introduction_request_sent,
contactName.getValue(), name); contactName.getValue(), name);
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text; String text;
if (r.wasAnswered()) { if (r.wasAnswered()) {
@@ -243,7 +258,7 @@ class ConversationVisitor implements
contactName.getValue(), name); contactName.getValue(), name);
} }
return new ConversationRequestItem( return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, R.layout.list_item_conversation_request, text, contactName,
INTRODUCTION, r); INTRODUCTION, r);
} }
} }
@@ -268,7 +283,8 @@ class ConversationVisitor implements
introducedAuthor); introducedAuthor);
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r); R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else { } else {
String text; String text;
if (r.wasAccepted()) { if (r.wasAccepted()) {
@@ -288,7 +304,8 @@ class ConversationVisitor implements
introducedAuthor); introducedAuthor);
} }
return new ConversationNoticeItem( return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r); R.layout.list_item_conversation_notice_in, text,
contactName, r);
} }
} }

View File

@@ -35,6 +35,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
@@ -46,6 +48,8 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName; import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard; 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.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; import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -201,8 +205,8 @@ public class IntroductionMessageFragment extends BaseFragment
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double invitations // disable button to prevent accidental double invitations
ui.message.setReady(false); ui.message.setReady(false);
@@ -212,6 +216,7 @@ public class IntroductionMessageFragment extends BaseFragment
hideSoftKeyboard(ui.message); hideSoftKeyboard(ui.message);
introductionActivity.setResult(RESULT_OK); introductionActivity.setResult(RESULT_OK);
introductionActivity.supportFinishAfterTransition(); introductionActivity.supportFinishAfterTransition();
return new MutableLiveData<>(SENT);
} }
private void makeIntroduction(Contact c1, Contact c2, private void makeIntroduction(Contact c1, Contact c2,
@@ -219,8 +224,7 @@ public class IntroductionMessageFragment extends BaseFragment
introductionActivity.runOnDbThread(() -> { introductionActivity.runOnDbThread(() -> {
// actually make the introduction // actually make the introduction
try { try {
long timestamp = System.currentTimeMillis(); introductionManager.makeIntroduction(c1, c2, text);
introductionManager.makeIntroduction(c1, c2, text, timestamp);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
introductionError(); introductionError();

View File

@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; 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.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; 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.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl; import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; 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.identity.AuthorManager;
import org.briarproject.briar.api.privategroup.GroupMessage; import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory; import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -36,6 +40,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING; 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; import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable @Immutable
@@ -44,9 +50,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
implements CreateGroupController { implements CreateGroupController {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(CreateGroupControllerImpl.class.getName()); getLogger(CreateGroupControllerImpl.class.getName());
private final Executor cryptoExecutor; private final Executor cryptoExecutor;
private final TransactionManager db;
private final AutoDeleteManager autoDeleteManager;
private final ConversationManager conversationManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final IdentityManager identityManager; private final IdentityManager identityManager;
private final PrivateGroupFactory groupFactory; private final PrivateGroupFactory groupFactory;
@@ -57,17 +66,27 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private final Clock clock; private final Clock clock;
@Inject @Inject
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor, CreateGroupControllerImpl(
@DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor, @CryptoExecutor Executor cryptoExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager, TransactionManager db,
AuthorManager authorManager, IdentityManager identityManager, AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager,
LifecycleManager lifecycleManager,
ContactManager contactManager,
AuthorManager authorManager,
IdentityManager identityManager,
PrivateGroupFactory groupFactory, PrivateGroupFactory groupFactory,
GroupMessageFactory groupMessageFactory, GroupMessageFactory groupMessageFactory,
PrivateGroupManager groupManager, PrivateGroupManager groupManager,
GroupInvitationFactory groupInvitationFactory, GroupInvitationFactory groupInvitationFactory,
GroupInvitationManager groupInvitationManager, Clock clock) { GroupInvitationManager groupInvitationManager,
Clock clock) {
super(dbExecutor, lifecycleManager, contactManager, authorManager); super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.cryptoExecutor = cryptoExecutor; this.cryptoExecutor = cryptoExecutor;
this.db = db;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
this.contactManager = contactManager; this.contactManager = contactManager;
this.identityManager = identityManager; this.identityManager = identityManager;
this.groupFactory = groupFactory; this.groupFactory = groupFactory;
@@ -131,16 +150,14 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler<Void, DbException> handler) { ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
LocalAuthor localAuthor = identityManager.getLocalAuthor(); db.transaction(false, txn -> {
List<Contact> contacts = new ArrayList<>(); LocalAuthor localAuthor =
for (ContactId c : contactIds) { identityManager.getLocalAuthor(txn);
try { List<InvitationContext> contexts =
contacts.add(contactManager.getContact(c)); createInvitationContexts(txn, contactIds);
} catch (NoSuchContactException e) { txn.attach(() -> signInvitations(g, localAuthor, contexts,
// Continue text, handler));
} });
}
signInvitations(g, localAuthor, contacts, text, handler);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
handler.onException(e); handler.onException(e);
@@ -148,17 +165,32 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
}); });
} }
private List<InvitationContext> createInvitationContexts(Transaction txn,
Collection<ContactId> contactIds) throws DbException {
List<InvitationContext> 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, private void signInvitations(GroupId g, LocalAuthor localAuthor,
Collection<Contact> contacts, @Nullable String text, List<InvitationContext> contexts, @Nullable String text,
ResultExceptionHandler<Void, DbException> handler) { ResultExceptionHandler<Void, DbException> handler) {
cryptoExecutor.execute(() -> { cryptoExecutor.execute(() -> {
long timestamp = clock.currentTimeMillis(); for (InvitationContext ctx : contexts) {
List<InvitationContext> contexts = new ArrayList<>(); ctx.signature = groupInvitationFactory.signInvitation(
for (Contact c : contacts) { ctx.contact, g, ctx.timestamp,
byte[] signature = groupInvitationFactory.signInvitation(c, g, localAuthor.getPrivateKey());
timestamp, localAuthor.getPrivateKey());
contexts.add(new InvitationContext(c.getId(), timestamp,
signature));
} }
sendInvitations(g, contexts, text, handler); sendInvitations(g, contexts, text, handler);
}); });
@@ -169,11 +201,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler<Void, DbException> handler) { ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
for (InvitationContext context : contexts) { for (InvitationContext ctx : contexts) {
try { try {
groupInvitationManager.sendInvitation(g, groupInvitationManager.sendInvitation(g,
context.contactId, text, context.timestamp, ctx.contact.getId(), text, ctx.timestamp,
context.signature); requireNonNull(ctx.signature),
ctx.autoDeleteTimer);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
// Continue // Continue
} }
@@ -188,15 +221,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private static class InvitationContext { private static class InvitationContext {
private final ContactId contactId; private final Contact contact;
private final long timestamp; private final long timestamp, autoDeleteTimer;
private final byte[] signature; @Nullable
private byte[] signature = null;
private InvitationContext(ContactId contactId, long timestamp, private InvitationContext(Contact contact, long timestamp,
byte[] signature) { long autoDeleteTimer) {
this.contactId = contactId; this.contact = contact;
this.timestamp = timestamp; this.timestamp = timestamp;
this.signature = signature; this.autoDeleteTimer = autoDeleteTimer;
} }
} }
} }

View File

@@ -15,6 +15,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.LargeTextInputView; import org.briarproject.briar.android.view.LargeTextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List; import java.util.List;
@@ -22,6 +23,10 @@ import java.util.List;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -79,13 +84,14 @@ public abstract class BaseMessageFragment extends BaseFragment
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double actions // disable button to prevent accidental double actions
sendController.setReady(false); sendController.setReady(false);
message.hideSoftKeyboard(); message.hideSoftKeyboard();
listener.onButtonClick(text); listener.onButtonClick(text);
return new MutableLiveData<>(SENT);
} }
@UiThread @UiThread

View File

@@ -10,11 +10,9 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; 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.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler; import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorManager; import org.briarproject.briar.api.identity.AuthorManager;
import java.util.Collection; import java.util.Collection;
@@ -26,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable @Immutable
@@ -34,22 +33,17 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
implements ShareBlogController { implements ShareBlogController {
private final static Logger LOG = private final static Logger LOG =
Logger.getLogger(ShareBlogControllerImpl.class.getName()); getLogger(ShareBlogControllerImpl.class.getName());
private final ConversationManager conversationManager;
private final BlogSharingManager blogSharingManager; private final BlogSharingManager blogSharingManager;
private final Clock clock;
@Inject @Inject
ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor, ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager, LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager, AuthorManager authorManager,
ConversationManager conversationManager, BlogSharingManager blogSharingManager) {
BlogSharingManager blogSharingManager, Clock clock) {
super(dbExecutor, lifecycleManager, contactManager, authorManager); super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.conversationManager = conversationManager;
this.blogSharingManager = blogSharingManager; this.blogSharingManager = blogSharingManager;
this.clock = clock;
} }
@Override @Override
@@ -64,10 +58,7 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
try { try {
for (ContactId c : contacts) { for (ContactId c : contacts) {
try { try {
long time = Math.max(clock.currentTimeMillis(), blogSharingManager.sendInvitation(g, c, text);
conversationManager.getGroupCount(c)
.getLatestMsgTime() + 1);
blogSharingManager.sendInvitation(g, c, text, time);
} catch (NoSuchContactException | NoSuchGroupException e) { } catch (NoSuchContactException | NoSuchGroupException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }

View File

@@ -10,10 +10,8 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; 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.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler; 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.forum.ForumSharingManager;
import org.briarproject.briar.api.identity.AuthorManager; import org.briarproject.briar.api.identity.AuthorManager;
@@ -26,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable @Immutable
@@ -34,22 +33,17 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
implements ShareForumController { implements ShareForumController {
private final static Logger LOG = private final static Logger LOG =
Logger.getLogger(ShareForumControllerImpl.class.getName()); getLogger(ShareForumControllerImpl.class.getName());
private final ConversationManager conversationManager;
private final ForumSharingManager forumSharingManager; private final ForumSharingManager forumSharingManager;
private final Clock clock;
@Inject @Inject
ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor, ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager, LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager, AuthorManager authorManager,
ConversationManager conversationManager, ForumSharingManager forumSharingManager) {
ForumSharingManager forumSharingManager, Clock clock) {
super(dbExecutor, lifecycleManager, contactManager, authorManager); super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.conversationManager = conversationManager;
this.forumSharingManager = forumSharingManager; this.forumSharingManager = forumSharingManager;
this.clock = clock;
} }
@Override @Override
@@ -64,10 +58,7 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
try { try {
for (ContactId c : contacts) { for (ContactId c : contacts) {
try { try {
long time = Math.max(clock.currentTimeMillis(), forumSharingManager.sendInvitation(g, c, text);
conversationManager.getGroupCount(c)
.getLatestMsgTime() + 1);
forumSharingManager.sendInvitation(g, c, text, time);
} catch (NoSuchContactException | NoSuchGroupException e) { } catch (NoSuchContactException | NoSuchGroupException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }

View File

@@ -19,6 +19,7 @@ import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; 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.android.view.UnreadMessageButton;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
@@ -29,10 +30,13 @@ import javax.annotation.Nullable;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -231,8 +235,8 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError(); if (isNullOrEmpty(text)) throw new AssertionError();
MessageId replyId = getViewModel().getReplyId(); MessageId replyId = getViewModel().getReplyId();
@@ -241,6 +245,7 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
textInput.clearText(); textInput.clearText();
getViewModel().setReplyId(null); getViewModel().setReplyId(null);
updateTextInput(); updateTextInput();
return new MutableLiveData<>(SENT);
} }
protected abstract int getMaxTextLength(); protected abstract int getMaxTextLength();

View File

@@ -5,6 +5,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import org.briarproject.briar.R; import org.briarproject.briar.R;
@@ -19,6 +20,7 @@ import static java.util.Objects.requireNonNull;
public class CompositeSendButton extends FrameLayout { public class CompositeSendButton extends FrameLayout {
private final AppCompatImageButton sendButton, imageButton; private final AppCompatImageButton sendButton, imageButton;
private final ImageView bombBadge;
private final ProgressBar progressBar; private final ProgressBar progressBar;
private boolean hasImageSupport = false; private boolean hasImageSupport = false;
@@ -32,6 +34,7 @@ public class CompositeSendButton extends FrameLayout {
sendButton = findViewById(R.id.sendButton); sendButton = findViewById(R.id.sendButton);
imageButton = findViewById(R.id.imageButton); imageButton = findViewById(R.id.imageButton);
bombBadge = findViewById(R.id.bombBadge);
progressBar = findViewById(R.id.progressBar); progressBar = findViewById(R.id.progressBar);
} }
@@ -71,6 +74,10 @@ public class CompositeSendButton extends FrameLayout {
return hasImageSupport; return hasImageSupport;
} }
public void setBombVisible(boolean visible) {
bombBadge.setVisibility(visible ? VISIBLE : INVISIBLE);
}
public void showImageButton(boolean showImageButton, boolean sendEnabled) { public void showImageButton(boolean showImageButton, boolean sendEnabled) {
if (showImageButton) { if (showImageButton) {
imageButton.setVisibility(VISIBLE); imageButton.setVisibility(VISIBLE);

View File

@@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.app.AlertDialog.Builder;
import androidx.customview.view.AbsSavedState; import androidx.customview.view.AbsSavedState;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; 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.customview.view.AbsSavedState.EMPTY_STATE;
import static androidx.lifecycle.Lifecycle.State.DESTROYED; import static androidx.lifecycle.Lifecycle.State.DESTROYED;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; 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; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
@UiThread @UiThread
@@ -52,7 +52,6 @@ public class TextAttachmentController extends TextSendController
private final AttachmentManager attachmentManager; private final AttachmentManager attachmentManager;
private final List<Uri> imageUris = new ArrayList<>(); private final List<Uri> imageUris = new ArrayList<>();
private final CharSequence textHint;
private boolean loadingUris = false; private boolean loadingUris = false;
public TextAttachmentController(TextInputView v, ImagePreview imagePreview, public TextAttachmentController(TextInputView v, ImagePreview imagePreview,
@@ -66,23 +65,44 @@ public class TextAttachmentController extends TextSendController
sendButton = (CompositeSendButton) compositeSendButton; sendButton = (CompositeSendButton) compositeSendButton;
sendButton.setOnImageClickListener(view -> onImageButtonClicked()); sendButton.setOnImageClickListener(view -> onImageButtonClicked());
textHint = textInput.getHint();
} }
@Override @Override
protected void updateViewState() { protected void updateViewState() {
textInput.setEnabled(ready && !loadingUris); super.updateViewState();
boolean sendEnabled = ready && !loadingUris &&
(!textIsEmpty || canSendEmptyText());
if (loadingUris) { if (loadingUris) {
sendButton.showProgress(true); sendButton.showProgress(true);
} else if (imageUris.isEmpty()) { } else if (imageUris.isEmpty()) {
sendButton.showProgress(false); sendButton.showProgress(false);
sendButton.showImageButton(textIsEmpty, sendEnabled); sendButton.showImageButton(textIsEmpty, isSendButtonEnabled());
} else { } else {
sendButton.showProgress(false); 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 (canSend()) {
if (loadingUris) throw new AssertionError(); if (loadingUris) throw new AssertionError();
listener.onSendClick(textInput.getText(), listener.onSendClick(textInput.getText(),
attachmentManager.getAttachmentHeadersForSending()); attachmentManager.getAttachmentHeadersForSending(),
reset(); expectedTimer).observe(listener, this::onSendStateChanged);
} }
} }
@Override
protected void onSendStateChanged(SendState sendState) {
super.onSendStateChanged(sendState);
if (sendState == SENT) reset();
}
@Override @Override
protected boolean canSendEmptyText() { protected boolean canSendEmptyText() {
return !imageUris.isEmpty(); return !imageUris.isEmpty();
@@ -154,6 +180,7 @@ public class TextAttachmentController extends TextSendController
private void onNewUris(boolean restart, List<Uri> newUris) { private void onNewUris(boolean restart, List<Uri> newUris) {
if (newUris.isEmpty()) return; if (newUris.isEmpty()) return;
if (loadingUris) throw new AssertionError(); if (loadingUris) throw new AssertionError();
if (textIsEmpty) onStartingMessage();
loadingUris = true; loadingUris = true;
if (newUris.size() > MAX_ATTACHMENTS_PER_MESSAGE) { if (newUris.size() > MAX_ATTACHMENTS_PER_MESSAGE) {
newUris = newUris.subList(0, MAX_ATTACHMENTS_PER_MESSAGE); newUris = newUris.subList(0, MAX_ATTACHMENTS_PER_MESSAGE);
@@ -161,7 +188,6 @@ public class TextAttachmentController extends TextSendController
} }
imageUris.addAll(newUris); imageUris.addAll(newUris);
updateViewState(); updateViewState();
textInput.setHint(R.string.image_caption_hint);
List<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris); List<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris);
imagePreview.showPreview(items); imagePreview.showPreview(items);
// store attachments and show preview when successful // store attachments and show preview when successful
@@ -207,8 +233,6 @@ public class TextAttachmentController extends TextSendController
} }
private void reset() { private void reset() {
// restore hint
textInput.setHint(textHint);
// hide image layout // hide image layout
imagePreview.setVisibility(GONE); imagePreview.setVisibility(GONE);
// reset image URIs // reset image URIs
@@ -303,7 +327,7 @@ public class TextAttachmentController extends TextSendController
} }
@UiThread @UiThread
public interface AttachmentListener extends SendListener, LifecycleOwner { public interface AttachmentListener extends SendListener {
void onAttachImage(Intent intent); void onAttachImage(Intent intent);

View File

@@ -1,7 +1,9 @@
package org.briarproject.briar.android.view; package org.briarproject.briar.android.view;
import android.content.Context;
import android.os.Parcelable; import android.os.Parcelable;
import android.view.View; import android.view.View;
import android.widget.Toast;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
@@ -12,11 +14,20 @@ import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List; import java.util.List;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; 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 com.google.android.material.snackbar.Snackbar.LENGTH_SHORT;
import static java.util.Collections.emptyList; 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 @UiThread
@NotNullByDefault @NotNullByDefault
@@ -26,8 +37,12 @@ public class TextSendController implements TextInputListener {
protected final View compositeSendButton; protected final View compositeSendButton;
protected final SendListener listener; 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; private final boolean allowEmptyText;
public TextSendController(TextInputView v, SendListener listener, public TextSendController(TextInputView v, SendListener listener,
@@ -36,31 +51,91 @@ public class TextSendController implements TextInputListener {
this.compositeSendButton.setOnClickListener(view -> onSendEvent()); this.compositeSendButton.setOnClickListener(view -> onSendEvent());
this.listener = listener; this.listener = listener;
this.textInput = v.getEmojiTextInputView(); this.textInput = v.getEmojiTextInputView();
this.defaultHint = textInput.getHint();
this.allowEmptyText = allowEmptyText; this.allowEmptyText = allowEmptyText;
} }
@Override @Override
public void onTextIsEmptyChanged(boolean isEmpty) { public void onTextIsEmptyChanged(boolean isEmpty) {
textIsEmpty = isEmpty; textIsEmpty = isEmpty;
if (!isEmpty) onStartingMessage();
updateViewState(); updateViewState();
} }
@Override @Override
public void onSendEvent() { public void onSendEvent() {
if (canSend()) { 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) { public void setReady(boolean ready) {
this.ready = ready; this.ready = ready;
updateViewState(); updateViewState();
} }
/**
* Sets the current auto delete timer and updates the UI accordingly.
*/
public void setAutoDeleteTimer(long timer) {
currentTimer = timer;
updateViewState();
}
@CallSuper
protected void updateViewState() { protected void updateViewState() {
textInput.setEnabled(ready); textInput.setEnabled(isTextInputEnabled());
compositeSendButton textInput.setHint(getCurrentTextHint());
.setEnabled(ready && (!textIsEmpty || canSendEmptyText())); 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() { protected final boolean canSend() {
@@ -76,6 +151,23 @@ public class TextSendController implements TextInputListener {
return allowEmptyText; 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 @Nullable
public Parcelable onSaveInstanceState(@Nullable Parcelable superState) { public Parcelable onSaveInstanceState(@Nullable Parcelable superState) {
return superState; return superState;
@@ -86,9 +178,11 @@ public class TextSendController implements TextInputListener {
return state; return state;
} }
@UiThread public enum SendState {SENT, ERROR, UNEXPECTED_TIMER}
public interface SendListener {
void onSendClick(@Nullable String text, List<AttachmentHeader> headers); public interface SendListener extends LifecycleOwner {
LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer);
} }
} }

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M11.25,6A3.25,3.25 0 0,1 14.5,2.75A3.25,3.25 0 0,1 17.75,6C17.75,6.42 18.08,6.75 18.5,6.75C18.92,6.75 19.25,6.42 19.25,6V5.25H20.75V6A2.25,2.25 0 0,1 18.5,8.25A2.25,2.25 0 0,1 16.25,6A1.75,1.75 0 0,0 14.5,4.25A1.75,1.75 0 0,0 12.75,6H14V7.29C16.89,8.15 19,10.83 19,14A7,7 0 0,1 12,21A7,7 0 0,1 5,14C5,10.83 7.11,8.15 10,7.29V6H11.25M22,6H24V7H22V6M19,4V2H20V4H19M20.91,4.38L22.33,2.96L23.04,3.67L21.62,5.09L20.91,4.38Z" />
</vector>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/abc_ic_ab_back_material"
app:title="@string/disappearing_messages_title" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingHorizontal="@dimen/margin_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageViewBomb"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_bomb"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchDisappearingMessages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="@string/disappearing_messages_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageViewBomb" />
<TextView
android:id="@+id/buttonLearnMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:clickable="true"
android:focusable="true"
android:text="@string/learn_more"
android:textColor="@color/briar_text_link"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/switchDisappearingMessages" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="?dialogPreferredPadding">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/disappearing_messages_explanation_long"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ScrollView>

View File

@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- This layout is only used indirectly by cloning and setting its ConstraintSet -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -68,6 +70,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView
android:id="@+id/bomb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_bomb"
app:tint="?android:attr/textColorSecondary"
tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- This layout is only used indirectly by cloning and setting its ConstraintSet -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -58,7 +60,8 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0" app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text"> app:layout_constraintTop_toBottomOf="@+id/text"
tools:ignore="UseCompoundDrawables">
<TextView <TextView
android:id="@+id/time" android:id="@+id/time"
@@ -67,6 +70,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView
android:id="@+id/bomb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_bomb"
app:tint="?android:attr/textColorSecondary"
tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,14 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable"> android:background="@drawable/list_item_background_selectable"
android:orientation="vertical">
<!-- <!--
We need to wrap the actual layout, because We need to wrap the actual layout, because we want to
* we want to clone the ConstraintLayout's constraints in the ViewHolder * clone the ConstraintLayout's constraints in the ViewHolder
* we want to have a selectable frame around the message bubble * have a selectable frame around the message bubble
* insert a top notice with its own independent width
--> -->
<include layout="@layout/list_item_conversation_top_notice_in" />
<include layout="@layout/list_item_conversation_msg_in_content" /> <include layout="@layout/list_item_conversation_msg_in_content" />
</FrameLayout> </LinearLayout>

View File

@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- This layout gets wrapped in *_msg_in.xml -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -68,6 +70,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView
android:id="@+id/bomb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
app:srcCompat="@drawable/ic_bomb"
app:tint="?android:attr/textColorSecondary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable"> android:background="@drawable/list_item_background_selectable"
android:orientation="vertical"
android:paddingTop="@dimen/message_bubble_margin">
<include layout="@layout/list_item_conversation_top_notice_out" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout" android:id="@+id/layout"
@@ -73,15 +77,26 @@
style="@style/TextMessage.Timestamp" style="@style/TextMessage.Timestamp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="6dp" android:layout_marginEnd="4dp"
android:layout_marginRight="6dp" android:layout_marginRight="4dp"
android:textColor="@color/private_message_date_inverse" android:textColor="@color/private_message_date_inverse"
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView
android:id="@+id/bomb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
app:srcCompat="@drawable/ic_bomb"
app:tint="@color/private_message_date_inverse"
tools:ignore="ContentDescription" />
<ImageView <ImageView
android:id="@+id/status" android:id="@+id/status"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@color/private_message_date_inverse"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered" /> tools:src="@drawable/message_delivered" />
@@ -89,4 +104,4 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </LinearLayout>

View File

@@ -5,8 +5,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable" android:background="@drawable/list_item_background_selectable"
android:orientation="vertical" android:orientation="vertical">
android:paddingTop="@dimen/message_bubble_margin">
<include layout="@layout/list_item_conversation_top_notice_in" />
<com.vanniktech.emoji.EmojiTextView <com.vanniktech.emoji.EmojiTextView
android:id="@+id/msgText" android:id="@+id/msgText"
@@ -15,11 +16,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_margin_tail" android:layout_marginStart="@dimen/message_bubble_margin_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_tail" android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginTop="@dimen/message_bubble_margin"
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail" android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail" android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/msg_in_top" android:background="@drawable/msg_in_top"
android:elevation="@dimen/message_bubble_elevation" android:elevation="@dimen/message_bubble_elevation"
tools:text="Short message" /> tools:text="Short message"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout" android:id="@+id/layout"
@@ -48,10 +51,28 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin" android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
app:layout_constraintEnd_toEndOf="@+id/text" app:layout_constraintEnd_toStartOf="@+id/bomb"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text" app:layout_constraintTop_toBottomOf="@+id/text"
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView
android:id="@+id/bomb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
app:layout_constraintEnd_toEndOf="@+id/text"
app:layout_constraintStart_toEndOf="@+id/time"
app:layout_constraintTop_toBottomOf="@+id/text"
app:srcCompat="@drawable/ic_bomb"
app:tint="?android:attr/textColorSecondary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>

View File

@@ -8,6 +8,8 @@
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="@dimen/message_bubble_margin"> android:paddingTop="@dimen/message_bubble_margin">
<include layout="@layout/list_item_conversation_top_notice_out" />
<com.vanniktech.emoji.EmojiTextView <com.vanniktech.emoji.EmojiTextView
android:id="@+id/msgText" android:id="@+id/msgText"
style="@style/TextMessage" style="@style/TextMessage"
@@ -15,12 +17,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_margin_non_tail" android:layout_marginStart="@dimen/message_bubble_margin_non_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail" android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginTop="@dimen/message_bubble_margin"
android:layout_marginEnd="@dimen/message_bubble_margin_tail" android:layout_marginEnd="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail" android:layout_marginRight="@dimen/message_bubble_margin_tail"
android:background="@drawable/msg_out_top" android:background="@drawable/msg_out_top"
android:elevation="@dimen/message_bubble_elevation" android:elevation="@dimen/message_bubble_elevation"
android:textColor="@color/briar_text_primary_inverse" android:textColor="@color/briar_text_primary_inverse"
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here." /> tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout" android:id="@+id/layout"
@@ -56,14 +60,28 @@
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView <ImageView
android:id="@+id/status" android:id="@+id/bomb"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium" android:layout_marginStart="4dp"
android:layout_marginLeft="@dimen/margin_medium" android:layout_marginLeft="4dp"
app:layout_constraintBottom_toBottomOf="@+id/time" app:layout_constraintBottom_toBottomOf="@+id/time"
app:layout_constraintStart_toEndOf="@+id/time" app:layout_constraintStart_toEndOf="@+id/time"
app:layout_constraintTop_toTopOf="@+id/time" app:layout_constraintTop_toTopOf="@+id/time"
app:srcCompat="@drawable/ic_bomb"
app:tint="@color/private_message_date_inverse"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
app:layout_constraintBottom_toBottomOf="@+id/time"
app:layout_constraintStart_toEndOf="@+id/bomb"
app:layout_constraintTop_toTopOf="@+id/time"
app:tint="@color/private_message_date_inverse"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered" /> tools:src="@drawable/message_delivered" />

View File

@@ -5,8 +5,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_item_background_selectable" android:background="@drawable/list_item_background_selectable"
android:orientation="vertical" android:orientation="vertical">
android:paddingTop="@dimen/message_bubble_margin">
<include layout="@layout/list_item_conversation_top_notice_in" />
<com.vanniktech.emoji.EmojiTextView <com.vanniktech.emoji.EmojiTextView
android:id="@+id/msgText" android:id="@+id/msgText"
@@ -15,6 +16,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_margin_tail" android:layout_marginStart="@dimen/message_bubble_margin_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_tail" android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginTop="@dimen/message_bubble_margin"
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail" android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail" android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/msg_in_top" android:background="@drawable/msg_in_top"
@@ -68,10 +70,27 @@
style="@style/TextMessage.Timestamp" style="@style/TextMessage.Timestamp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/bomb"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/acceptButton" app:layout_constraintTop_toBottomOf="@+id/acceptButton"
tools:text="Dec 24, 13:37" /> tools:text="Dec 24, 13:37" />
<ImageView
android:id="@+id/bomb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/time"
app:layout_constraintTop_toBottomOf="@+id/acceptButton"
app:srcCompat="@drawable/ic_bomb"
app:tint="?android:attr/textColorSecondary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<com.vanniktech.emoji.EmojiTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/topNotice"
style="@style/TextMessage.Notice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_bubble_margin_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginTop="@dimen/message_bubble_margin"
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:layout_marginBottom="@dimen/message_bubble_margin"
android:background="@drawable/notice_in"
android:elevation="@dimen/message_bubble_elevation"
android:paddingBottom="@dimen/message_bubble_padding_top"
android:textIsSelectable="false"
android:visibility="gone"
tools:text="@string/auto_delete_msg_contact_enabled"
tools:visibility="visible" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<com.vanniktech.emoji.EmojiTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/topNotice"
style="@style/TextMessage.Notice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|right"
android:layout_marginStart="@dimen/message_bubble_margin_non_tail"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginEnd="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail"
android:layout_marginBottom="@dimen/message_bubble_margin"
android:background="@drawable/notice_out"
android:elevation="@dimen/message_bubble_elevation"
android:paddingBottom="@dimen/message_bubble_padding_top"
android:textColor="@color/private_message_date_inverse"
android:textIsSelectable="false"
android:visibility="gone"
tools:showIn="@layout/list_item_conversation_msg_out"
tools:text="@string/auto_delete_msg_you_enabled"
tools:visibility="visible" />

View File

@@ -37,6 +37,18 @@
app:srcCompat="@drawable/social_send_now_white" app:srcCompat="@drawable/social_send_now_white"
app:tint="@color/briar_accent" /> app:tint="@color/briar_accent" />
<ImageView
android:id="@+id/bombBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="3dp"
android:contentDescription="@string/auto_delete_msg_contact_enabled"
android:visibility="invisible"
app:srcCompat="@drawable/ic_bomb"
app:tint="@color/briar_accent"
tools:visibility="visible" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="@dimen/text_input_height" android:layout_width="@dimen/text_input_height"

View File

@@ -1,30 +1,37 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:tools="http://schemas.android.com/tools">
<item <item
android:id="@+id/action_introduction" android:id="@+id/action_introduction"
android:enabled="false"
android:icon="@drawable/introduction_white" android:icon="@drawable/introduction_white"
android:title="@string/introduction_menu_item" android:title="@string/introduction_menu_item"
android:enabled="false" app:showAsAction="never" />
app:showAsAction="never"/>
<item <item
android:id="@+id/action_set_alias" android:id="@+id/action_set_alias"
android:title="@string/set_contact_alias"
android:enabled="false" android:enabled="false"
app:showAsAction="never"/> android:title="@string/set_contact_alias"
app:showAsAction="never" />
<item
android:id="@+id/action_conversation_settings"
android:title="@string/menu_item_disappearing_messages"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
<item <item
android:id="@+id/action_delete_all_messages" android:id="@+id/action_delete_all_messages"
android:title="@string/delete_all_messages" android:title="@string/delete_all_messages"
app:showAsAction="never"/> app:showAsAction="never" />
<item <item
android:id="@+id/action_social_remove_person" android:id="@+id/action_social_remove_person"
android:icon="@drawable/action_delete_white" android:icon="@drawable/action_delete_white"
android:title="@string/delete_contact" android:title="@string/delete_contact"
app:showAsAction="never"/> app:showAsAction="never" />
</menu> </menu>

View File

@@ -157,7 +157,9 @@
<string name="no_contacts_action">Tap the + icon to add a contact</string> <string name="no_contacts_action">Tap the + icon to add a contact</string>
<string name="date_no_private_messages">No messages.</string> <string name="date_no_private_messages">No messages.</string>
<string name="no_private_messages">No messages to show</string> <string name="no_private_messages">No messages to show</string>
<string name="message_hint">Type message</string> <string name="message_hint">New message</string>
<string name="message_hint_auto_delete">New disappearing message</string>
<string name="message_error">Error sending message</string>
<string name="image_caption_hint">Add a caption (optional)</string> <string name="image_caption_hint">Add a caption (optional)</string>
<string name="image_attach">Attach image</string> <string name="image_attach">Attach image</string>
<string name="image_attach_error">Could not attach image(s)</string> <string name="image_attach_error">Could not attach image(s)</string>
@@ -165,6 +167,20 @@
<string name="image_attach_error_invalid_mime_type">Image format unsupported: %s</string> <string name="image_attach_error_invalid_mime_type">Image format unsupported: %s</string>
<string name="set_contact_alias">Change contact name</string> <string name="set_contact_alias">Change contact name</string>
<string name="set_contact_alias_hint">Contact name</string> <string name="set_contact_alias_hint">Contact name</string>
<string name="menu_item_disappearing_messages">Disappearing messages</string>
<!-- The placeholder at the end will add "Tap to learn more." -->
<string name="auto_delete_msg_you_enabled">Your messages will disappear after 7 days. %1$s</string>
<!-- The placeholder at the end will add "Tap to learn more." -->
<string name="auto_delete_msg_you_disabled">Your messages will not disappear. %1$s</string>
<!-- The second placeholder at the end will add "Tap to learn more." -->
<string name="auto_delete_msg_contact_enabled">%1$s\'s messages will disappear after 7 days. %2$s</string>
<!-- The second placeholder at the end will add "Tap to learn more." -->
<string name="auto_delete_msg_contact_disabled">%1$s\'s messages will not disappear. %2$s</string>
<string name="tap_to_learn_more">Tap to learn more.</string>
<string name="auto_delete_changed_warning_title">Disappearing messages changed</string>
<string name="auto_delete_changed_warning_message_enabled">Since you started composing your message, disappearing messages have been enabled.</string>
<string name="auto_delete_changed_warning_message_disabled">Since you started composing your message, disappearing messages have been disabled.</string>
<string name="auto_delete_changed_warning_send">Send anyway</string>
<string name="delete_all_messages">Delete all messages</string> <string name="delete_all_messages">Delete all messages</string>
<string name="dialog_title_delete_all_messages">Confirm Message Deletion</string> <string name="dialog_title_delete_all_messages">Confirm Message Deletion</string>
<string name="dialog_message_delete_all_messages">Are you sure that you want to delete all messages?</string> <string name="dialog_message_delete_all_messages">Are you sure that you want to delete all messages?</string>
@@ -551,6 +567,19 @@
<string name="choose_ringtone_title">Choose ringtone</string> <string name="choose_ringtone_title">Choose ringtone</string>
<string name="cannot_load_ringtone">Cannot load ringtone</string> <string name="cannot_load_ringtone">Cannot load ringtone</string>
<!-- Conversation Settings -->
<string name="disappearing_messages_title">Disappearing messages</string>
<string name="disappearing_messages_explanation_long">Turning on this setting will make new
messages in this conversation automatically disappear 7\u00A0days after being received.
This applies to messages you send to your contact as well as messages your contact sends to you.
Your contact can also change this setting for the both of you.
\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 messages immediately and to your
contact\'s messages once they receive your next message.</string>
<string name="learn_more">Learn more</string>
<string name="disappearing_messages_summary">Make future messages in this conversation automatically disappear 7\u00A0days after being received.</string>
<!-- Settings Feedback --> <!-- Settings Feedback -->
<string name="feedback_settings_title">Feedback</string> <string name="feedback_settings_title">Feedback</string>
<string name="send_feedback">Send feedback</string> <string name="send_feedback">Send feedback</string>

View File

@@ -43,6 +43,16 @@
<item name="android:filterTouchesWhenObscured">true</item> <item name="android:filterTouchesWhenObscured">true</item>
</style> </style>
<style name="BriarFullScreenDialogTheme" parent="BriarDialogTheme">
<item name="android:windowIsFloating">false</item>
<item name="android:windowAnimationStyle">@style/FullScreenDialogAnimation</item>
</style>
<style name="FullScreenDialogAnimation" parent="@android:style/Animation.Activity">
<item name="android:windowEnterAnimation">@anim/step_next_in</item>
<item name="android:windowExitAnimation">@anim/step_next_out</item>
</style>
<!-- Use this with care. Only used for the screen filter warning dialog --> <!-- Use this with care. Only used for the screen filter warning dialog -->
<style name="BriarDialogThemeNoFilter" parent="BriarDialogTheme"> <style name="BriarDialogThemeNoFilter" parent="BriarDialogTheme">
<item name="android:filterTouchesWhenObscured">false</item> <item name="android:filterTouchesWhenObscured">false</item>

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.api.autodelete;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
public interface AutoDeleteConstants {
/**
* The minimum valid auto-delete timer duration in milliseconds.
*/
long MIN_AUTO_DELETE_TIMER_MS = MINUTES.toMillis(1);
/**
* The maximum valid auto-delete timer duration in milliseconds.
*/
long MAX_AUTO_DELETE_TIMER_MS = DAYS.toMillis(365);
/**
* Placeholder value indicating that a message has no auto-delete timer.
* This value should not be sent over the wire - send null instead.
*/
long NO_AUTO_DELETE_TIMER = -1;
}

View File

@@ -0,0 +1,56 @@
package org.briarproject.briar.api.autodelete;
import org.briarproject.bramble.api.contact.ContactId;
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.ClientId;
@NotNullByDefault
public interface AutoDeleteManager {
/**
* The unique ID of the auto-delete client.
*/
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.autodelete");
/**
* The current major version of the auto-delete client.
*/
int MAJOR_VERSION = 0;
/**
* The current minor version of the auto-delete client.
*/
int MINOR_VERSION = 0;
/**
* Returns the auto-delete timer duration for the given contact. Use
* {@link #getAutoDeleteTimer(Transaction, ContactId, long)} if the timer
* will be used in an outgoing message.
*/
long getAutoDeleteTimer(Transaction txn, ContactId c) throws DbException;
/**
* Returns the auto-delete timer duration for the given contact, for use in
* a message with the given timestamp. The timestamp is stored.
*/
long getAutoDeleteTimer(Transaction txn, ContactId c, long timestamp)
throws DbException;
/**
* Sets the auto-delete timer duration for the given contact.
*/
void setAutoDeleteTimer(Transaction txn, ContactId c, long timer)
throws DbException;
/**
* Receives an auto-delete timer duration from the given contact, carried
* in a message with the given timestamp. The local timer is set to the
* same duration unless it has been
* {@link #setAutoDeleteTimer(Transaction, ContactId, long) changed} more
* recently than the remote timer.
*/
void receiveAutoDeleteTimer(Transaction txn, ContactId c, long timer,
long timestamp) throws DbException;
}

View File

@@ -0,0 +1,12 @@
package org.briarproject.briar.api.autodelete;
import org.briarproject.bramble.api.db.DbException;
/**
* Thrown when a database operation is attempted as part of message storing
* and the operation is expecting a different timer state. This
* exception may occur due to concurrent updates and does not indicate a
* database error.
*/
public class UnexpectedTimerException extends DbException {
}

View File

@@ -0,0 +1,28 @@
package org.briarproject.briar.api.autodelete.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AutoDeleteTimerMirroredEvent extends Event {
private final ContactId contactId;
private final long newTimer;
public AutoDeleteTimerMirroredEvent(ContactId contactId, long newTimer) {
this.contactId = contactId;
this.newTimer = newTimer;
}
public ContactId getContactId() {
return contactId;
}
public long getNewTimer() {
return newTimer;
}
}

View File

@@ -15,9 +15,9 @@ public class BlogInvitationRequest extends InvitationRequest<Blog> {
public BlogInvitationRequest(MessageId id, GroupId groupId, long time, public BlogInvitationRequest(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, Blog blog, @Nullable String text, SessionId sessionId, Blog blog, @Nullable String text,
boolean available, boolean canBeOpened) { boolean available, boolean canBeOpened, long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, blog, super(id, groupId, time, local, read, sent, seen, sessionId, blog,
text, available, canBeOpened); text, available, canBeOpened, autoDeleteTimer);
} }
@Override @Override

View File

@@ -12,9 +12,10 @@ public class BlogInvitationResponse extends InvitationResponse {
public BlogInvitationResponse(MessageId id, GroupId groupId, long time, public BlogInvitationResponse(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, boolean accept, GroupId shareableId) { SessionId sessionId, boolean accept, GroupId shareableId,
long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, super(id, groupId, time, local, read, sent, seen, sessionId,
accept, shareableId); accept, shareableId, autoDeleteTimer);
} }
@Override @Override

View File

@@ -18,5 +18,5 @@ public interface BlogSharingManager extends SharingManager<Blog> {
/** /**
* The current minor version of the blog sharing client. * The current minor version of the blog sharing client.
*/ */
int MINOR_VERSION = 0; int MINOR_VERSION = 1;
} }

View File

@@ -48,6 +48,13 @@ public interface ConversationManager {
*/ */
GroupCount getGroupCount(Transaction txn, ContactId c) throws DbException; GroupCount getGroupCount(Transaction txn, ContactId c) throws DbException;
/**
* Returns a timestamp for an outgoing message, which is later than the
* timestamp of any message in the conversation with the given contact.
*/
long getTimestampForOutgoingMessage(Transaction txn, ContactId c)
throws DbException;
/** /**
* Deletes all messages exchanged with the given contact. * Deletes all messages exchanged with the given contact.
*/ */

View File

@@ -12,18 +12,20 @@ public abstract class ConversationMessageHeader {
private final MessageId id; private final MessageId id;
private final GroupId groupId; private final GroupId groupId;
private final long timestamp; private final long timestamp, autoDeleteTimer;
private final boolean local, sent, seen, read; private final boolean local, read, sent, seen;
public ConversationMessageHeader(MessageId id, GroupId groupId, long timestamp, public ConversationMessageHeader(MessageId id, GroupId groupId,
boolean local, boolean read, boolean sent, boolean seen) { long timestamp, boolean local, boolean read, boolean sent,
boolean seen, long autoDeleteTimer) {
this.id = id; this.id = id;
this.groupId = groupId; this.groupId = groupId;
this.timestamp = timestamp; this.timestamp = timestamp;
this.local = local; this.local = local;
this.read = read;
this.sent = sent; this.sent = sent;
this.seen = seen; this.seen = seen;
this.read = read; this.autoDeleteTimer = autoDeleteTimer;
} }
public MessageId getId() { public MessageId getId() {
@@ -55,4 +57,8 @@ public abstract class ConversationMessageHeader {
} }
public abstract <T> T accept(ConversationMessageVisitor<T> v); public abstract <T> T accept(ConversationMessageVisitor<T> v);
public long getAutoDeleteTimer() {
return autoDeleteTimer;
}
} }

View File

@@ -20,11 +20,12 @@ public abstract class ConversationRequest<N extends Nameable>
private final String text; private final String text;
private final boolean answered; private final boolean answered;
public ConversationRequest(MessageId messageId, GroupId groupId, long time, public ConversationRequest(MessageId messageId, GroupId groupId,
boolean local, boolean read, boolean sent, boolean seen, long timestamp, boolean local, boolean read, boolean sent,
SessionId sessionId, N nameable, @Nullable String text, boolean seen, SessionId sessionId, N nameable,
boolean answered) { @Nullable String text, boolean answered, long autoDeleteTimer) {
super(messageId, groupId, time, local, read, sent, seen); super(messageId, groupId, timestamp, local, read, sent, seen,
autoDeleteTimer);
this.sessionId = sessionId; this.sessionId = sessionId;
this.nameable = nameable; this.nameable = nameable;
this.text = text; this.text = text;

View File

@@ -16,8 +16,8 @@ public abstract class ConversationResponse extends ConversationMessageHeader {
public ConversationResponse(MessageId id, GroupId groupId, long time, public ConversationResponse(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, boolean accepted) { SessionId sessionId, boolean accepted, long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen); super(id, groupId, time, local, read, sent, seen, autoDeleteTimer);
this.sessionId = sessionId; this.sessionId = sessionId;
this.accepted = accepted; this.accepted = accepted;
} }

View File

@@ -17,9 +17,9 @@ public class ForumInvitationRequest extends InvitationRequest<Forum> {
public ForumInvitationRequest(MessageId id, GroupId groupId, long time, public ForumInvitationRequest(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, Forum forum, @Nullable String text, SessionId sessionId, Forum forum, @Nullable String text,
boolean available, boolean canBeOpened) { boolean available, boolean canBeOpened, long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, forum, super(id, groupId, time, local, read, sent, seen, sessionId, forum,
text, available, canBeOpened); text, available, canBeOpened, autoDeleteTimer);
} }
@Override @Override

View File

@@ -15,9 +15,10 @@ public class ForumInvitationResponse extends InvitationResponse {
public ForumInvitationResponse(MessageId id, GroupId groupId, long time, public ForumInvitationResponse(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, boolean accept, GroupId shareableId) { SessionId sessionId, boolean accept, GroupId shareableId,
long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, super(id, groupId, time, local, read, sent, seen, sessionId,
accept, shareableId); accept, shareableId, autoDeleteTimer);
} }
@Override @Override

View File

@@ -18,5 +18,5 @@ public interface ForumSharingManager extends SharingManager<Forum> {
/** /**
* The current minor version of the forum sharing client. * The current minor version of the forum sharing client.
*/ */
int MINOR_VERSION = 0; int MINOR_VERSION = 1;
} }

View File

@@ -31,18 +31,18 @@ public interface IntroductionManager extends ConversationClient {
/** /**
* The current minor version of the introduction client. * The current minor version of the introduction client.
*/ */
int MINOR_VERSION = 0; int MINOR_VERSION = 1;
/** /**
* Sends two initial introduction messages. * Sends two initial introduction messages.
*/ */
void makeIntroduction(Contact c1, Contact c2, @Nullable String text, void makeIntroduction(Contact c1, Contact c2, @Nullable String text)
long timestamp) throws DbException; throws DbException;
/** /**
* Responds to an introduction. * Responds to an introduction.
*/ */
void respondToIntroduction(ContactId contactId, SessionId sessionId, void respondToIntroduction(ContactId contactId, SessionId sessionId,
long timestamp, boolean accept) throws DbException; boolean accept) throws DbException;
} }

View File

@@ -18,12 +18,12 @@ public class IntroductionRequest extends ConversationRequest<Author> {
private final AuthorInfo authorInfo; private final AuthorInfo authorInfo;
public IntroductionRequest(MessageId messageId, GroupId groupId, public IntroductionRequest(MessageId messageId, GroupId groupId, long time,
long time, boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, Author author, @Nullable String text, SessionId sessionId, Author author, @Nullable String text,
boolean answered, AuthorInfo authorInfo) { boolean answered, AuthorInfo authorInfo, long autoDeleteTimer) {
super(messageId, groupId, time, local, read, sent, seen, sessionId, super(messageId, groupId, time, local, read, sent, seen, sessionId,
author, text, answered); author, text, answered, autoDeleteTimer);
this.authorInfo = authorInfo; this.authorInfo = authorInfo;
} }

View File

@@ -25,9 +25,10 @@ public class IntroductionResponse extends ConversationResponse {
public IntroductionResponse(MessageId messageId, GroupId groupId, long time, public IntroductionResponse(MessageId messageId, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, boolean accepted, Author author, SessionId sessionId, boolean accepted, Author author,
AuthorInfo introducedAuthorInfo, Role role, boolean canSucceed) { AuthorInfo introducedAuthorInfo, Role role, boolean canSucceed,
long autoDeleteTimer) {
super(messageId, groupId, time, local, read, sent, seen, sessionId, super(messageId, groupId, time, local, read, sent, seen, sessionId,
accepted); accepted, autoDeleteTimer);
this.introducedAuthor = author; this.introducedAuthor = author;
this.introducedAuthorInfo = introducedAuthorInfo; this.introducedAuthorInfo = introducedAuthorInfo;
this.ourRole = role; this.ourRole = role;

View File

@@ -32,13 +32,18 @@ public interface MessagingManager extends ConversationClient {
/** /**
* The current minor version of the messaging client. * The current minor version of the messaging client.
*/ */
int MINOR_VERSION = 2; int MINOR_VERSION = 3;
/** /**
* Stores a local private message. * Stores a local private message.
*/ */
void addLocalMessage(PrivateMessage m) throws DbException; void addLocalMessage(PrivateMessage m) throws DbException;
/**
* Stores a local private message.
*/
void addLocalMessage(Transaction txn, PrivateMessage m) throws DbException;
/** /**
* Stores a local attachment message. * Stores a local attachment message.
* *
@@ -70,12 +75,8 @@ public interface MessagingManager extends ConversationClient {
String getMessageText(MessageId m) throws DbException; String getMessageText(MessageId m) throws DbException;
/** /**
* Returns true if the contact with the given {@link ContactId} does support * Returns the private message format supported by the given contact.
* image attachments.
* <p>
* Added: 2019-01-01
*/ */
boolean contactSupportsImages(Transaction txn, ContactId c) PrivateMessageFormat getContactMessageFormat(Transaction txn, ContactId c)
throws DbException; throws DbException;
} }

View File

@@ -9,44 +9,66 @@ import java.util.List;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class PrivateMessage { public class PrivateMessage {
private final Message message; private final Message message;
private final boolean legacyFormat, hasText; private final boolean hasText;
private final List<AttachmentHeader> attachmentHeaders; private final List<AttachmentHeader> attachmentHeaders;
private final long autoDeleteTimer;
private final PrivateMessageFormat format;
/** /**
* Constructor for private messages in the legacy format, which does not * Constructor for private messages in the
* support attachments. * {@link PrivateMessageFormat#TEXT_ONLY TEXT_ONLY} format.
*/ */
public PrivateMessage(Message message) { public PrivateMessage(Message message) {
this.message = message; this.message = message;
legacyFormat = true;
hasText = true; hasText = true;
attachmentHeaders = emptyList(); attachmentHeaders = emptyList();
autoDeleteTimer = NO_AUTO_DELETE_TIMER;
format = TEXT_ONLY;
} }
/** /**
* Constructor for private messages in the current format, which supports * Constructor for private messages in the
* attachments. * {@link PrivateMessageFormat#TEXT_IMAGES TEXT_IMAGES} format.
*/ */
public PrivateMessage(Message message, boolean hasText, public PrivateMessage(Message message, boolean hasText,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers) {
this.message = message; this.message = message;
this.hasText = hasText; this.hasText = hasText;
this.attachmentHeaders = headers; this.attachmentHeaders = headers;
legacyFormat = false; autoDeleteTimer = NO_AUTO_DELETE_TIMER;
format = TEXT_IMAGES;
}
/**
* Constructor for private messages in the
* {@link PrivateMessageFormat#TEXT_IMAGES_AUTO_DELETE TEXT_IMAGES_AUTO_DELETE}
* format.
*/
public PrivateMessage(Message message, boolean hasText,
List<AttachmentHeader> headers, long autoDeleteTimer) {
this.message = message;
this.hasText = hasText;
this.attachmentHeaders = headers;
this.autoDeleteTimer = autoDeleteTimer;
format = TEXT_IMAGES_AUTO_DELETE;
} }
public Message getMessage() { public Message getMessage() {
return message; return message;
} }
public boolean isLegacyFormat() { public PrivateMessageFormat getFormat() {
return legacyFormat; return format;
} }
public boolean hasText() { public boolean hasText() {
@@ -56,4 +78,8 @@ public class PrivateMessage {
public List<AttachmentHeader> getAttachmentHeaders() { public List<AttachmentHeader> getAttachmentHeaders() {
return attachmentHeaders; return attachmentHeaders;
} }
public long getAutoDeleteTimer() {
return autoDeleteTimer;
}
} }

View File

@@ -12,11 +12,29 @@ import javax.annotation.Nullable;
@NotNullByDefault @NotNullByDefault
public interface PrivateMessageFactory { public interface PrivateMessageFactory {
/**
* Creates a private message in the
* {@link PrivateMessageFormat#TEXT_ONLY TEXT_ONLY} format.
*/
PrivateMessage createLegacyPrivateMessage(GroupId groupId, long timestamp, PrivateMessage createLegacyPrivateMessage(GroupId groupId, long timestamp,
String text) throws FormatException; String text) throws FormatException;
/**
* Creates a private message in the
* {@link PrivateMessageFormat#TEXT_IMAGES TEXT_IMAGES} format. This format
* requires the contact to support client version 0.1 or higher.
*/
PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
@Nullable String text, List<AttachmentHeader> headers) @Nullable String text, List<AttachmentHeader> headers)
throws FormatException; throws FormatException;
/**
* Creates a private message in the
* {@link PrivateMessageFormat#TEXT_IMAGES_AUTO_DELETE TEXT_IMAGES_AUTO_DELETE}
* format. This format requires the contact to support client version 0.3
* or higher.
*/
PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
@Nullable String text, List<AttachmentHeader> headers,
long autoDeleteTimer) throws FormatException;
} }

View File

@@ -0,0 +1,24 @@
package org.briarproject.briar.api.messaging;
public enum PrivateMessageFormat {
/**
* First version of the private message format, which doesn't support
* image attachments or auto-deletion.
*/
TEXT_ONLY,
/**
* Second version of the private message format, which supports image
* attachments but not auto-deletion. Support for this format was
* added in client version 0.1.
*/
TEXT_IMAGES,
/**
* Third version of the private message format, which supports image
* attachments and auto-deletion. Support for this format was added
* in client version 0.3.
*/
TEXT_IMAGES_AUTO_DELETE
}

View File

@@ -20,8 +20,9 @@ public class PrivateMessageHeader extends ConversationMessageHeader {
public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp, public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
boolean hasText, List<AttachmentHeader> headers) { boolean hasText, List<AttachmentHeader> headers,
super(id, groupId, timestamp, local, read, sent, seen); long autoDeleteTimer) {
super(id, groupId, timestamp, local, read, sent, seen, autoDeleteTimer);
this.hasText = hasText; this.hasText = hasText;
this.attachmentHeaders = headers; this.attachmentHeaders = headers;
} }
@@ -38,5 +39,4 @@ public class PrivateMessageHeader extends ConversationMessageHeader {
public <T> T accept(ConversationMessageVisitor<T> v) { public <T> T accept(ConversationMessageVisitor<T> v) {
return v.visitPrivateMessageHeader(this); return v.visitPrivateMessageHeader(this);
} }
} }

View File

@@ -32,7 +32,7 @@ public interface GroupInvitationManager extends ConversationClient {
/** /**
* The current minor version of the private group invitation client. * The current minor version of the private group invitation client.
*/ */
int MINOR_VERSION = 0; int MINOR_VERSION = 1;
/** /**
* Sends an invitation to share the given private group with the given * Sends an invitation to share the given private group with the given
@@ -43,7 +43,8 @@ public interface GroupInvitationManager extends ConversationClient {
* pending. * pending.
*/ */
void sendInvitation(GroupId g, ContactId c, @Nullable String text, void sendInvitation(GroupId g, ContactId c, @Nullable String text,
long timestamp, byte[] signature) throws DbException; long timestamp, byte[] signature, long autoDeleteTimer)
throws DbException;
/** /**
* Responds to a pending private group invitation from the given contact. * Responds to a pending private group invitation from the given contact.

View File

@@ -18,9 +18,10 @@ public class GroupInvitationRequest extends InvitationRequest<PrivateGroup> {
public GroupInvitationRequest(MessageId id, GroupId groupId, long time, public GroupInvitationRequest(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, PrivateGroup shareable, SessionId sessionId, PrivateGroup shareable,
@Nullable String text, boolean available, boolean canBeOpened) { @Nullable String text, boolean available, boolean canBeOpened,
long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, shareable, super(id, groupId, time, local, read, sent, seen, sessionId, shareable,
text, available, canBeOpened); text, available, canBeOpened, autoDeleteTimer);
} }
@Override @Override

View File

@@ -15,9 +15,10 @@ public class GroupInvitationResponse extends InvitationResponse {
public GroupInvitationResponse(MessageId id, GroupId groupId, long time, public GroupInvitationResponse(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, boolean accept, GroupId shareableId) { SessionId sessionId, boolean accept, GroupId shareableId,
long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, super(id, groupId, time, local, read, sent, seen, sessionId,
accept, shareableId); accept, shareableId, autoDeleteTimer);
} }
@Override @Override

View File

@@ -15,9 +15,9 @@ public abstract class InvitationRequest<S extends Shareable> extends
public InvitationRequest(MessageId messageId, GroupId groupId, long time, public InvitationRequest(MessageId messageId, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, S object, @Nullable String text, SessionId sessionId, S object, @Nullable String text,
boolean available, boolean canBeOpened) { boolean available, boolean canBeOpened, long autoDeleteTimer) {
super(messageId, groupId, time, local, read, sent, seen, sessionId, super(messageId, groupId, time, local, read, sent, seen, sessionId,
object, text, !available); object, text, !available, autoDeleteTimer);
this.canBeOpened = canBeOpened; this.canBeOpened = canBeOpened;
} }

View File

@@ -11,8 +11,10 @@ public abstract class InvitationResponse extends ConversationResponse {
public InvitationResponse(MessageId id, GroupId groupId, long time, public InvitationResponse(MessageId id, GroupId groupId, long time,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
SessionId sessionId, boolean accepted, GroupId shareableId) { SessionId sessionId, boolean accepted, GroupId shareableId,
super(id, groupId, time, local, read, sent, seen, sessionId, accepted); long autoDeleteTimer) {
super(id, groupId, time, local, read, sent, seen, sessionId, accepted,
autoDeleteTimer);
this.shareableId = shareableId; this.shareableId = shareableId;
} }

View File

@@ -22,7 +22,7 @@ public interface SharingManager<S extends Shareable>
* including optional text. * including optional text.
*/ */
void sendInvitation(GroupId shareableId, ContactId contactId, void sendInvitation(GroupId shareableId, ContactId contactId,
@Nullable String text, long timestamp) throws DbException; @Nullable String text) throws DbException;
/** /**
* Responds to a pending group invitation * Responds to a pending group invitation

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar; package org.briarproject.briar;
import org.briarproject.briar.autodelete.AutoDeleteModule;
import org.briarproject.briar.avatar.AvatarModule; import org.briarproject.briar.avatar.AvatarModule;
import org.briarproject.briar.blog.BlogModule; import org.briarproject.briar.blog.BlogModule;
import org.briarproject.briar.feed.FeedModule; import org.briarproject.briar.feed.FeedModule;
@@ -13,6 +14,8 @@ import org.briarproject.briar.sharing.SharingModule;
public interface BriarCoreEagerSingletons { public interface BriarCoreEagerSingletons {
void inject(AutoDeleteModule.EagerSingletons init);
void inject(AvatarModule.EagerSingletons init); void inject(AvatarModule.EagerSingletons init);
void inject(BlogModule.EagerSingletons init); void inject(BlogModule.EagerSingletons init);
@@ -36,6 +39,7 @@ public interface BriarCoreEagerSingletons {
class Helper { class Helper {
public static void injectEagerSingletons(BriarCoreEagerSingletons c) { public static void injectEagerSingletons(BriarCoreEagerSingletons c) {
c.inject(new AutoDeleteModule.EagerSingletons());
c.inject(new AvatarModule.EagerSingletons()); c.inject(new AvatarModule.EagerSingletons());
c.inject(new BlogModule.EagerSingletons()); c.inject(new BlogModule.EagerSingletons());
c.inject(new FeedModule.EagerSingletons()); c.inject(new FeedModule.EagerSingletons());

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar; package org.briarproject.briar;
import org.briarproject.briar.attachment.AttachmentModule; import org.briarproject.briar.attachment.AttachmentModule;
import org.briarproject.briar.autodelete.AutoDeleteModule;
import org.briarproject.briar.avatar.AvatarModule; import org.briarproject.briar.avatar.AvatarModule;
import org.briarproject.briar.blog.BlogModule; import org.briarproject.briar.blog.BlogModule;
import org.briarproject.briar.client.BriarClientModule; import org.briarproject.briar.client.BriarClientModule;
@@ -18,6 +19,8 @@ import org.briarproject.briar.test.TestModule;
import dagger.Module; import dagger.Module;
@Module(includes = { @Module(includes = {
AttachmentModule.class,
AutoDeleteModule.class,
AvatarModule.class, AvatarModule.class,
BlogModule.class, BlogModule.class,
BriarClientModule.class, BriarClientModule.class,
@@ -27,7 +30,6 @@ import dagger.Module;
GroupInvitationModule.class, GroupInvitationModule.class,
IdentityModule.class, IdentityModule.class,
IntroductionModule.class, IntroductionModule.class,
AttachmentModule.class,
MessagingModule.class, MessagingModule.class,
PrivateGroupModule.class, PrivateGroupModule.class,
SharingModule.class, SharingModule.class,

View File

@@ -0,0 +1,29 @@
package org.briarproject.briar.autodelete;
interface AutoDeleteConstants {
/**
* Group metadata key for storing the auto-delete timer duration.
*/
String GROUP_KEY_TIMER = "autoDeleteTimer";
/**
* Group metadata key for storing the timestamp of the latest incoming or
* outgoing message carrying an auto-delete timer (including a null timer).
*/
String GROUP_KEY_TIMESTAMP = "autoDeleteTimestamp";
/**
* Group metadata key for storing the previous auto-delete timer duration.
* This is used to decide whether a local change to the duration should be
* overwritten by a duration received from the contact.
*/
String GROUP_KEY_PREVIOUS_TIMER = "autoDeletePreviousTimer";
/**
* Special value for {@link #GROUP_KEY_PREVIOUS_TIMER} indicating that
* there are no local changes to the auto-delete timer duration that need
* to be compared with durations received from the contact.
*/
long NO_PREVIOUS_TIMER = 0;
}

View File

@@ -0,0 +1,191 @@
package org.briarproject.briar.autodelete;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
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.lifecycle.LifecycleManager.OpenDatabaseHook;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupFactory;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.autodelete.event.AutoDeleteTimerMirroredEvent;
import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MAX_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.autodelete.AutoDeleteConstants.GROUP_KEY_PREVIOUS_TIMER;
import static org.briarproject.briar.autodelete.AutoDeleteConstants.GROUP_KEY_TIMER;
import static org.briarproject.briar.autodelete.AutoDeleteConstants.GROUP_KEY_TIMESTAMP;
import static org.briarproject.briar.autodelete.AutoDeleteConstants.NO_PREVIOUS_TIMER;
@Immutable
@NotNullByDefault
class AutoDeleteManagerImpl
implements AutoDeleteManager, OpenDatabaseHook, ContactHook {
private static final Logger LOG =
getLogger(AutoDeleteManagerImpl.class.getName());
private final DatabaseComponent db;
private final ClientHelper clientHelper;
private final GroupFactory groupFactory;
private final Group localGroup;
@Inject
AutoDeleteManagerImpl(
DatabaseComponent db,
ClientHelper clientHelper,
GroupFactory groupFactory,
ContactGroupFactory contactGroupFactory) {
this.db = db;
this.clientHelper = clientHelper;
this.groupFactory = groupFactory;
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
MAJOR_VERSION);
}
@Override
public void onDatabaseOpened(Transaction txn) throws DbException {
if (db.containsGroup(txn, localGroup.getId())) return;
db.addGroup(txn, localGroup);
// Set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
}
@Override
public void addingContact(Transaction txn, Contact c) throws DbException {
Group g = getGroup(c);
db.addGroup(txn, g);
clientHelper.setContactId(txn, g.getId(), c.getId());
}
@Override
public void removingContact(Transaction txn, Contact c) throws DbException {
db.removeGroup(txn, getGroup(c));
}
@Override
public long getAutoDeleteTimer(Transaction txn, ContactId c)
throws DbException {
try {
Group g = getGroup(db.getContact(txn, c));
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
return meta.getLong(GROUP_KEY_TIMER, NO_AUTO_DELETE_TIMER);
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public long getAutoDeleteTimer(Transaction txn, ContactId c, long timestamp)
throws DbException {
try {
Group g = getGroup(db.getContact(txn, c));
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
long timer = meta.getLong(GROUP_KEY_TIMER, NO_AUTO_DELETE_TIMER);
if (LOG.isLoggable(INFO)) {
LOG.info("Sending message with auto-delete timer " + timer);
}
// Update the timestamp and clear the previous timer, if any
meta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_TIMESTAMP, timestamp),
new BdfEntry(GROUP_KEY_PREVIOUS_TIMER, NO_PREVIOUS_TIMER));
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
return timer;
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public void setAutoDeleteTimer(Transaction txn, ContactId c, long timer)
throws DbException {
validateTimer(timer);
try {
Group g = getGroup(db.getContact(txn, c));
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
long oldTimer = meta.getLong(GROUP_KEY_TIMER, NO_AUTO_DELETE_TIMER);
if (timer == oldTimer) return;
if (LOG.isLoggable(INFO)) {
LOG.info("Setting auto-delete timer to " + timer);
}
// Store the new timer and the previous timer
meta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_TIMER, timer),
new BdfEntry(GROUP_KEY_PREVIOUS_TIMER, oldTimer));
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public void receiveAutoDeleteTimer(Transaction txn, ContactId c,
long timer, long timestamp) throws DbException {
validateTimer(timer);
try {
Group g = getGroup(db.getContact(txn, c));
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
long oldTimestamp = meta.getLong(GROUP_KEY_TIMESTAMP, 0L);
if (timestamp <= oldTimestamp) return;
long oldTimer =
meta.getLong(GROUP_KEY_PREVIOUS_TIMER, NO_PREVIOUS_TIMER);
meta = new BdfDictionary();
if (oldTimer == NO_PREVIOUS_TIMER) {
// We don't have an unsent change. Mirror their timer
if (LOG.isLoggable(INFO)) {
LOG.info("Mirroring auto-delete timer " + timer);
}
meta.put(GROUP_KEY_TIMER, timer);
txn.attach(new AutoDeleteTimerMirroredEvent(c, timer));
} else if (timer != oldTimer) {
// Their sent change trumps our unsent change. Mirror their
// timer and clear the previous timer to drop our unsent change
if (LOG.isLoggable(INFO)) {
LOG.info("Mirroring auto-delete timer " + timer
+ " and forgetting unsent change");
}
meta.put(GROUP_KEY_TIMER, timer);
meta.put(GROUP_KEY_PREVIOUS_TIMER, NO_PREVIOUS_TIMER);
txn.attach(new AutoDeleteTimerMirroredEvent(c, timer));
}
// Always update the timestamp
meta.put(GROUP_KEY_TIMESTAMP, timestamp);
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new DbException(e);
}
}
private Group getGroup(Contact c) {
byte[] descriptor = c.getAuthor().getId().getBytes();
return groupFactory.createGroup(CLIENT_ID, MAJOR_VERSION, descriptor);
}
private void validateTimer(long timer) {
if (timer != NO_AUTO_DELETE_TIMER &&
(timer < MIN_AUTO_DELETE_TIMER_MS ||
timer > MAX_AUTO_DELETE_TIMER_MS)) {
throw new IllegalArgumentException();
}
}
}

View File

@@ -0,0 +1,32 @@
package org.briarproject.briar.autodelete;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class AutoDeleteModule {
public static class EagerSingletons {
@Inject
AutoDeleteManager autoDeleteManager;
}
@Provides
@Singleton
AutoDeleteManager provideAutoDeleteManager(
LifecycleManager lifecycleManager, ContactManager contactManager,
AutoDeleteManagerImpl autoDeleteManager) {
lifecycleManager.registerOpenDatabaseHook(autoDeleteManager);
contactManager.registerContactHook(autoDeleteManager);
// Don't need to register with the client versioning manager as this
// client's groups aren't shared with contacts
return autoDeleteManager;
}
}

View File

@@ -8,6 +8,8 @@ import org.briarproject.briar.api.client.SessionId;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class AbortMessage extends AbstractIntroductionMessage { class AbortMessage extends AbstractIntroductionMessage {
@@ -16,7 +18,8 @@ class AbortMessage extends AbstractIntroductionMessage {
protected AbortMessage(MessageId messageId, GroupId groupId, long timestamp, protected AbortMessage(MessageId messageId, GroupId groupId, long timestamp,
@Nullable MessageId previousMessageId, SessionId sessionId) { @Nullable MessageId previousMessageId, SessionId sessionId) {
super(messageId, groupId, timestamp, previousMessageId); super(messageId, groupId, timestamp, previousMessageId,
NO_AUTO_DELETE_TIMER);
this.sessionId = sessionId; this.sessionId = sessionId;
} }

View File

@@ -16,13 +16,16 @@ abstract class AbstractIntroductionMessage {
private final long timestamp; private final long timestamp;
@Nullable @Nullable
private final MessageId previousMessageId; private final MessageId previousMessageId;
private final long autoDeleteTimer;
AbstractIntroductionMessage(MessageId messageId, GroupId groupId, AbstractIntroductionMessage(MessageId messageId, GroupId groupId,
long timestamp, @Nullable MessageId previousMessageId) { long timestamp, @Nullable MessageId previousMessageId,
long autoDeleteTimer) {
this.messageId = messageId; this.messageId = messageId;
this.groupId = groupId; this.groupId = groupId;
this.timestamp = timestamp; this.timestamp = timestamp;
this.previousMessageId = previousMessageId; this.previousMessageId = previousMessageId;
this.autoDeleteTimer = autoDeleteTimer;
} }
MessageId getMessageId() { MessageId getMessageId() {
@@ -42,4 +45,7 @@ abstract class AbstractIntroductionMessage {
return previousMessageId; return previousMessageId;
} }
public long getAutoDeleteTimer() {
return autoDeleteTimer;
}
} }

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.client.ContactGroupFactory; import org.briarproject.bramble.api.client.ContactGroupFactory;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
@@ -16,11 +17,15 @@ import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorInfo; import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager; import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.introduction.IntroductionResponse; import org.briarproject.briar.api.introduction.IntroductionResponse;
@@ -31,6 +36,9 @@ import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
import static org.briarproject.briar.introduction.MessageType.ABORT; import static org.briarproject.briar.introduction.MessageType.ABORT;
import static org.briarproject.briar.introduction.MessageType.ACCEPT; import static org.briarproject.briar.introduction.MessageType.ACCEPT;
import static org.briarproject.briar.introduction.MessageType.ACTIVATE; import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
@@ -40,7 +48,7 @@ import static org.briarproject.briar.introduction.MessageType.REQUEST;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
abstract class AbstractProtocolEngine<S extends Session> abstract class AbstractProtocolEngine<S extends Session<?>>
implements ProtocolEngine<S> { implements ProtocolEngine<S> {
protected final DatabaseComponent db; protected final DatabaseComponent db;
@@ -52,6 +60,9 @@ abstract class AbstractProtocolEngine<S extends Session>
protected final AuthorManager authorManager; protected final AuthorManager authorManager;
protected final MessageParser messageParser; protected final MessageParser messageParser;
protected final MessageEncoder messageEncoder; protected final MessageEncoder messageEncoder;
protected final ClientVersioningManager clientVersioningManager;
protected final AutoDeleteManager autoDeleteManager;
protected final ConversationManager conversationManager;
protected final Clock clock; protected final Clock clock;
AbstractProtocolEngine( AbstractProtocolEngine(
@@ -64,6 +75,9 @@ abstract class AbstractProtocolEngine<S extends Session>
AuthorManager authorManager, AuthorManager authorManager,
MessageParser messageParser, MessageParser messageParser,
MessageEncoder messageEncoder, MessageEncoder messageEncoder,
ClientVersioningManager clientVersioningManager,
AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager,
Clock clock) { Clock clock) {
this.db = db; this.db = db;
this.clientHelper = clientHelper; this.clientHelper = clientHelper;
@@ -74,16 +88,29 @@ abstract class AbstractProtocolEngine<S extends Session>
this.authorManager = authorManager; this.authorManager = authorManager;
this.messageParser = messageParser; this.messageParser = messageParser;
this.messageEncoder = messageEncoder; this.messageEncoder = messageEncoder;
this.clientVersioningManager = clientVersioningManager;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
this.clock = clock; this.clock = clock;
} }
Message sendRequestMessage(Transaction txn, PeerSession s, Message sendRequestMessage(Transaction txn, PeerSession s,
long timestamp, Author author, @Nullable String text) long timestamp, Author author, @Nullable String text)
throws DbException { throws DbException {
Message m = messageEncoder Message m;
.encodeRequestMessage(s.getContactGroupId(), timestamp, ContactId c = getContactId(txn, s.getContactGroupId());
s.getLastLocalMessageId(), author, text); if (contactSupportsAutoDeletion(txn, c)) {
sendMessage(txn, REQUEST, s.getSessionId(), m, true); long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
timestamp);
m = messageEncoder.encodeRequestMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), author, text, timer);
sendMessage(txn, REQUEST, s.getSessionId(), m, true, timer);
} else {
m = messageEncoder.encodeRequestMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), author, text);
sendMessage(txn, REQUEST, s.getSessionId(), m, true,
NO_AUTO_DELETE_TIMER);
}
return m; return m;
} }
@@ -91,21 +118,43 @@ abstract class AbstractProtocolEngine<S extends Session>
PublicKey ephemeralPublicKey, long acceptTimestamp, PublicKey ephemeralPublicKey, long acceptTimestamp,
Map<TransportId, TransportProperties> transportProperties, Map<TransportId, TransportProperties> transportProperties,
boolean visible) throws DbException { boolean visible) throws DbException {
Message m = messageEncoder Message m;
.encodeAcceptMessage(s.getContactGroupId(), timestamp, ContactId c = getContactId(txn, s.getContactGroupId());
s.getLastLocalMessageId(), s.getSessionId(), if (contactSupportsAutoDeletion(txn, c)) {
ephemeralPublicKey, acceptTimestamp, long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
transportProperties); timestamp);
sendMessage(txn, ACCEPT, s.getSessionId(), m, visible); m = messageEncoder.encodeAcceptMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId(),
ephemeralPublicKey, acceptTimestamp, transportProperties,
timer);
sendMessage(txn, ACCEPT, s.getSessionId(), m, visible, timer);
} else {
m = messageEncoder.encodeAcceptMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId(),
ephemeralPublicKey, acceptTimestamp, transportProperties);
sendMessage(txn, ACCEPT, s.getSessionId(), m, visible,
NO_AUTO_DELETE_TIMER);
}
return m; return m;
} }
Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp, Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp,
boolean visible) throws DbException { boolean visible) throws DbException {
Message m = messageEncoder Message m;
.encodeDeclineMessage(s.getContactGroupId(), timestamp, ContactId c = getContactId(txn, s.getContactGroupId());
s.getLastLocalMessageId(), s.getSessionId()); if (contactSupportsAutoDeletion(txn, c)) {
sendMessage(txn, DECLINE, s.getSessionId(), m, visible); long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
timestamp);
m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId(),
timer);
sendMessage(txn, DECLINE, s.getSessionId(), m, visible, timer);
} else {
m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId());
sendMessage(txn, DECLINE, s.getSessionId(), m, visible,
NO_AUTO_DELETE_TIMER);
}
return m; return m;
} }
@@ -115,7 +164,8 @@ abstract class AbstractProtocolEngine<S extends Session>
.encodeAuthMessage(s.getContactGroupId(), timestamp, .encodeAuthMessage(s.getContactGroupId(), timestamp,
s.getLastLocalMessageId(), s.getSessionId(), mac, s.getLastLocalMessageId(), s.getSessionId(), mac,
signature); signature);
sendMessage(txn, AUTH, s.getSessionId(), m, false); sendMessage(txn, AUTH, s.getSessionId(), m, false,
NO_AUTO_DELETE_TIMER);
return m; return m;
} }
@@ -124,7 +174,8 @@ abstract class AbstractProtocolEngine<S extends Session>
Message m = messageEncoder Message m = messageEncoder
.encodeActivateMessage(s.getContactGroupId(), timestamp, .encodeActivateMessage(s.getContactGroupId(), timestamp,
s.getLastLocalMessageId(), s.getSessionId(), mac); s.getLastLocalMessageId(), s.getSessionId(), mac);
sendMessage(txn, ACTIVATE, s.getSessionId(), m, false); sendMessage(txn, ACTIVATE, s.getSessionId(), m, false,
NO_AUTO_DELETE_TIMER);
return m; return m;
} }
@@ -133,16 +184,17 @@ abstract class AbstractProtocolEngine<S extends Session>
Message m = messageEncoder Message m = messageEncoder
.encodeAbortMessage(s.getContactGroupId(), timestamp, .encodeAbortMessage(s.getContactGroupId(), timestamp,
s.getLastLocalMessageId(), s.getSessionId()); s.getLastLocalMessageId(), s.getSessionId());
sendMessage(txn, ABORT, s.getSessionId(), m, false); sendMessage(txn, ABORT, s.getSessionId(), m, false,
NO_AUTO_DELETE_TIMER);
return m; return m;
} }
private void sendMessage(Transaction txn, MessageType type, private void sendMessage(Transaction txn, MessageType type,
SessionId sessionId, Message m, boolean visibleInConversation) SessionId sessionId, Message m, boolean visibleInConversation,
throws DbException { long autoDeleteTimer) throws DbException {
BdfDictionary meta = messageEncoder BdfDictionary meta = messageEncoder.encodeMetadata(type, sessionId,
.encodeMetadata(type, sessionId, m.getTimestamp(), true, true, m.getTimestamp(), true, true, visibleInConversation,
visibleInConversation); autoDeleteTimer);
try { try {
clientHelper.addLocalMessage(txn, m, meta, true, false); clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) { } catch (FormatException e) {
@@ -150,9 +202,10 @@ abstract class AbstractProtocolEngine<S extends Session>
} }
} }
void broadcastIntroductionResponseReceivedEvent(Transaction txn, Session s, void broadcastIntroductionResponseReceivedEvent(Transaction txn,
AuthorId sender, Author otherAuthor, AbstractIntroductionMessage m, Session<?> s, AuthorId sender, Author otherAuthor,
boolean canSucceed) throws DbException { AbstractIntroductionMessage m, boolean canSucceed)
throws DbException {
AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId(); AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
Contact c = contactManager.getContact(txn, sender, localAuthorId); Contact c = contactManager.getContact(txn, sender, localAuthorId);
AuthorInfo otherAuthorInfo = AuthorInfo otherAuthorInfo =
@@ -161,7 +214,8 @@ abstract class AbstractProtocolEngine<S extends Session>
new IntroductionResponse(m.getMessageId(), m.getGroupId(), new IntroductionResponse(m.getMessageId(), m.getGroupId(),
m.getTimestamp(), false, false, false, false, m.getTimestamp(), false, false, false, false,
s.getSessionId(), m instanceof AcceptMessage, s.getSessionId(), m instanceof AcceptMessage,
otherAuthor, otherAuthorInfo, s.getRole(), canSucceed); otherAuthor, otherAuthorInfo, s.getRole(), canSucceed,
m.getAutoDeleteTimer());
IntroductionResponseReceivedEvent e = IntroductionResponseReceivedEvent e =
new IntroductionResponseReceivedEvent(response, c.getId()); new IntroductionResponseReceivedEvent(response, c.getId());
txn.attach(e); txn.attach(e);
@@ -184,14 +238,33 @@ abstract class AbstractProtocolEngine<S extends Session>
return !dependency.equals(lastRemoteMessageId); return !dependency.equals(lastRemoteMessageId);
} }
long getLocalTimestamp(long localTimestamp, long requestTimestamp) { long getTimestampForOutgoingMessage(Transaction txn, GroupId contactGroupId)
return Math.max( throws DbException {
clock.currentTimeMillis(), ContactId c = getContactId(txn, contactGroupId);
Math.max( return conversationManager.getTimestampForOutgoingMessage(txn, c);
localTimestamp,
requestTimestamp
) + 1
);
} }
void receiveAutoDeleteTimer(Transaction txn, AbstractIntroductionMessage m)
throws DbException {
ContactId c = getContactId(txn, m.getGroupId());
autoDeleteManager.receiveAutoDeleteTimer(txn, c, m.getAutoDeleteTimer(),
m.getTimestamp());
}
private ContactId getContactId(Transaction txn, GroupId contactGroupId)
throws DbException {
try {
return clientHelper.getContactId(txn, contactGroupId);
} catch (FormatException e) {
throw new DbException(e);
}
}
private boolean contactSupportsAutoDeletion(Transaction txn, ContactId c)
throws DbException {
int minorVersion = clientVersioningManager.getClientMinorVersion(txn, c,
CLIENT_ID, MAJOR_VERSION);
// Auto-delete was added in client version 0.1
return minorVersion >= 1;
}
} }

View File

@@ -26,8 +26,10 @@ class AcceptMessage extends AbstractIntroductionMessage {
long timestamp, @Nullable MessageId previousMessageId, long timestamp, @Nullable MessageId previousMessageId,
SessionId sessionId, PublicKey ephemeralPublicKey, SessionId sessionId, PublicKey ephemeralPublicKey,
long acceptTimestamp, long acceptTimestamp,
Map<TransportId, TransportProperties> transportProperties) { Map<TransportId, TransportProperties> transportProperties,
super(messageId, groupId, timestamp, previousMessageId); long autoDeleteTimer) {
super(messageId, groupId, timestamp, previousMessageId,
autoDeleteTimer);
this.sessionId = sessionId; this.sessionId = sessionId;
this.ephemeralPublicKey = ephemeralPublicKey; this.ephemeralPublicKey = ephemeralPublicKey;
this.acceptTimestamp = acceptTimestamp; this.acceptTimestamp = acceptTimestamp;

View File

@@ -7,6 +7,8 @@ import org.briarproject.briar.api.client.SessionId;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class ActivateMessage extends AbstractIntroductionMessage { class ActivateMessage extends AbstractIntroductionMessage {
@@ -17,7 +19,8 @@ class ActivateMessage extends AbstractIntroductionMessage {
protected ActivateMessage(MessageId messageId, GroupId groupId, protected ActivateMessage(MessageId messageId, GroupId groupId,
long timestamp, MessageId previousMessageId, SessionId sessionId, long timestamp, MessageId previousMessageId, SessionId sessionId,
byte[] mac) { byte[] mac) {
super(messageId, groupId, timestamp, previousMessageId); super(messageId, groupId, timestamp, previousMessageId,
NO_AUTO_DELETE_TIMER);
this.sessionId = sessionId; this.sessionId = sessionId;
this.mac = mac; this.mac = mac;
} }

View File

@@ -7,6 +7,8 @@ import org.briarproject.briar.api.client.SessionId;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class AuthMessage extends AbstractIntroductionMessage { class AuthMessage extends AbstractIntroductionMessage {
@@ -17,7 +19,8 @@ class AuthMessage extends AbstractIntroductionMessage {
protected AuthMessage(MessageId messageId, GroupId groupId, protected AuthMessage(MessageId messageId, GroupId groupId,
long timestamp, MessageId previousMessageId, SessionId sessionId, long timestamp, MessageId previousMessageId, SessionId sessionId,
byte[] mac, byte[] signature) { byte[] mac, byte[] signature) {
super(messageId, groupId, timestamp, previousMessageId); super(messageId, groupId, timestamp, previousMessageId,
NO_AUTO_DELETE_TIMER);
this.sessionId = sessionId; this.sessionId = sessionId;
this.mac = mac; this.mac = mac;
this.signature = signature; this.signature = signature;

Some files were not shown because too many files have changed in this diff Show More