mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Compare commits
10 Commits
68-test-ou
...
block-inpu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
707066f1a1 | ||
|
|
65e5dc266f | ||
|
|
3487a7cfee | ||
|
|
eea453fc5f | ||
|
|
c6f460a936 | ||
|
|
a5c9e7c74d | ||
|
|
8a4a343147 | ||
|
|
7b22d3b84d | ||
|
|
c8fa23273f | ||
|
|
fbe5df8938 |
@@ -208,6 +208,24 @@ public interface DatabaseComponent {
|
||||
Collection<Message> generateRequestedBatch(Transaction txn, ContactId c,
|
||||
int maxLength, int maxLatency) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the number of blocks in the given message.
|
||||
* <p>
|
||||
* Read-only.
|
||||
*/
|
||||
int getBlockCount(Transaction txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the given block of the given message.
|
||||
* <p>
|
||||
* Read-only.
|
||||
*
|
||||
* @throws NoSuchBlockException if 'blockNumber' is greater than or equal
|
||||
* to the number of blocks in the message
|
||||
*/
|
||||
byte[] getBlock(Transaction txn, MessageId m, int blockNumber)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the contact with the given ID.
|
||||
* <p/>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.briarproject.bramble.api.db;
|
||||
|
||||
/**
|
||||
* Thrown when a database operation is attempted for a block that is not in
|
||||
* the database. This exception may occur due to concurrent updates and does
|
||||
* not indicate a database error.
|
||||
*/
|
||||
public class NoSuchBlockException extends DbException {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.briarproject.bramble.api.io;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchBlockException;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
public interface BlockSource {
|
||||
|
||||
/**
|
||||
* Returns the number of blocks in the given message.
|
||||
*/
|
||||
int getBlockCount(MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the given block of the given message.
|
||||
*
|
||||
* @throws NoSuchBlockException if 'blockNumber' is greater than or equal
|
||||
* to the number of blocks in the message
|
||||
*/
|
||||
byte[] getBlock(MessageId m, int blockNumber) throws DbException;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.briarproject.bramble.api.io;
|
||||
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface MessageInputStreamFactory {
|
||||
|
||||
/**
|
||||
* Returns an {@link InputStream} for reading the given message from the
|
||||
* database. This method returns immediately. If the message is not in the
|
||||
* database or cannot be read, reading from the stream will throw an
|
||||
* {@link IOException};
|
||||
*/
|
||||
InputStream getMessageInputStream(MessageId m);
|
||||
}
|
||||
@@ -35,4 +35,9 @@ public interface SyncConstants {
|
||||
* The maximum number of message IDs in an ack, offer or request record.
|
||||
*/
|
||||
int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH;
|
||||
|
||||
/**
|
||||
* The maximum length of a message block in bytes.
|
||||
*/
|
||||
int MAX_BLOCK_LENGTH = 32 * 2014; // 32 KiB
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.briarproject.bramble.db.DatabaseExecutorModule;
|
||||
import org.briarproject.bramble.db.DatabaseModule;
|
||||
import org.briarproject.bramble.event.EventModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.io.IoModule;
|
||||
import org.briarproject.bramble.keyagreement.KeyAgreementModule;
|
||||
import org.briarproject.bramble.lifecycle.LifecycleModule;
|
||||
import org.briarproject.bramble.plugin.PluginModule;
|
||||
@@ -36,6 +37,7 @@ import dagger.Module;
|
||||
DatabaseExecutorModule.class,
|
||||
EventModule.class,
|
||||
IdentityModule.class,
|
||||
IoModule.class,
|
||||
KeyAgreementModule.class,
|
||||
LifecycleModule.class,
|
||||
PluginModule.class,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.briarproject.bramble.db;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.io.BlockSource;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
class BlockSourceImpl implements BlockSource {
|
||||
|
||||
private final DatabaseComponent db;
|
||||
|
||||
@Inject
|
||||
BlockSourceImpl(DatabaseComponent db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBlockCount(MessageId m) throws DbException {
|
||||
return db.transactionWithResult(true, txn -> db.getBlockCount(txn, m));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBlock(MessageId m, int blockNumber) throws DbException {
|
||||
return db.transactionWithResult(true, txn ->
|
||||
db.getBlock(txn, m, blockNumber));
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.DbRunnable;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.MigrationListener;
|
||||
import org.briarproject.bramble.api.db.NoSuchBlockException;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
||||
import org.briarproject.bramble.api.db.NoSuchLocalAuthorException;
|
||||
@@ -420,6 +421,26 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBlockCount(Transaction transaction, MessageId m)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsMessage(txn, m))
|
||||
throw new NoSuchMessageException();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBlock(Transaction transaction, MessageId m,
|
||||
int blockNumber) throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
if (!db.containsMessage(txn, m))
|
||||
throw new NoSuchMessageException();
|
||||
if (blockNumber != 0)
|
||||
throw new NoSuchBlockException();
|
||||
return db.getMessage(txn, m).getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contact getContact(Transaction transaction, ContactId c)
|
||||
throws DbException {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.bramble.db;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DatabaseConfig;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.io.BlockSource;
|
||||
import org.briarproject.bramble.api.lifecycle.ShutdownManager;
|
||||
import org.briarproject.bramble.api.sync.MessageFactory;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
@@ -31,4 +32,9 @@ public class DatabaseModule {
|
||||
return new DatabaseComponentImpl<>(db, Connection.class, eventBus,
|
||||
shutdown);
|
||||
}
|
||||
|
||||
@Provides
|
||||
BlockSource provideBlockSource(BlockSourceImpl blockSource) {
|
||||
return blockSource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.lang.System.arraycopy;
|
||||
import static java.lang.Thread.currentThread;
|
||||
|
||||
/**
|
||||
* An {@link InputStream} that asynchronously fetches blocks of data on demand.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
abstract class BlockInputStream extends InputStream {
|
||||
|
||||
private final int minBufferBytes;
|
||||
private final BlockingQueue<Buffer> queue = new ArrayBlockingQueue<>(1);
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Buffer buffer = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private int offset = 0;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean fetchingBlock = false;
|
||||
|
||||
abstract void fetchBlockAsync(int blockNumber);
|
||||
|
||||
BlockInputStream(int minBufferBytes) {
|
||||
this.minBufferBytes = minBufferBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
synchronized (lock) {
|
||||
if (!prepareRead()) return -1;
|
||||
if (buffer == null) throw new AssertionError();
|
||||
return buffer.data[offset++] & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (off < 0 || len < 0 || off + len > b.length)
|
||||
throw new IllegalArgumentException();
|
||||
synchronized (lock) {
|
||||
if (!prepareRead()) return -1;
|
||||
if (buffer == null) throw new AssertionError();
|
||||
len = Math.min(len, buffer.length - offset);
|
||||
if (len < 0) throw new AssertionError();
|
||||
arraycopy(buffer.data, offset, b, off, len);
|
||||
offset += len;
|
||||
return len;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean prepareRead() throws IOException {
|
||||
throwExceptionIfNecessary();
|
||||
if (isEndOfStream()) return false;
|
||||
if (shouldFetchBlock()) fetchBlockAsync();
|
||||
waitForBlock();
|
||||
if (buffer == null) throw new AssertionError();
|
||||
return offset < buffer.length;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void throwExceptionIfNecessary() throws IOException {
|
||||
if (buffer != null && buffer.exception != null)
|
||||
throw new IOException(buffer.exception);
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean isEndOfStream() {
|
||||
return buffer != null && offset == buffer.length && !fetchingBlock;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean shouldFetchBlock() {
|
||||
if (fetchingBlock) return false;
|
||||
if (buffer == null) return true;
|
||||
if (buffer.length == 0) return false;
|
||||
return buffer.length - offset < minBufferBytes;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void fetchBlockAsync() {
|
||||
if (buffer == null) fetchBlockAsync(0);
|
||||
else fetchBlockAsync(buffer.blockNumber + 1);
|
||||
fetchingBlock = true;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void waitForBlock() throws IOException {
|
||||
if (buffer != null && offset < buffer.length) return;
|
||||
try {
|
||||
buffer = queue.take();
|
||||
} catch (InterruptedException e) {
|
||||
currentThread().interrupt();
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
fetchingBlock = false;
|
||||
offset = 0;
|
||||
throwExceptionIfNecessary();
|
||||
}
|
||||
|
||||
void fetchSucceeded(int blockNumber, byte[] data, int length) {
|
||||
queue.add(new Buffer(blockNumber, data, length));
|
||||
}
|
||||
|
||||
void fetchFailed(int blockNumber, Exception exception) {
|
||||
queue.add(new Buffer(blockNumber, exception));
|
||||
}
|
||||
|
||||
private static class Buffer {
|
||||
|
||||
private final int blockNumber;
|
||||
private final byte[] data;
|
||||
private final int length;
|
||||
@Nullable
|
||||
private final Exception exception;
|
||||
|
||||
private Buffer(int blockNumber, byte[] data, int length) {
|
||||
if (length < 0 || length > data.length)
|
||||
throw new IllegalArgumentException();
|
||||
this.blockNumber = blockNumber;
|
||||
this.data = data;
|
||||
this.length = length;
|
||||
exception = null;
|
||||
}
|
||||
|
||||
private Buffer(int blockNumber, Exception exception) {
|
||||
this.blockNumber = blockNumber;
|
||||
this.exception = exception;
|
||||
data = new byte[0];
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.io.BlockSource;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* A {@link BlockInputStream} that fetches data from a {@link BlockSource}.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class BlockSourceInputStream extends BlockInputStream {
|
||||
|
||||
private final Executor executor;
|
||||
private final BlockSource blockSource;
|
||||
private final MessageId messageId;
|
||||
|
||||
private volatile int blockCount = -1;
|
||||
|
||||
BlockSourceInputStream(int minBufferBytes, Executor executor,
|
||||
BlockSource blockSource, MessageId messageId) {
|
||||
super(minBufferBytes);
|
||||
this.executor = executor;
|
||||
this.blockSource = blockSource;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
void fetchBlockAsync(int blockNumber) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
if (blockCount == -1) {
|
||||
blockCount = blockSource.getBlockCount(messageId);
|
||||
}
|
||||
if (blockNumber > blockCount) {
|
||||
fetchFailed(blockNumber, new IllegalArgumentException());
|
||||
} else if (blockNumber == blockCount) {
|
||||
fetchSucceeded(blockNumber, new byte[0], 0); // EOF
|
||||
} else {
|
||||
byte[] block = blockSource.getBlock(messageId, blockNumber);
|
||||
fetchSucceeded(blockNumber, block, block.length);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
fetchFailed(blockNumber, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.io.MessageInputStreamFactory;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
public class IoModule {
|
||||
|
||||
@Provides
|
||||
MessageInputStreamFactory provideMessageInputStreamFactory(
|
||||
MessageInputStreamFactoryImpl messageInputStreamFactory) {
|
||||
return messageInputStreamFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.io.BlockSource;
|
||||
import org.briarproject.bramble.api.io.MessageInputStreamFactory;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_BLOCK_LENGTH;
|
||||
|
||||
class MessageInputStreamFactoryImpl implements MessageInputStreamFactory {
|
||||
|
||||
private final Executor dbExecutor;
|
||||
private final BlockSource blockSource;
|
||||
|
||||
@Inject
|
||||
MessageInputStreamFactoryImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
BlockSource blockSource) {
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.blockSource = blockSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getMessageInputStream(MessageId m) {
|
||||
return new BlockSourceInputStream(MAX_BLOCK_LENGTH, dbExecutor,
|
||||
blockSource, m);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.io.BlockSource;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.concurrent.Synchroniser;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static java.util.concurrent.Executors.newSingleThreadExecutor;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_BLOCK_LENGTH;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.spongycastle.util.Arrays.copyOfRange;
|
||||
|
||||
public class BlockSourceInputStreamTest extends BrambleMockTestCase {
|
||||
|
||||
private static final int MAX_DATA_BYTES = 1_000_000;
|
||||
private static final int READ_BUFFER_BYTES = 4 * 1024;
|
||||
|
||||
private final BlockSource blockSource;
|
||||
|
||||
private final Random random = new Random();
|
||||
private final Executor executor = newSingleThreadExecutor();
|
||||
private final MessageId messageId = new MessageId(getRandomId());
|
||||
|
||||
public BlockSourceInputStreamTest() {
|
||||
context.setThreadingPolicy(new Synchroniser());
|
||||
blockSource = context.mock(BlockSource.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadSingleBytes() throws IOException {
|
||||
byte[] data = createRandomData();
|
||||
BlockSource source = new ByteArrayBlockSource(data, MAX_BLOCK_LENGTH);
|
||||
InputStream in = new BlockSourceInputStream(MAX_BLOCK_LENGTH, executor,
|
||||
source, messageId);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
//noinspection ForLoopReplaceableByForEach
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
int read = in.read();
|
||||
assertNotEquals(-1, read);
|
||||
out.write(read);
|
||||
}
|
||||
assertEquals(-1, in.read());
|
||||
in.close();
|
||||
out.flush();
|
||||
out.close();
|
||||
assertArrayEquals(data, out.toByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteArrays() throws IOException {
|
||||
byte[] data = createRandomData();
|
||||
BlockSource source = new ByteArrayBlockSource(data, MAX_BLOCK_LENGTH);
|
||||
InputStream in = new BlockSourceInputStream(MAX_BLOCK_LENGTH, executor,
|
||||
source, messageId);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[READ_BUFFER_BYTES];
|
||||
int dataOffset = 0;
|
||||
while (dataOffset < data.length) {
|
||||
int length = Math.min(random.nextInt(buf.length) + 1,
|
||||
data.length - dataOffset);
|
||||
int bufOffset = 0;
|
||||
if (length < buf.length)
|
||||
bufOffset = random.nextInt(buf.length - length);
|
||||
int read = in.read(buf, bufOffset, length);
|
||||
assertNotEquals(-1, read);
|
||||
out.write(buf, bufOffset, read);
|
||||
dataOffset += read;
|
||||
}
|
||||
assertEquals(-1, in.read(buf, 0, 0));
|
||||
in.close();
|
||||
out.flush();
|
||||
out.close();
|
||||
assertArrayEquals(data, out.toByteArray());
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDbExceptionFromGetBlockCountIsRethrown() throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(blockSource).getBlockCount(messageId);
|
||||
will(throwException(new DbException()));
|
||||
}});
|
||||
|
||||
InputStream in = new BlockSourceInputStream(MAX_BLOCK_LENGTH, executor,
|
||||
blockSource, messageId);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
in.read();
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testDbExceptionFromGetBlockIsRethrown() throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(blockSource).getBlockCount(messageId);
|
||||
will(returnValue(1));
|
||||
oneOf(blockSource).getBlock(messageId, 0);
|
||||
will(throwException(new DbException()));
|
||||
}});
|
||||
|
||||
InputStream in = new BlockSourceInputStream(MAX_BLOCK_LENGTH, executor,
|
||||
blockSource, messageId);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
in.read();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFullBlockAtEndOfMessage() throws Exception {
|
||||
testReadBlockAtEndOfMessage(MAX_BLOCK_LENGTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPartialBlockAtEndOfMessage() throws Exception {
|
||||
testReadBlockAtEndOfMessage(MAX_BLOCK_LENGTH - 1);
|
||||
}
|
||||
|
||||
private void testReadBlockAtEndOfMessage(int blockLength) throws Exception {
|
||||
byte[] block = new byte[blockLength];
|
||||
random.nextBytes(block);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(blockSource).getBlockCount(messageId);
|
||||
will(returnValue(1));
|
||||
oneOf(blockSource).getBlock(messageId, 0);
|
||||
will(returnValue(block));
|
||||
}});
|
||||
|
||||
InputStream in = new BlockSourceInputStream(MAX_BLOCK_LENGTH, executor,
|
||||
blockSource, messageId);
|
||||
byte[] buf = new byte[MAX_BLOCK_LENGTH * 2];
|
||||
assertEquals(block.length, in.read(buf, 0, buf.length));
|
||||
assertArrayEquals(block, copyOfRange(buf, 0, block.length));
|
||||
assertEquals(-1, in.read(buf, 0, buf.length));
|
||||
}
|
||||
|
||||
private byte[] createRandomData() {
|
||||
int length = random.nextInt(MAX_DATA_BYTES) + 1;
|
||||
byte[] data = new byte[length];
|
||||
random.nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.io;
|
||||
|
||||
import org.briarproject.bramble.api.io.BlockSource;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import static java.lang.System.arraycopy;
|
||||
|
||||
class ByteArrayBlockSource implements BlockSource {
|
||||
|
||||
private final byte[] data;
|
||||
private final int blockBytes;
|
||||
|
||||
ByteArrayBlockSource(byte[] data, int blockBytes) {
|
||||
this.data = data;
|
||||
this.blockBytes = blockBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBlockCount(MessageId m) {
|
||||
return (data.length + blockBytes - 1) / blockBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBlock(MessageId m, int blockNumber) {
|
||||
int offset = blockNumber * blockBytes;
|
||||
if (offset >= data.length) throw new IllegalArgumentException();
|
||||
int length = Math.min(blockBytes, data.length - offset);
|
||||
byte[] block = new byte[length];
|
||||
arraycopy(data, offset, block, 0, length);
|
||||
return block;
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,7 @@ dependencies {
|
||||
exclude group: 'com.android.support'
|
||||
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
|
||||
}
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.4' // later versions already use androidx
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
@@ -113,6 +113,15 @@
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.conversation.ImageActivity"
|
||||
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
|
||||
android:theme="@style/BriarTheme.Transparent.NoActionBar">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
|
||||
android:label="@string/groups_create_group_title"
|
||||
|
||||
@@ -15,10 +15,11 @@ import org.briarproject.briar.android.blog.ReblogFragment;
|
||||
import org.briarproject.briar.android.blog.RssFeedImportActivity;
|
||||
import org.briarproject.briar.android.blog.RssFeedManageActivity;
|
||||
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
|
||||
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
||||
import org.briarproject.briar.android.contact.ContactListFragment;
|
||||
import org.briarproject.briar.android.contact.ContactModule;
|
||||
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||
import org.briarproject.briar.android.conversation.ImageActivity;
|
||||
import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||
@@ -110,6 +111,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(ConversationActivity activity);
|
||||
|
||||
void inject(ImageActivity activity);
|
||||
|
||||
void inject(ForumInvitationActivity activity);
|
||||
|
||||
void inject(BlogInvitationActivity activity);
|
||||
|
||||
@@ -2,12 +2,11 @@ package org.briarproject.briar.android.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.transition.Slide;
|
||||
import android.transition.Transition;
|
||||
import android.view.Gravity;
|
||||
import android.view.Window;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
@@ -33,6 +32,7 @@ import static android.os.Build.VERSION.SDK_INT;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_UNLOCK;
|
||||
import static org.briarproject.briar.android.util.UiUtils.excludeSystemUi;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
|
||||
|
||||
@@ -111,21 +111,28 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
lockManager.onActivityStop();
|
||||
}
|
||||
|
||||
public void setSceneTransitionAnimation() {
|
||||
if (SDK_INT < 21) return;
|
||||
/**
|
||||
* Sets the transition animations.
|
||||
* @param enterTransition used to move views into initial positions
|
||||
* @param exitTransition used to move views out when starting a <b>new</b> activity.
|
||||
* @param returnTransition used when window is closing, because the activity is finishing.
|
||||
*/
|
||||
@RequiresApi(api = 21)
|
||||
public void setSceneTransitionAnimation(
|
||||
@Nullable Transition enterTransition,
|
||||
@Nullable Transition exitTransition,
|
||||
@Nullable Transition returnTransition) {
|
||||
// workaround for #1007
|
||||
if (isSamsung7()) {
|
||||
return;
|
||||
}
|
||||
Transition slide = new Slide(Gravity.RIGHT);
|
||||
slide.excludeTarget(android.R.id.statusBarBackground, true);
|
||||
slide.excludeTarget(android.R.id.navigationBarBackground, true);
|
||||
if (enterTransition != null) excludeSystemUi(enterTransition);
|
||||
if (exitTransition != null) excludeSystemUi(exitTransition);
|
||||
if (returnTransition != null) excludeSystemUi(returnTransition);
|
||||
Window window = getWindow();
|
||||
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
|
||||
window.setExitTransition(slide);
|
||||
window.setEnterTransition(slide);
|
||||
window.setTransitionBackgroundFadeDuration(getResources()
|
||||
.getInteger(android.R.integer.config_longAnimTime));
|
||||
window.setEnterTransition(enterTransition);
|
||||
window.setExitTransition(exitTransition);
|
||||
window.setReturnTransition(returnTransition);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,6 @@ public class ReblogActivity extends BriarActivity implements
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
setSceneTransitionAnimation();
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
@@ -7,13 +10,26 @@ import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class AttachmentItem {
|
||||
public class AttachmentItem implements Parcelable {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final int width, height;
|
||||
private final int thumbnailWidth, thumbnailHeight;
|
||||
private final boolean hasError;
|
||||
|
||||
public static final Creator<AttachmentItem> CREATOR =
|
||||
new Creator<AttachmentItem>() {
|
||||
@Override
|
||||
public AttachmentItem createFromParcel(Parcel in) {
|
||||
return new AttachmentItem(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentItem[] newArray(int size) {
|
||||
return new AttachmentItem[size];
|
||||
}
|
||||
};
|
||||
|
||||
AttachmentItem(MessageId messageId, int width, int height,
|
||||
int thumbnailWidth, int thumbnailHeight, boolean hasError) {
|
||||
this.messageId = messageId;
|
||||
@@ -24,6 +40,17 @@ public class AttachmentItem {
|
||||
this.hasError = hasError;
|
||||
}
|
||||
|
||||
protected AttachmentItem(Parcel in) {
|
||||
byte[] messageIdByte = new byte[MessageId.LENGTH];
|
||||
in.readByteArray(messageIdByte);
|
||||
messageId = new MessageId(messageIdByte);
|
||||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
thumbnailWidth = in.readInt();
|
||||
thumbnailHeight = in.readInt();
|
||||
hasError = in.readByte() != 0;
|
||||
}
|
||||
|
||||
public MessageId getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
@@ -48,4 +75,24 @@ public class AttachmentItem {
|
||||
return hasError;
|
||||
}
|
||||
|
||||
// TODO use counter instead, because in theory one attachment can appear in more than one messages
|
||||
String getTransitionName() {
|
||||
return String.valueOf(messageId.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeByteArray(messageId.getBytes());
|
||||
dest.writeInt(width);
|
||||
dest.writeInt(height);
|
||||
dest.writeInt(thumbnailWidth);
|
||||
dest.writeInt(thumbnailHeight);
|
||||
dest.writeByte((byte) (hasError ? 1 : 0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,11 +9,15 @@ import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.ActionMenuView;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.transition.Slide;
|
||||
import android.transition.Transition;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -96,8 +100,11 @@ import im.delight.android.identicons.IdenticonDrawable;
|
||||
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
|
||||
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
|
||||
import static android.support.v4.view.ViewCompat.setTransitionName;
|
||||
import static android.support.v7.util.SortedList.INVALID_POSITION;
|
||||
import static android.view.Gravity.END;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.sort;
|
||||
@@ -108,6 +115,9 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
|
||||
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
|
||||
@@ -186,7 +196,10 @@ public class ConversationActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
setSceneTransitionAnimation();
|
||||
if (SDK_INT >= 21) {
|
||||
Transition slide = new Slide(END);
|
||||
setSceneTransitionAnimation(slide, null, slide);
|
||||
}
|
||||
super.onCreate(state);
|
||||
|
||||
Intent i = getIntent();
|
||||
@@ -802,6 +815,31 @@ public class ConversationActivity extends BriarActivity
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachmentClicked(View view,
|
||||
ConversationMessageItem messageItem, AttachmentItem item) {
|
||||
String name;
|
||||
if (messageItem.isIncoming()) {
|
||||
// must be available when items are being displayed
|
||||
name = viewModel.getContactDisplayName().getValue();
|
||||
} else {
|
||||
name = getString(R.string.you);
|
||||
}
|
||||
Intent i = new Intent(this, ImageActivity.class);
|
||||
i.putExtra(ATTACHMENT, item);
|
||||
i.putExtra(NAME, name);
|
||||
i.putExtra(DATE, messageItem.getTime());
|
||||
if (SDK_INT >= 23) {
|
||||
String transitionName = item.getTransitionName();
|
||||
ActivityOptionsCompat options =
|
||||
makeSceneTransitionAnimation(this, view, transitionName);
|
||||
ActivityCompat.startActivity(this, i, options.toBundle());
|
||||
} else {
|
||||
// work-around for android bug #224270
|
||||
startActivity(i);
|
||||
}
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void respondToIntroductionRequest(SessionId sessionId,
|
||||
boolean accept, long time) throws DbException {
|
||||
@@ -845,4 +883,5 @@ public class ConversationActivity extends BriarActivity
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.support.annotation.UiThread;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@@ -14,4 +15,7 @@ interface ConversationListener {
|
||||
|
||||
void openRequestedShareable(ConversationRequestItem item);
|
||||
|
||||
void onAttachmentClicked(View view, ConversationMessageItem messageItem,
|
||||
AttachmentItem attachmentItem);
|
||||
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
|
||||
if (item.getAttachments().isEmpty()) {
|
||||
bindTextItem();
|
||||
} else {
|
||||
bindImageItem(item);
|
||||
bindImageItem(item, listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
|
||||
textConstraints.applyTo(layout);
|
||||
}
|
||||
|
||||
private void bindImageItem(ConversationMessageItem item) {
|
||||
private void bindImageItem(ConversationMessageItem item,
|
||||
ConversationListener listener) {
|
||||
// TODO show more than just the first image
|
||||
AttachmentItem attachment = item.getAttachments().get(0);
|
||||
|
||||
@@ -127,17 +128,18 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
|
||||
clearImage();
|
||||
imageView.setImageResource(ERROR_RES);
|
||||
} else {
|
||||
loadImage(item, attachment);
|
||||
loadImage(item, attachment, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearImage() {
|
||||
GlideApp.with(imageView)
|
||||
.clear(imageView);
|
||||
imageView.setOnClickListener(null);
|
||||
}
|
||||
|
||||
private void loadImage(ConversationMessageItem item,
|
||||
AttachmentItem attachment) {
|
||||
AttachmentItem attachment, ConversationListener listener) {
|
||||
boolean leftCornerSmall =
|
||||
(isIncoming() && !isRtl) || (!isIncoming() && isRtl);
|
||||
boolean bottomRound = item.getText() == null;
|
||||
@@ -152,6 +154,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
|
||||
.transition(withCrossFade())
|
||||
.into(imageView)
|
||||
.waitForLayout();
|
||||
imageView.setOnClickListener(
|
||||
view -> listener.onAttachmentClicked(view, item, attachment));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.transition.Fade;
|
||||
import android.transition.Transition;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
import org.briarproject.briar.android.view.PullDownLayout;
|
||||
|
||||
import static android.graphics.Color.TRANSPARENT;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
|
||||
import static android.widget.ImageView.ScaleType.FIT_START;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute;
|
||||
|
||||
public class ImageActivity extends BriarActivity
|
||||
implements PullDownLayout.Callback {
|
||||
|
||||
final static String ATTACHMENT = "attachment";
|
||||
final static String NAME = "name";
|
||||
final static String DATE = "date";
|
||||
|
||||
private PullDownLayout layout;
|
||||
private AppBarLayout appBarLayout;
|
||||
private PhotoView photoView;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
// Transitions
|
||||
supportPostponeEnterTransition();
|
||||
Window window = getWindow();
|
||||
if (SDK_INT >= 21) {
|
||||
Transition transition = new Fade();
|
||||
setSceneTransitionAnimation(transition, null, transition);
|
||||
}
|
||||
|
||||
// inflate layout
|
||||
setContentView(R.layout.activity_image);
|
||||
layout = findViewById(R.id.layout);
|
||||
layout.getBackground().setAlpha(255);
|
||||
layout.setCallback(this);
|
||||
|
||||
// Status Bar
|
||||
if (SDK_INT >= 21) {
|
||||
window.setStatusBarColor(TRANSPARENT);
|
||||
} else if (SDK_INT >= 19) {
|
||||
// we can't make the status bar transparent, but translucent
|
||||
window.addFlags(FLAG_TRANSLUCENT_STATUS);
|
||||
}
|
||||
|
||||
// Toolbar
|
||||
appBarLayout = findViewById(R.id.appBarLayout);
|
||||
Toolbar toolbar = requireNonNull(setUpCustomToolbar(true));
|
||||
TextView contactName = toolbar.findViewById(R.id.contactName);
|
||||
TextView dateView = toolbar.findViewById(R.id.dateView);
|
||||
|
||||
// Intent Extras
|
||||
AttachmentItem attachment = getIntent().getParcelableExtra(ATTACHMENT);
|
||||
String name = getIntent().getStringExtra(NAME);
|
||||
long time = getIntent().getLongExtra(DATE, 0);
|
||||
String date = formatDateAbsolute(this, time);
|
||||
contactName.setText(name);
|
||||
dateView.setText(date);
|
||||
|
||||
// Image View
|
||||
photoView = findViewById(R.id.photoView);
|
||||
if (SDK_INT >= 16) {
|
||||
photoView.setOnClickListener(view -> toggleSystemUi());
|
||||
window.getDecorView().setSystemUiVisibility(
|
||||
SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
}
|
||||
|
||||
// Request Listener
|
||||
RequestListener<Drawable> listener = new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e,
|
||||
Object model, Target<Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
supportStartPostponedEnterTransition();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model,
|
||||
Target<Drawable> target, DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
|
||||
// set transition name only when not animatable,
|
||||
// because the animation won't start otherwise
|
||||
photoView.setTransitionName(
|
||||
attachment.getTransitionName());
|
||||
}
|
||||
// Move image to the top if overlapping toolbar
|
||||
if (isOverlappingToolbar(resource)) {
|
||||
photoView.setScaleType(FIT_START);
|
||||
}
|
||||
supportStartPostponedEnterTransition();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load Image
|
||||
GlideApp.with(this)
|
||||
.load(attachment)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(R.drawable.ic_image_broken)
|
||||
.dontTransform()
|
||||
.addListener(listener)
|
||||
.into(photoView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPullStart() {
|
||||
appBarLayout.animate()
|
||||
.alpha(0f)
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPull(float progress) {
|
||||
layout.getBackground().setAlpha(Math.round((1 - progress) * 255));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPullCancel() {
|
||||
appBarLayout.animate()
|
||||
.alpha(1f)
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPullComplete() {
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void toggleSystemUi() {
|
||||
View decorView = getWindow().getDecorView();
|
||||
if (appBarLayout.getVisibility() == VISIBLE) {
|
||||
hideSystemUi(decorView);
|
||||
} else {
|
||||
showSystemUi(decorView);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void hideSystemUi(View decorView) {
|
||||
decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| SYSTEM_UI_FLAG_FULLSCREEN
|
||||
);
|
||||
appBarLayout.animate()
|
||||
.translationYBy(-1 * appBarLayout.getHeight())
|
||||
.alpha(0f)
|
||||
.withEndAction(() -> appBarLayout.setVisibility(GONE))
|
||||
.start();
|
||||
}
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void showSystemUi(View decorView) {
|
||||
decorView.setSystemUiVisibility(
|
||||
SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
);
|
||||
appBarLayout.animate()
|
||||
.translationYBy(appBarLayout.getHeight())
|
||||
.alpha(1f)
|
||||
.withStartAction(() -> appBarLayout.setVisibility(VISIBLE))
|
||||
.start();
|
||||
}
|
||||
|
||||
private boolean isOverlappingToolbar(Drawable drawable) {
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
float widthPercentage = photoView.getWidth() / (float) width;
|
||||
float heightPercentage = photoView.getHeight() / (float) height;
|
||||
float scaleFactor = Math.min(widthPercentage, heightPercentage);
|
||||
int realWidth = (int) (width * scaleFactor);
|
||||
int realHeight = (int) (height * scaleFactor);
|
||||
// return if photo doesn't use the full width,
|
||||
// because it will be moved to the right otherwise
|
||||
if (realWidth < photoView.getWidth()) return false;
|
||||
int drawableTop = (photoView.getHeight() - realHeight) / 2;
|
||||
return drawableTop < appBarLayout.getBottom() &&
|
||||
drawableTop != appBarLayout.getTop();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.ColorRes;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@@ -31,6 +32,7 @@ import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.transition.Transition;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
@@ -60,12 +62,16 @@ import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_NO;
|
||||
import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_YES;
|
||||
import static android.support.v7.app.AppCompatDelegate.setDefaultNightMode;
|
||||
import static android.text.format.DateUtils.DAY_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_ALL;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_TIME;
|
||||
import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
|
||||
import static android.text.format.DateUtils.FORMAT_SHOW_TIME;
|
||||
import static android.text.format.DateUtils.FORMAT_SHOW_YEAR;
|
||||
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
|
||||
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
|
||||
import static android.view.KeyEvent.ACTION_DOWN;
|
||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
import static android.view.inputmethod.EditorInfo.IME_NULL;
|
||||
@@ -117,6 +123,13 @@ public class UiUtils {
|
||||
MIN_DATE_RESOLUTION, flags).toString();
|
||||
}
|
||||
|
||||
public static String formatDateAbsolute(Context ctx, long time) {
|
||||
int flags = FORMAT_SHOW_TIME | FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL;
|
||||
long diff = System.currentTimeMillis() - time;
|
||||
if (diff >= YEAR_IN_MILLIS) flags |= FORMAT_SHOW_YEAR;
|
||||
return DateUtils.formatDateTime(ctx, time, flags);
|
||||
}
|
||||
|
||||
public static int getDaysUntilExpiry() {
|
||||
long now = System.currentTimeMillis();
|
||||
long daysBeforeExpiry = (EXPIRY_DATE - now) / 1000 / 60 / 60 / 24;
|
||||
@@ -318,6 +331,12 @@ public class UiUtils {
|
||||
keyEvent.getKeyCode() == KEYCODE_ENTER;
|
||||
}
|
||||
|
||||
@RequiresApi(api = 21)
|
||||
public static void excludeSystemUi(Transition transition) {
|
||||
transition.excludeTarget(android.R.id.statusBarBackground, true);
|
||||
transition.excludeTarget(android.R.id.navigationBarBackground, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the given {@link LiveData} until the first change.
|
||||
* If the LiveData's value is available, the {@link Observer} will be
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2015 XiNGRZ <xxx@oxo.ooo>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.briarproject.briar.android.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.widget.ViewDragHelper;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public class PullDownLayout extends FrameLayout {
|
||||
|
||||
private final ViewDragHelper dragger;
|
||||
|
||||
private final int minimumFlingVelocity;
|
||||
|
||||
@Nullable
|
||||
private Callback callback;
|
||||
|
||||
public PullDownLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public PullDownLayout(Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public PullDownLayout(Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
dragger = ViewDragHelper.create(this, 1f / 8f, new ViewDragCallback());
|
||||
minimumFlingVelocity =
|
||||
ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
|
||||
}
|
||||
|
||||
public void setCallback(@Nullable Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
return dragger.shouldInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
dragger.processTouchEvent(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeScroll() {
|
||||
if (dragger.continueSettling(true)) {
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
void onPullStart();
|
||||
|
||||
void onPull(float progress);
|
||||
|
||||
void onPullCancel();
|
||||
|
||||
void onPullComplete();
|
||||
|
||||
}
|
||||
|
||||
private class ViewDragCallback extends ViewDragHelper.Callback {
|
||||
|
||||
@Override
|
||||
public boolean tryCaptureView(View child, int pointerId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int clampViewPositionHorizontal(View child, int left, int dx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int clampViewPositionVertical(View child, int top, int dy) {
|
||||
return Math.max(0, top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewHorizontalDragRange(View child) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewVerticalDragRange(View child) {
|
||||
return getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCaptured(View capturedChild, int activePointerId) {
|
||||
if (callback != null) {
|
||||
callback.onPullStart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewPositionChanged(View changedView, int left, int top,
|
||||
int dx, int dy) {
|
||||
if (callback != null) {
|
||||
callback.onPull((float) top / (float) getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewReleased(View releasedChild, float xvel, float yvel) {
|
||||
int slop = yvel > minimumFlingVelocity ? getHeight() / 6 :
|
||||
getHeight() / 3;
|
||||
if (releasedChild.getTop() > slop) {
|
||||
if (callback != null) {
|
||||
callback.onPullComplete();
|
||||
}
|
||||
} else {
|
||||
if (callback != null) {
|
||||
callback.onPullCancel();
|
||||
}
|
||||
|
||||
dragger.settleCapturedViewAt(0, 0);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
60
briar-android/src/main/res/layout/activity_image.xml
Normal file
60
briar-android/src/main/res/layout/activity_image.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.briarproject.briar.android.view.PullDownLayout
|
||||
android:id="@+id/layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/briar_black"
|
||||
tools:context=".android.conversation.ImageActivity">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/photoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:srcCompat="@tools:sample/backgrounds/scenic"/>
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/msg_status_bubble_background">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/BriarToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/msg_status_bubble_background"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/contactName"
|
||||
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/action_bar_text"
|
||||
tools:text="Contact Name of someone who chose a long name"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/action_bar_text"
|
||||
tools:text="date"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v7.widget.Toolbar>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
</org.briarproject.briar.android.view.PullDownLayout>
|
||||
@@ -134,6 +134,8 @@
|
||||
<string name="dialog_title_delete_contact">Confirm Contact Deletion</string>
|
||||
<string name="dialog_message_delete_contact">Are you sure that you want to remove this contact and all messages exchanged with this contact?</string>
|
||||
<string name="contact_deleted_toast">Contact deleted</string>
|
||||
<!-- This is shown in the action bar when opening an image in fullscreen that the user sent -->
|
||||
<string name="you">You</string>
|
||||
|
||||
<!-- Adding Contacts -->
|
||||
<string name="add_contact_title">Add a Contact</string>
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
<item name="toolbarStyle">@style/BriarToolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="BriarTheme.Transparent.NoActionBar" parent="BriarTheme.NoActionBar">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowActionBarOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="ActivityAnimation" parent="@android:style/Animation.Activity">
|
||||
<item name="android:activityOpenEnterAnimation">@anim/screen_new_in</item>
|
||||
<item name="android:activityOpenExitAnimation">@anim/screen_old_out</item>
|
||||
|
||||
@@ -89,6 +89,7 @@ dependencyVerification {
|
||||
'com.github.bumptech.glide:compiler:4.8.0:compiler-4.8.0.jar:1fa93dd0cf7ef0b8b98a59a67a1ee84915416c2d677d83a771ea3e32ad15e6bf',
|
||||
'com.github.bumptech.glide:gifdecoder:4.8.0:gifdecoder-4.8.0.aar:b00c5454a023a9488ea49603930d9c25e09192e5ceaadf64977aa52946b3c1b4',
|
||||
'com.github.bumptech.glide:glide:4.8.0:glide-4.8.0.aar:5ddf08b12cc43332e812988f16c2c39e7fce49d1c4d94b7948dcde7f00bf49d6',
|
||||
'com.github.chrisbanes:PhotoView:2.1.4:PhotoView-2.1.4.aar:04cb397fcb3df0757c8aed6927ebdd247930b5c78ee9acc59cd07dccdaaf3460',
|
||||
'com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:2.0:accessibility-test-framework-2.0.jar:cdf16ef8f5b8023d003ce3cc1b0d51bda737762e2dab2fedf43d1c4292353f7f',
|
||||
'com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:2.1:accessibility-test-framework-2.1.jar:7b0aa6ed7553597ce0610684a9f7eca8021eee218f2e2f427c04a7fbf5f920bd',
|
||||
'com.google.code.findbugs:jsr305:1.3.9:jsr305-1.3.9.jar:905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed',
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.briarproject.briar.api.messaging.PrivateMessage;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public abstract class ThreadedMessage extends PrivateMessage {
|
||||
@@ -19,7 +21,7 @@ public abstract class ThreadedMessage extends PrivateMessage {
|
||||
|
||||
public ThreadedMessage(Message message, @Nullable MessageId parent,
|
||||
Author author) {
|
||||
super(message);
|
||||
super(message, emptyList());
|
||||
this.parent = parent;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.briarproject.briar.api.messaging;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@@ -10,13 +12,18 @@ import javax.annotation.concurrent.Immutable;
|
||||
public class PrivateMessage {
|
||||
|
||||
private final Message message;
|
||||
private final List<AttachmentHeader> attachments;
|
||||
|
||||
public PrivateMessage(Message message) {
|
||||
public PrivateMessage(Message message, List<AttachmentHeader> attachments) {
|
||||
this.message = message;
|
||||
this.attachments = attachments;
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public List<AttachmentHeader> getAttachmentHeaders() {
|
||||
return attachments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,16 @@ import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataParser;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.io.MessageInputStreamFactory;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Client;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageFactory;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.MessageStatus;
|
||||
import org.briarproject.bramble.api.versioning.ClientVersioningManager;
|
||||
@@ -32,16 +35,17 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent;
|
||||
import org.briarproject.briar.client.ConversationClientImpl;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
|
||||
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
|
||||
|
||||
@Immutable
|
||||
@@ -51,15 +55,21 @@ class MessagingManagerImpl extends ConversationClientImpl
|
||||
|
||||
private final ClientVersioningManager clientVersioningManager;
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final MessageFactory messageFactory;
|
||||
private final MessageInputStreamFactory messageInputStreamFactory;
|
||||
|
||||
@Inject
|
||||
MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
|
||||
ClientVersioningManager clientVersioningManager,
|
||||
MetadataParser metadataParser, MessageTracker messageTracker,
|
||||
ContactGroupFactory contactGroupFactory) {
|
||||
ContactGroupFactory contactGroupFactory,
|
||||
MessageFactory messageFactory,
|
||||
MessageInputStreamFactory messageInputStreamFactory) {
|
||||
super(db, clientHelper, metadataParser, messageTracker);
|
||||
this.clientVersioningManager = clientVersioningManager;
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.messageFactory = messageFactory;
|
||||
this.messageInputStreamFactory = messageInputStreamFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,9 +152,11 @@ class MessagingManagerImpl extends ConversationClientImpl
|
||||
meta.put("read", true);
|
||||
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
|
||||
messageTracker.trackOutgoingMessage(txn, m.getMessage());
|
||||
for (AttachmentHeader h : m.getAttachmentHeaders())
|
||||
db.setMessageShared(txn, h.getMessageId());
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
@@ -152,11 +164,16 @@ class MessagingManagerImpl extends ConversationClientImpl
|
||||
|
||||
@Override
|
||||
public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
|
||||
String contentType, ByteBuffer data) {
|
||||
// TODO add real implementation
|
||||
byte[] b = new byte[MessageId.LENGTH];
|
||||
new Random().nextBytes(b);
|
||||
return new AttachmentHeader(new MessageId(b), "image/png");
|
||||
String contentType, ByteBuffer data) throws DbException {
|
||||
// TODO: Remove this restriction when large messages are supported
|
||||
byte[] body = data.array();
|
||||
if (body.length > MAX_MESSAGE_LENGTH) throw new DbException();
|
||||
// TODO: Store message type and content type
|
||||
Message m = messageFactory.createMessage(groupId, timestamp, body);
|
||||
Metadata meta = new Metadata();
|
||||
// Attachment will be shared when private message is added
|
||||
db.transaction(false, txn -> db.addLocalMessage(txn, m, meta, false));
|
||||
return new AttachmentHeader(m.getId(), contentType);
|
||||
}
|
||||
|
||||
private ContactId getContactId(Transaction txn, GroupId g)
|
||||
@@ -236,8 +253,9 @@ class MessagingManagerImpl extends ConversationClientImpl
|
||||
|
||||
@Override
|
||||
public Attachment getAttachment(MessageId m) {
|
||||
// TODO add real implementation
|
||||
throw new IllegalStateException("Not yet implemented");
|
||||
InputStream in = messageInputStreamFactory.getMessageInputStream(m);
|
||||
// TODO: Read message type and content type
|
||||
return new Attachment(in);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,6 +39,6 @@ class PrivateMessageFactoryImpl implements PrivateMessageFactory {
|
||||
// Serialise the message
|
||||
BdfList message = BdfList.of(text);
|
||||
Message m = clientHelper.createMessage(groupId, timestamp, message);
|
||||
return new PrivateMessage(m);
|
||||
return new PrivateMessage(m, attachments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.briarproject.bramble.data.DataModule;
|
||||
import org.briarproject.bramble.db.DatabaseModule;
|
||||
import org.briarproject.bramble.event.EventModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.io.IoModule;
|
||||
import org.briarproject.bramble.lifecycle.LifecycleModule;
|
||||
import org.briarproject.bramble.properties.PropertiesModule;
|
||||
import org.briarproject.bramble.record.RecordModule;
|
||||
@@ -50,6 +51,7 @@ import dagger.Component;
|
||||
GroupInvitationModule.class,
|
||||
IdentityModule.class,
|
||||
IntroductionModule.class,
|
||||
IoModule.class,
|
||||
LifecycleModule.class,
|
||||
MessagingModule.class,
|
||||
PrivateGroupModule.class,
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.briarproject.bramble.data.DataModule;
|
||||
import org.briarproject.bramble.db.DatabaseModule;
|
||||
import org.briarproject.bramble.event.EventModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.io.IoModule;
|
||||
import org.briarproject.bramble.sync.SyncModule;
|
||||
import org.briarproject.bramble.sync.validation.ValidationModule;
|
||||
import org.briarproject.bramble.system.SystemModule;
|
||||
@@ -40,6 +41,7 @@ import dagger.Component;
|
||||
EventModule.class,
|
||||
ForumModule.class,
|
||||
IdentityModule.class,
|
||||
IoModule.class,
|
||||
MessagingModule.class,
|
||||
SyncModule.class,
|
||||
SystemModule.class,
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.briarproject.bramble.data.DataModule;
|
||||
import org.briarproject.bramble.db.DatabaseModule;
|
||||
import org.briarproject.bramble.event.EventModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.io.IoModule;
|
||||
import org.briarproject.bramble.lifecycle.LifecycleModule;
|
||||
import org.briarproject.bramble.record.RecordModule;
|
||||
import org.briarproject.bramble.sync.SyncModule;
|
||||
@@ -48,6 +49,7 @@ import dagger.Component;
|
||||
DatabaseModule.class,
|
||||
EventModule.class,
|
||||
IdentityModule.class,
|
||||
IoModule.class,
|
||||
LifecycleModule.class,
|
||||
MessagingModule.class,
|
||||
RecordModule.class,
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.briarproject.bramble.data.DataModule;
|
||||
import org.briarproject.bramble.db.DatabaseModule;
|
||||
import org.briarproject.bramble.event.EventModule;
|
||||
import org.briarproject.bramble.identity.IdentityModule;
|
||||
import org.briarproject.bramble.io.IoModule;
|
||||
import org.briarproject.bramble.lifecycle.LifecycleModule;
|
||||
import org.briarproject.bramble.properties.PropertiesModule;
|
||||
import org.briarproject.bramble.record.RecordModule;
|
||||
@@ -68,6 +69,7 @@ import dagger.Component;
|
||||
GroupInvitationModule.class,
|
||||
IdentityModule.class,
|
||||
IntroductionModule.class,
|
||||
IoModule.class,
|
||||
LifecycleModule.class,
|
||||
MessagingModule.class,
|
||||
PrivateGroupModule.class,
|
||||
|
||||
@@ -62,7 +62,7 @@ internal class MessagingControllerImplTest : ControllerTest() {
|
||||
emptyList()
|
||||
)
|
||||
private val sessionId = SessionId(getRandomId())
|
||||
private val privateMessage = PrivateMessage(message)
|
||||
private val privateMessage = PrivateMessage(message, emptyList())
|
||||
|
||||
@Test
|
||||
fun list() {
|
||||
|
||||
@@ -5,6 +5,7 @@ allprojects {
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
google()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
afterEvaluate {
|
||||
tasks.withType(Test) {
|
||||
|
||||
Reference in New Issue
Block a user