Compare commits

...

50 Commits

Author SHA1 Message Date
akwizgran
0e51ddb767 Bumped expiry date to 1 May 2017. 2017-02-27 09:29:24 +00:00
Torsten Grote
36f02b36d9 Update expiry and translations 2017-02-01 11:17:53 -02:00
akwizgran
2fb11fba2a Merge branch '877-save-invitation-outcome-to-invitation-message-and-make-available-to-ui' into 'master'
Store invitation outcome in metadata and make it available to the UI

This MR is based on !479 and should only be merged after that one has been merged as well.

It stores the invitation outcome in the message metadata  and includes it in the `canBeOpened()` calculation for private groups and sharables.

Closes #877

See merge request !480
2017-01-06 16:06:19 +00:00
akwizgran
1d11857e75 Merge branch '476-blog-sharing-protocol-modifies-state-external-to-session' into 'master'
Migrate blog sharing to new sharing client infrastructure

This MR contains a second small commit that fixes #816 and adds a test for it.

Closes #476, #701

See merge request !479
2017-01-06 16:04:17 +00:00
Torsten Grote
04508a7431 Store invitation outcome in metadata
and include in canBeOpened calculation for private groups and sharables.
2017-01-06 13:29:21 -02:00
Torsten Grote
5653c6d650 Address review comments 2017-01-06 13:25:13 -02:00
Torsten Grote
ab100ad19b Properly remove the blog when deleting a contact and inform all peers
Fixes #816
2017-01-06 13:01:37 -02:00
Torsten Grote
c13eafef14 Migrate blog sharing to new sharing client infrastructure 2017-01-06 13:01:34 -02:00
akwizgran
d5443e9651 Merge branch '889-select-navdrawer-item-when-coming-from-notification' into 'master'
Check the blog item in NavDrawer when opening a blog via notification

There are three scenarios where the selected item in the NavDrawer changes:
1. The user selects an item -> the item is checked automatically.
2. The user pressed back -> already handled in onBackPressed (needs to be extended with #606)
3. The user touched a notification -> handled by this commit.
   
Signed-off-by: goapunk <noobie@goapunks.net>

Closes #889

See merge request !481
2017-01-06 14:59:21 +00:00
akwizgran
d5f9a3280d Merge branch 'briar-recycler-view-log' into 'master'
Stop periodic list update only once

When testing the forum unread code, I noticed the recycler view is detaching twice. Once because the stop method is called and once because the view detaches from the window. Wouldn't it be safe to null the refresher when the updates are stopped?

See merge request !478
2017-01-06 14:15:48 +00:00
goapunk
09b2ecaecf Check the corresponding NavDrawer item when coming from a notification
Signed-off-by: goapunk <noobie@goapunks.net>
2017-01-06 15:12:40 +01:00
Torsten Grote
dc6a6f27ab Fix MessageTreeImplTest 2017-01-04 16:27:49 -02:00
Torsten Grote
8d9ddeeeee Stop periodic list update only once 2017-01-04 11:15:19 -02:00
Torsten Grote
baed2b8483 Merge branch '879-remove-thread-collapsing-unread-count' into 'master'
Remove code for collapsing threads and for reply count

Besides removing lots of code, this MR also improves the encapsulation between adapter and view holders.

Closes #478, #502, #526, #682,  #683,  #835,  #836

See merge request !477
2017-01-04 13:00:16 +00:00
Torsten Grote
b3d3230549 Remove code for collapsing threads and for reply count 2017-01-04 10:58:31 -02:00
akwizgran
deb8787668 Merge branch '879-threaded-unread-messages' into 'master'
Threaded Unread Handling

![unread](/uploads/457c3bc36c897d683ff8161b4f3a344c/unread.mp4)

This leaves in the collapsing code for now and just hides the UI element for collapsing. The code can be removed in a second pass to simplify the adapter.

Closes #879

See merge request !476
2017-01-04 11:12:24 +00:00
Torsten Grote
7034ea28f3 Merge branch '475-new-sharing-client' into 'master'
New Forum Sharing Client

This is very similar to how the private group invitations work and I am sure there's still some tiny bugs that I didn't catch.

All existing integration tests either pass or have been modified to pass.

Once this has been merged, the code should be usable for blog sharing as well.

Closes #475

See merge request !467
2017-01-03 19:30:48 +00:00
Torsten Grote
51b78cf9b1 Address review comments for new sharing client 2017-01-03 17:25:45 -02:00
Torsten Grote
b4c669243b Add UnreadMessageButton to threaded conversations 2017-01-03 13:25:31 -02:00
Torsten Grote
694e662028 New Forum Sharing Client 2017-01-03 11:23:02 -02:00
Torsten Grote
409e0fb5a5 ForumSharingValidator 2017-01-03 11:23:00 -02:00
Torsten Grote
279f4d668a Add new UnreadMessageButton class 2017-01-02 16:30:00 -02:00
akwizgran
d2608e28ac Merge branch '881-forumactivitytest-fails-due-to-custom-toolbar' into 'master'
Fix ForumActivityTest and get rid of redundant theme definition

Closes #881

See merge request !474
2017-01-02 14:22:27 +00:00
akwizgran
8cf02c5f0e Merge branch '851-refresher-memory-leak' into 'master'
Fix memory leaks caused by periodic view refreshing tasks

This branch implements @goapunk's suggested solution to #851. Credit goes to @ernir for finding the bug and the initial solution, and @goapunk for the improved solution - I just did a quick implementation so we can get this fixed as quickly as possible.

Closes #851

See merge request !473
2017-01-02 14:10:32 +00:00
Torsten Grote
c5df2100da Fix ForumActivityTest and get rid of redundant theme definition 2017-01-02 11:57:45 -02:00
akwizgran
a6999a8197 Merge branch '710-conversationactivity-uses-uninitialised-field-as-format-string-argument' into 'master'
Make sure contact name is initialized when needed

This uses a Listenable Future and unfortunately requires 4 basically identical methods to handle the incoming events. Any suggestions for improving that are welcome.

Closes #710

See merge request !472
2017-01-02 11:11:41 +00:00
akwizgran
da89f11419 Merge branch '882-feedmanagerimpl-logs-rss-feed-urls' into 'master'
Do not log information from RSS feeds

Closes #882

See merge request !475
2017-01-02 10:52:04 +00:00
Torsten Grote
a9663875f4 Update expiry date and translations 2016-12-30 12:13:43 -02:00
Torsten Grote
804966ede6 Do not log information from RSS feeds 2016-12-29 13:07:18 -02:00
Torsten Grote
f0f22b42e5 Make sure contact name is initialized when needed 2016-12-29 12:29:32 -02:00
akwizgran
59316ae3c4 Fix memory leaks caused by periodic view refreshing tasks. 2016-12-28 15:23:21 +00:00
Torsten Grote
460b524e4b Merge branch 'record-reading-tests' into 'master'
Unit tests for record readers

I thought we should have some tests for the new logic that skips unrecognised record types.

See merge request !471
2016-12-21 17:14:16 +00:00
akwizgran
48e949c9f8 Merge branch '742-use-unique-request-ids-across-the-app' into 'master'
Use unique request codes across the app

Closes #742

See merge request !470
2016-12-21 15:33:03 +00:00
Torsten Grote
924398c829 Use unique request codes across the app 2016-12-21 12:52:11 -02:00
akwizgran
8619b044ce Merge branch '469-handle-background-errors' into 'master'
Add a handleDbException() method to BaseActivity

This adds a `handleDbException()` method to BaseActivity and a corresponding method for fragments that calls through to the activity.
For now, the method just finishes the activity
and NavDrawerActivity overrides it to do nothing,
and all the error places marked with TODO that finish the activity call the method instead.

That gives us zero functional improvement over the status quo,
but it allows us to change the default behaviour easily,
and then we can start thinking about which cases should have non-default behaviour.

First part of #469

See merge request !469
2016-12-21 14:46:53 +00:00
akwizgran
3c3731a562 Merge branch '876-group-invitation-not-marked-unavailable' into 'master'
Mark invitation unavailable to answer when creator dissolved the group after the invitation.

Closes #876

See merge request !468
2016-12-21 14:43:38 +00:00
akwizgran
b54984b542 Unit tests for RecordReaderImpl. 2016-12-21 14:39:56 +00:00
akwizgran
2390f767f5 Unit tests for KeyAgreementTransport. 2016-12-21 14:08:21 +00:00
Torsten Grote
0a9840997f This adds a handleDbException() method to BaseActivity
and a corresponding method for fragments that calls through to the activity.
For now, the method just finishes the activity
and NavDrawerActivity overrides it to do nothing,
and all the error places marked with TODO that finish the activity call the method instead.

That gives us zero functional improvement over the status quo,
but it allows us to change the default behaviour easily,
and then we can start thinking about which cases should have non-default behaviour.
2016-12-21 12:06:20 -02:00
Torsten Grote
6a94785d9a Mark invitation unavailable to answer when creator dissolved the group
after the invitation.

Closes #876
2016-12-21 11:24:08 -02:00
akwizgran
79fc41477c Merge branch '628-bring-protocols-into-line-with-spec' into 'master'
Bring protocols in line with spec

Closes #628

See merge request !465
2016-12-21 12:52:43 +00:00
Torsten Grote
efb89adf41 Merge branch '793-show-open-button-after-accepting-invitations' into 'master'
Show open button in private conversation after accepting invitations

To keep the implementation simple, the Open button does appear where the Accept button had been previously.

In order to make the Open button functional, I had to make the `GroupId` of the invitation target available to the UI. Most code in this MR is due to that.

![device-2016-12-13-105228](/uploads/d0f27fc02f1411596458d9203a0810d2/device-2016-12-13-105228.png)

Closes #793

See merge request !457
2016-12-20 14:07:13 +00:00
Torsten Grote
c04580e321 Don't open unsubscribed shareables 2016-12-20 12:00:01 -02:00
Torsten Grote
2ef9b8f4b6 Show open button in private conversation after accepting invitations 2016-12-20 08:47:27 -02:00
akwizgran
d63d15329c Merge branch '814-enable-QrScanner-after-QrCode-was-created' into 'master'
Ignore QR code results until local QR code is created

* Make scanning only possible after we are "ready" (= our QrCode was created and set).

Signed-off-by: goapunk <noobie@goapunks.net>

Closes #814

See merge request !454
2016-12-19 19:28:14 +00:00
Torsten Grote
5345db0b6b Address review comments 2016-12-19 11:15:53 -02:00
Torsten Grote
501980d8fe Bring protocols in line with spec 2016-12-19 10:26:48 -02:00
akwizgran
cc5c000278 Merge branch '738-older-devices-show-overflow-icon-on-some-screens-but-not-others' into 'master'
Also show overflow icon on devices with menu key by using Toolbar

Closes #738

See merge request !463
2016-12-16 11:58:18 +00:00
goapunk
7666b210e4 Ignore results from the QrScanner if task is not ready
* Ignore results until the KeyAgreementTask is ready and returned the local payload

Signed-off-by: goapunk <noobie@goapunks.net>
2016-12-14 17:28:01 +01:00
Torsten Grote
db71472501 Also show overflow icon on devices with menu key by using Toolbar 2016-12-14 11:17:04 -02:00
215 changed files with 5330 additions and 5168 deletions

View File

@@ -3,7 +3,7 @@ package org.briarproject.bramble.api.sync;
import java.util.Collection;
/**
* A packet acknowledging receipt of one or more {@link Message Messages}.
* A record acknowledging receipt of one or more {@link Message Messages}.
*/
public class Ack {

View File

@@ -1,5 +1,7 @@
package org.briarproject.bramble.api.sync;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
public class Group {
public enum Visibility {
@@ -13,6 +15,8 @@ public class Group {
private final byte[] descriptor;
public Group(GroupId id, ClientId clientId, byte[] descriptor) {
if (descriptor.length > MAX_GROUP_DESCRIPTOR_LENGTH)
throw new IllegalArgumentException();
this.id = id;
this.clientId = clientId;
this.descriptor = descriptor;

View File

@@ -3,7 +3,7 @@ package org.briarproject.bramble.api.sync;
import java.util.Collection;
/**
* A packet offering the recipient one or more {@link Message Messages}.
* A record offering the recipient one or more {@link Message Messages}.
*/
public class Offer {

View File

@@ -5,7 +5,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.IOException;
@NotNullByDefault
public interface PacketReader {
public interface RecordReader {
boolean eof() throws IOException;
@@ -24,4 +24,5 @@ public interface PacketReader {
boolean hasRequest() throws IOException;
Request readRequest() throws IOException;
}

View File

@@ -5,7 +5,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
@NotNullByDefault
public interface PacketReaderFactory {
public interface RecordReaderFactory {
PacketReader createPacketReader(InputStream in);
RecordReader createRecordReader(InputStream in);
}

View File

@@ -1,12 +1,13 @@
package org.briarproject.bramble.api.sync;
/**
* Packet types for the sync protocol.
* Record types for the sync protocol.
*/
public interface PacketTypes {
public interface RecordTypes {
byte ACK = 0;
byte MESSAGE = 1;
byte OFFER = 2;
byte REQUEST = 3;
}

View File

@@ -5,7 +5,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.IOException;
@NotNullByDefault
public interface PacketWriter {
public interface RecordWriter {
void writeAck(Ack a) throws IOException;

View File

@@ -5,7 +5,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.OutputStream;
@NotNullByDefault
public interface PacketWriterFactory {
public interface RecordWriterFactory {
PacketWriter createPacketWriter(OutputStream out);
RecordWriter createRecordWriter(OutputStream out);
}

View File

@@ -3,7 +3,7 @@ package org.briarproject.bramble.api.sync;
import java.util.Collection;
/**
* A packet requesting one or more {@link Message Messages} from the recipient.
* A record requesting one or more {@link Message Messages} from the recipient.
*/
public class Request {

View File

@@ -10,19 +10,17 @@ public interface SyncConstants {
byte PROTOCOL_VERSION = 0;
/**
* The length of the packet header in bytes.
* The length of the record header in bytes.
*/
int PACKET_HEADER_LENGTH = 4;
int RECORD_HEADER_LENGTH = 4;
/**
* The maximum length of the packet payload in bytes.
* The maximum length of the record payload in bytes.
*/
int MAX_PACKET_PAYLOAD_LENGTH = 32 * 1024; // 32 KiB
int MAX_RECORD_PAYLOAD_LENGTH = 48 * 1024; // 48 KiB
/**
* The maximum length of a message in bytes.
*/
int MAX_MESSAGE_LENGTH = MAX_PACKET_PAYLOAD_LENGTH - PACKET_HEADER_LENGTH;
/** The maximum length of a group descriptor in bytes. */
int MAX_GROUP_DESCRIPTOR_LENGTH = 16 * 1024; // 16 KiB
/**
* The length of the message header in bytes.
@@ -32,10 +30,15 @@ public interface SyncConstants {
/**
* The maximum length of a message body in bytes.
*/
int MAX_MESSAGE_BODY_LENGTH = MAX_MESSAGE_LENGTH - MESSAGE_HEADER_LENGTH;
int MAX_MESSAGE_BODY_LENGTH = 32 * 1024; // 32 KiB
/**
* The maximum number of message IDs in an ack, offer or request packet.
* The maximum length of a message in bytes.
*/
int MAX_MESSAGE_IDS = MAX_PACKET_PAYLOAD_LENGTH / UniqueId.LENGTH;
int MAX_MESSAGE_LENGTH = MESSAGE_HEADER_LENGTH + MAX_MESSAGE_BODY_LENGTH;
/**
* The maximum number of message IDs in an ack, offer or request record.
*/
int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_LENGTH / UniqueId.LENGTH;
}

View File

@@ -5,7 +5,7 @@ import java.io.IOException;
public interface SyncSession {
/**
* Runs the session. This method returns when there are no more packets to
* Runs the session. This method returns when there are no more records to
* send or receive, or when the {@link #interrupt()} method has been called.
*/
void run() throws IOException;

View File

@@ -52,7 +52,7 @@ class KeyAgreementProtocol {
void connectionWaiting();
void initialPacketReceived();
void initialRecordReceived();
}
private final Callbacks callbacks;
@@ -117,7 +117,7 @@ class KeyAgreementProtocol {
private byte[] receiveKey() throws AbortException {
byte[] publicKey = transport.receiveKey();
callbacks.initialPacketReceived();
callbacks.initialRecordReceived();
byte[] expected = crypto.deriveKeyCommitment(publicKey);
if (!Arrays.equals(expected, theirPayload.getCommitment()))
throw new AbortException();

View File

@@ -129,7 +129,7 @@ class KeyAgreementTaskImpl extends Thread implements
}
@Override
public void initialPacketReceived() {
public void initialRecordReceived() {
// We send this here instead of when we create the protocol, so that
// if device A makes a connection after getting device B's payload and
// starts its protocol, device A's UI doesn't change to prevent device B

View File

@@ -95,20 +95,30 @@ class KeyAgreementTransport {
out.flush();
}
private byte[] readRecord(byte type) throws AbortException {
byte[] header = readHeader();
if (header[0] != PROTOCOL_VERSION)
throw new AbortException(); // TODO handle?
if (header[1] != type) {
// Unexpected packet
throw new AbortException(header[1] == ABORT);
}
int len = ByteUtils.readUint16(header,
RECORD_HEADER_PAYLOAD_LENGTH_OFFSET);
try {
return readData(len);
} catch (IOException e) {
throw new AbortException(e);
private byte[] readRecord(byte expectedType) throws AbortException {
while (true) {
byte[] header = readHeader();
byte version = header[0], type = header[1];
int len = ByteUtils.readUint16(header,
RECORD_HEADER_PAYLOAD_LENGTH_OFFSET);
// Reject unrecognised protocol version
if (version != PROTOCOL_VERSION) throw new AbortException(false);
if (type == ABORT) throw new AbortException(true);
if (type == expectedType) {
try {
return readData(len);
} catch (IOException e) {
throw new AbortException(e);
}
}
// Reject recognised but unexpected record type
if (type == KEY || type == CONFIRM) throw new AbortException(false);
// Skip unrecognised record type
try {
readData(len);
} catch (IOException e) {
throw new AbortException(e);
}
}
}

View File

@@ -14,7 +14,7 @@ import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
@@ -37,19 +37,19 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
/**
* An outgoing {@link SyncSession} suitable for duplex transports. The session
* offers messages before sending them, keeps its output stream open when there
* are no packets to send, and reacts to events that make packets available to
* are no records to send, and reacts to events that make records available to
* send.
*/
@ThreadSafe
@NotNullByDefault
class DuplexOutgoingSession implements SyncSession, EventListener {
// Check for retransmittable packets once every 60 seconds
// Check for retransmittable records once every 60 seconds
private static final int RETX_QUERY_INTERVAL = 60 * 1000;
private static final Logger LOG =
Logger.getLogger(DuplexOutgoingSession.class.getName());
@@ -67,14 +67,14 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private final Clock clock;
private final ContactId contactId;
private final int maxLatency, maxIdleTime;
private final PacketWriter packetWriter;
private final RecordWriter recordWriter;
private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
private volatile boolean interrupted = false;
DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, Clock clock, ContactId contactId, int maxLatency,
int maxIdleTime, PacketWriter packetWriter) {
int maxIdleTime, RecordWriter recordWriter) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
@@ -82,7 +82,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
this.contactId = contactId;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
this.packetWriter = packetWriter;
this.recordWriter = recordWriter;
writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
}
@@ -91,7 +91,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
public void run() throws IOException {
eventBus.addListener(this);
try {
// Start a query for each type of packet
// Start a query for each type of record
dbExecutor.execute(new GenerateAck());
dbExecutor.execute(new GenerateBatch());
dbExecutor.execute(new GenerateOffer());
@@ -100,33 +100,33 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
long nextKeepalive = now + maxIdleTime;
long nextRetxQuery = now + RETX_QUERY_INTERVAL;
boolean dataToFlush = true;
// Write packets until interrupted
// Write records until interrupted
try {
while (!interrupted) {
// Work out how long we should wait for a packet
// Work out how long we should wait for a record
now = clock.currentTimeMillis();
long wait = Math.min(nextKeepalive, nextRetxQuery) - now;
if (wait < 0) wait = 0;
// Flush any unflushed data if we're going to wait
if (wait > 0 && dataToFlush && writerTasks.isEmpty()) {
packetWriter.flush();
recordWriter.flush();
dataToFlush = false;
nextKeepalive = now + maxIdleTime;
}
// Wait for a packet
// Wait for a record
ThrowingRunnable<IOException> task = writerTasks.poll(wait,
MILLISECONDS);
if (task == null) {
now = clock.currentTimeMillis();
if (now >= nextRetxQuery) {
// Check for retransmittable packets
// Check for retransmittable records
dbExecutor.execute(new GenerateBatch());
dbExecutor.execute(new GenerateOffer());
nextRetxQuery = now + RETX_QUERY_INTERVAL;
}
if (now >= nextKeepalive) {
// Flush the stream to keep it alive
packetWriter.flush();
recordWriter.flush();
dataToFlush = false;
nextKeepalive = now + maxIdleTime;
}
@@ -137,9 +137,9 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
dataToFlush = true;
}
}
if (dataToFlush) packetWriter.flush();
if (dataToFlush) recordWriter.flush();
} catch (InterruptedException e) {
LOG.info("Interrupted while waiting for a packet to write");
LOG.info("Interrupted while waiting for a record to write");
Thread.currentThread().interrupt();
}
} finally {
@@ -215,7 +215,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override
public void run() throws IOException {
if (interrupted) return;
packetWriter.writeAck(ack);
recordWriter.writeAck(ack);
LOG.info("Sent ack");
dbExecutor.execute(new GenerateAck());
}
@@ -232,7 +232,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
Transaction txn = db.startTransaction(false);
try {
b = db.generateRequestedBatch(txn, contactId,
MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
MAX_RECORD_PAYLOAD_LENGTH, maxLatency);
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
@@ -259,7 +259,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override
public void run() throws IOException {
if (interrupted) return;
for (byte[] raw : batch) packetWriter.writeMessage(raw);
for (byte[] raw : batch) recordWriter.writeMessage(raw);
LOG.info("Sent batch");
dbExecutor.execute(new GenerateBatch());
}
@@ -303,7 +303,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override
public void run() throws IOException {
if (interrupted) return;
packetWriter.writeOffer(offer);
recordWriter.writeOffer(offer);
LOG.info("Sent offer");
dbExecutor.execute(new GenerateOffer());
}
@@ -346,7 +346,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override
public void run() throws IOException {
if (interrupted) return;
packetWriter.writeRequest(request);
recordWriter.writeRequest(request);
LOG.info("Sent request");
dbExecutor.execute(new GenerateRequest());
}

View File

@@ -16,7 +16,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.PacketReader;
import org.briarproject.bramble.api.sync.RecordReader;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncSession;
@@ -42,18 +42,18 @@ class IncomingSession implements SyncSession, EventListener {
private final Executor dbExecutor;
private final EventBus eventBus;
private final ContactId contactId;
private final PacketReader packetReader;
private final RecordReader recordReader;
private volatile boolean interrupted = false;
IncomingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, ContactId contactId,
PacketReader packetReader) {
RecordReader recordReader) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
this.contactId = contactId;
this.packetReader = packetReader;
this.recordReader = recordReader;
}
@IoExecutor
@@ -61,21 +61,22 @@ class IncomingSession implements SyncSession, EventListener {
public void run() throws IOException {
eventBus.addListener(this);
try {
// Read packets until interrupted or EOF
while (!interrupted && !packetReader.eof()) {
if (packetReader.hasAck()) {
Ack a = packetReader.readAck();
// Read records until interrupted or EOF
while (!interrupted && !recordReader.eof()) {
if (recordReader.hasAck()) {
Ack a = recordReader.readAck();
dbExecutor.execute(new ReceiveAck(a));
} else if (packetReader.hasMessage()) {
Message m = packetReader.readMessage();
} else if (recordReader.hasMessage()) {
Message m = recordReader.readMessage();
dbExecutor.execute(new ReceiveMessage(m));
} else if (packetReader.hasOffer()) {
Offer o = packetReader.readOffer();
} else if (recordReader.hasOffer()) {
Offer o = recordReader.readOffer();
dbExecutor.execute(new ReceiveOffer(o));
} else if (packetReader.hasRequest()) {
Request r = packetReader.readRequest();
} else if (recordReader.hasRequest()) {
Request r = recordReader.readRequest();
dbExecutor.execute(new ReceiveRequest(r));
} else {
// unknown records are ignored in RecordReader#eof()
throw new FormatException();
}
}

View File

@@ -30,11 +30,15 @@ class MessageFactoryImpl implements MessageFactory {
public Message createMessage(GroupId g, long timestamp, byte[] body) {
if (body.length > MAX_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException();
byte[] timeBytes = new byte[ByteUtils.INT_64_BYTES];
ByteUtils.writeUint64(timestamp, timeBytes, 0);
byte[] idHash =
crypto.hash(MessageId.LABEL, g.getBytes(), timeBytes, body);
MessageId id = new MessageId(idHash);
byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length];
System.arraycopy(g.getBytes(), 0, raw, 0, UniqueId.LENGTH);
ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH);
System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length);
MessageId id = new MessageId(crypto.hash(MessageId.LABEL, raw));
return new Message(id, g, timestamp, raw);
}

View File

@@ -1,28 +0,0 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.PacketReader;
import org.briarproject.bramble.api.sync.PacketReaderFactory;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class PacketReaderFactoryImpl implements PacketReaderFactory {
private final CryptoComponent crypto;
@Inject
PacketReaderFactoryImpl(CryptoComponent crypto) {
this.crypto = crypto;
}
@Override
public PacketReader createPacketReader(InputStream in) {
return new PacketReaderImpl(crypto, in);
}
}

View File

@@ -1,16 +0,0 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.api.sync.PacketWriterFactory;
import java.io.OutputStream;
@NotNullByDefault
class PacketWriterFactoryImpl implements PacketWriterFactory {
@Override
public PacketWriter createPacketWriter(OutputStream out) {
return new PacketWriterImpl(out);
}
}

View File

@@ -0,0 +1,28 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.RecordReader;
import org.briarproject.bramble.api.sync.RecordReaderFactory;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class RecordReaderFactoryImpl implements RecordReaderFactory {
private final MessageFactory messageFactory;
@Inject
RecordReaderFactoryImpl(MessageFactory messageFactory) {
this.messageFactory = messageFactory;
}
@Override
public RecordReader createRecordReader(InputStream in) {
return new RecordReaderImpl(messageFactory, in);
}
}

View File

@@ -2,14 +2,14 @@ package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack;
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.Offer;
import org.briarproject.bramble.api.sync.PacketReader;
import org.briarproject.bramble.api.sync.RecordReader;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.util.ByteUtils;
@@ -20,66 +20,84 @@ import java.util.List;
import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.bramble.api.sync.PacketTypes.ACK;
import static org.briarproject.bramble.api.sync.PacketTypes.MESSAGE;
import static org.briarproject.bramble.api.sync.PacketTypes.OFFER;
import static org.briarproject.bramble.api.sync.PacketTypes.REQUEST;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
import static org.briarproject.bramble.api.sync.RecordTypes.MESSAGE;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PACKET_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.api.sync.SyncConstants.RECORD_HEADER_LENGTH;
@NotThreadSafe
@NotNullByDefault
class PacketReaderImpl implements PacketReader {
class RecordReaderImpl implements RecordReader {
private enum State { BUFFER_EMPTY, BUFFER_FULL, EOF }
private enum State {BUFFER_EMPTY, BUFFER_FULL, EOF}
private final CryptoComponent crypto;
private final MessageFactory messageFactory;
private final InputStream in;
private final byte[] header, payload;
private State state = State.BUFFER_EMPTY;
private int payloadLength = 0;
PacketReaderImpl(CryptoComponent crypto, InputStream in) {
this.crypto = crypto;
RecordReaderImpl(MessageFactory messageFactory, InputStream in) {
this.messageFactory = messageFactory;
this.in = in;
header = new byte[PACKET_HEADER_LENGTH];
payload = new byte[MAX_PACKET_PAYLOAD_LENGTH];
header = new byte[RECORD_HEADER_LENGTH];
payload = new byte[MAX_RECORD_PAYLOAD_LENGTH];
}
private void readPacket() throws IOException {
private void readRecord() throws IOException {
if (state != State.BUFFER_EMPTY) throw new IllegalStateException();
// Read the header
int offset = 0;
while (offset < PACKET_HEADER_LENGTH) {
int read = in.read(header, offset, PACKET_HEADER_LENGTH - offset);
if (read == -1) {
if (offset > 0) throw new FormatException();
state = State.EOF;
while (true) {
// Read the header
int offset = 0;
while (offset < RECORD_HEADER_LENGTH) {
int read =
in.read(header, offset, RECORD_HEADER_LENGTH - offset);
if (read == -1) {
if (offset > 0) throw new FormatException();
state = State.EOF;
return;
}
offset += read;
}
byte version = header[0], type = header[1];
payloadLength = ByteUtils.readUint16(header, 2);
// Check the protocol version
if (version != PROTOCOL_VERSION) throw new FormatException();
// Check the payload length
if (payloadLength > MAX_RECORD_PAYLOAD_LENGTH)
throw new FormatException();
// Read the payload
offset = 0;
while (offset < payloadLength) {
int read = in.read(payload, offset, payloadLength - offset);
if (read == -1) throw new FormatException();
offset += read;
}
state = State.BUFFER_FULL;
// Return if this is a known record type, otherwise continue
if (type == ACK || type == MESSAGE || type == OFFER ||
type == REQUEST) {
return;
}
offset += read;
}
// Check the protocol version
if (header[0] != PROTOCOL_VERSION) throw new FormatException();
// Read the payload length
payloadLength = ByteUtils.readUint16(header, 2);
if (payloadLength > MAX_PACKET_PAYLOAD_LENGTH) throw new FormatException();
// Read the payload
offset = 0;
while (offset < payloadLength) {
int read = in.read(payload, offset, payloadLength - offset);
if (read == -1) throw new FormatException();
offset += read;
}
state = State.BUFFER_FULL;
}
/**
* Returns true if there's another record available or false if we've
* reached the end of the input stream.
* <p>
* If a record is available, it's been read into the buffer by the time
* eof() returns, so the method that called eof() can access the record
* from the buffer, for example to check its type or extract its payload.
*/
@Override
public boolean eof() throws IOException {
if (state == State.BUFFER_EMPTY) readPacket();
if (state == State.BUFFER_EMPTY) readRecord();
if (state == State.BUFFER_EMPTY) throw new IllegalStateException();
return state == State.EOF;
}
@@ -124,13 +142,12 @@ class PacketReaderImpl implements PacketReader {
// Timestamp
long timestamp = ByteUtils.readUint64(payload, UniqueId.LENGTH);
if (timestamp < 0) throw new FormatException();
// Raw message
byte[] raw = new byte[payloadLength];
System.arraycopy(payload, 0, raw, 0, payloadLength);
// Body
byte[] body = new byte[payloadLength - MESSAGE_HEADER_LENGTH];
System.arraycopy(payload, MESSAGE_HEADER_LENGTH, body, 0,
payloadLength - MESSAGE_HEADER_LENGTH);
state = State.BUFFER_EMPTY;
// Message ID
MessageId messageId = new MessageId(crypto.hash(MessageId.LABEL, raw));
return new Message(messageId, groupId, timestamp, raw);
return messageFactory.createMessage(groupId, timestamp, body);
}
@Override
@@ -154,4 +171,5 @@ class PacketReaderImpl implements PacketReader {
if (!hasRequest()) throw new FormatException();
return new Request(readMessageIds());
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.briarproject.bramble.api.sync.RecordWriterFactory;
import java.io.OutputStream;
@NotNullByDefault
class RecordWriterFactoryImpl implements RecordWriterFactory {
@Override
public RecordWriter createRecordWriter(OutputStream out) {
return new RecordWriterImpl(out);
}
}

View File

@@ -4,8 +4,8 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.PacketTypes;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.api.sync.RecordTypes;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.util.ByteUtils;
@@ -15,30 +15,30 @@ import java.io.OutputStream;
import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.bramble.api.sync.PacketTypes.ACK;
import static org.briarproject.bramble.api.sync.PacketTypes.OFFER;
import static org.briarproject.bramble.api.sync.PacketTypes.REQUEST;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PACKET_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.RECORD_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@NotThreadSafe
@NotNullByDefault
class PacketWriterImpl implements PacketWriter {
class RecordWriterImpl implements RecordWriter {
private final OutputStream out;
private final byte[] header;
private final ByteArrayOutputStream payload;
PacketWriterImpl(OutputStream out) {
RecordWriterImpl(OutputStream out) {
this.out = out;
header = new byte[PACKET_HEADER_LENGTH];
header = new byte[RECORD_HEADER_LENGTH];
header[0] = PROTOCOL_VERSION;
payload = new ByteArrayOutputStream(MAX_PACKET_PAYLOAD_LENGTH);
payload = new ByteArrayOutputStream(MAX_RECORD_PAYLOAD_LENGTH);
}
private void writePacket(byte packetType) throws IOException {
header[1] = packetType;
private void writeRecord(byte recordType) throws IOException {
header[1] = recordType;
ByteUtils.writeUint16(payload.size(), header, 2);
out.write(header);
payload.writeTo(out);
@@ -49,12 +49,12 @@ class PacketWriterImpl implements PacketWriter {
public void writeAck(Ack a) throws IOException {
if (payload.size() != 0) throw new IllegalStateException();
for (MessageId m : a.getMessageIds()) payload.write(m.getBytes());
writePacket(ACK);
writeRecord(ACK);
}
@Override
public void writeMessage(byte[] raw) throws IOException {
header[1] = PacketTypes.MESSAGE;
header[1] = RecordTypes.MESSAGE;
ByteUtils.writeUint16(raw.length, header, 2);
out.write(header);
out.write(raw);
@@ -64,14 +64,14 @@ class PacketWriterImpl implements PacketWriter {
public void writeOffer(Offer o) throws IOException {
if (payload.size() != 0) throw new IllegalStateException();
for (MessageId m : o.getMessageIds()) payload.write(m.getBytes());
writePacket(OFFER);
writeRecord(OFFER);
}
@Override
public void writeRequest(Request r) throws IOException {
if (payload.size() != 0) throw new IllegalStateException();
for (MessageId m : r.getMessageIds()) payload.write(m.getBytes());
writePacket(REQUEST);
writeRecord(REQUEST);
}
@Override

View File

@@ -13,7 +13,7 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.briarproject.bramble.api.sync.SyncSession;
import java.io.IOException;
@@ -29,12 +29,12 @@ import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
/**
* An outgoing {@link SyncSession} suitable for simplex transports. The session
* sends messages without offering them first, and closes its output stream
* when there are no more packets to send.
* when there are no more records to send.
*/
@ThreadSafe
@NotNullByDefault
@@ -55,7 +55,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
private final EventBus eventBus;
private final ContactId contactId;
private final int maxLatency;
private final PacketWriter packetWriter;
private final RecordWriter recordWriter;
private final AtomicInteger outstandingQueries;
private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
@@ -63,14 +63,14 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
SimplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
EventBus eventBus, ContactId contactId,
int maxLatency, PacketWriter packetWriter) {
int maxLatency, RecordWriter recordWriter) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
this.contactId = contactId;
this.maxLatency = maxLatency;
this.packetWriter = packetWriter;
outstandingQueries = new AtomicInteger(2); // One per type of packet
this.recordWriter = recordWriter;
outstandingQueries = new AtomicInteger(2); // One per type of record
writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
}
@@ -79,19 +79,19 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
public void run() throws IOException {
eventBus.addListener(this);
try {
// Start a query for each type of packet
// Start a query for each type of record
dbExecutor.execute(new GenerateAck());
dbExecutor.execute(new GenerateBatch());
// Write packets until interrupted or no more packets to write
// Write records until interrupted or no more records to write
try {
while (!interrupted) {
ThrowingRunnable<IOException> task = writerTasks.take();
if (task == CLOSE) break;
task.run();
}
packetWriter.flush();
recordWriter.flush();
} catch (InterruptedException e) {
LOG.info("Interrupted while waiting for a packet to write");
LOG.info("Interrupted while waiting for a record to write");
Thread.currentThread().interrupt();
}
} finally {
@@ -157,7 +157,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
@Override
public void run() throws IOException {
if (interrupted) return;
packetWriter.writeAck(ack);
recordWriter.writeAck(ack);
LOG.info("Sent ack");
dbExecutor.execute(new GenerateAck());
}
@@ -174,7 +174,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
Transaction txn = db.startTransaction(false);
try {
b = db.generateBatch(txn, contactId,
MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
MAX_RECORD_PAYLOAD_LENGTH, maxLatency);
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
@@ -202,7 +202,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
@Override
public void run() throws IOException {
if (interrupted) return;
for (byte[] raw : batch) packetWriter.writeMessage(raw);
for (byte[] raw : batch) recordWriter.writeMessage(raw);
LOG.info("Sent batch");
dbExecutor.execute(new GenerateBatch());
}

View File

@@ -7,8 +7,8 @@ import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.sync.GroupFactory;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.PacketReaderFactory;
import org.briarproject.bramble.api.sync.PacketWriterFactory;
import org.briarproject.bramble.api.sync.RecordReaderFactory;
import org.briarproject.bramble.api.sync.RecordWriterFactory;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.sync.ValidationManager;
import org.briarproject.bramble.api.system.Clock;
@@ -40,23 +40,24 @@ public class SyncModule {
}
@Provides
PacketReaderFactory providePacketReaderFactory(CryptoComponent crypto) {
return new PacketReaderFactoryImpl(crypto);
RecordReaderFactory provideRecordReaderFactory(
RecordReaderFactoryImpl recordReaderFactory) {
return recordReaderFactory;
}
@Provides
PacketWriterFactory providePacketWriterFactory() {
return new PacketWriterFactoryImpl();
RecordWriterFactory provideRecordWriterFactory() {
return new RecordWriterFactoryImpl();
}
@Provides
@Singleton
SyncSessionFactory provideSyncSessionFactory(DatabaseComponent db,
@DatabaseExecutor Executor dbExecutor, EventBus eventBus,
Clock clock, PacketReaderFactory packetReaderFactory,
PacketWriterFactory packetWriterFactory) {
Clock clock, RecordReaderFactory recordReaderFactory,
RecordWriterFactory recordWriterFactory) {
return new SyncSessionFactoryImpl(db, dbExecutor, eventBus, clock,
packetReaderFactory, packetWriterFactory);
recordReaderFactory, recordWriterFactory);
}
@Provides

View File

@@ -5,10 +5,10 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.PacketReader;
import org.briarproject.bramble.api.sync.PacketReaderFactory;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.api.sync.PacketWriterFactory;
import org.briarproject.bramble.api.sync.RecordReader;
import org.briarproject.bramble.api.sync.RecordReaderFactory;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.briarproject.bramble.api.sync.RecordWriterFactory;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.system.Clock;
@@ -28,41 +28,41 @@ class SyncSessionFactoryImpl implements SyncSessionFactory {
private final Executor dbExecutor;
private final EventBus eventBus;
private final Clock clock;
private final PacketReaderFactory packetReaderFactory;
private final PacketWriterFactory packetWriterFactory;
private final RecordReaderFactory recordReaderFactory;
private final RecordWriterFactory recordWriterFactory;
@Inject
SyncSessionFactoryImpl(DatabaseComponent db,
@DatabaseExecutor Executor dbExecutor, EventBus eventBus,
Clock clock, PacketReaderFactory packetReaderFactory,
PacketWriterFactory packetWriterFactory) {
Clock clock, RecordReaderFactory recordReaderFactory,
RecordWriterFactory recordWriterFactory) {
this.db = db;
this.dbExecutor = dbExecutor;
this.eventBus = eventBus;
this.clock = clock;
this.packetReaderFactory = packetReaderFactory;
this.packetWriterFactory = packetWriterFactory;
this.recordReaderFactory = recordReaderFactory;
this.recordWriterFactory = recordWriterFactory;
}
@Override
public SyncSession createIncomingSession(ContactId c, InputStream in) {
PacketReader packetReader = packetReaderFactory.createPacketReader(in);
return new IncomingSession(db, dbExecutor, eventBus, c, packetReader);
RecordReader recordReader = recordReaderFactory.createRecordReader(in);
return new IncomingSession(db, dbExecutor, eventBus, c, recordReader);
}
@Override
public SyncSession createSimplexOutgoingSession(ContactId c,
int maxLatency, OutputStream out) {
PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out);
RecordWriter recordWriter = recordWriterFactory.createRecordWriter(out);
return new SimplexOutgoingSession(db, dbExecutor, eventBus, c,
maxLatency, packetWriter);
maxLatency, recordWriter);
}
@Override
public SyncSession createDuplexOutgoingSession(ContactId c, int maxLatency,
int maxIdleTime, OutputStream out) {
PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out);
RecordWriter recordWriter = recordWriterFactory.createRecordWriter(out);
return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c,
maxLatency, maxIdleTime, packetWriter);
maxLatency, maxIdleTime, recordWriter);
}
}

View File

@@ -63,6 +63,7 @@ import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_K
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
@@ -95,7 +96,7 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
public DatabaseComponentImplTest() {
clientId = new ClientId(TestUtils.getRandomString(5));
groupId = new GroupId(TestUtils.getRandomId());
byte[] descriptor = new byte[0];
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
group = new Group(groupId, clientId, descriptor);
authorId = new AuthorId(TestUtils.getRandomId());
author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);

View File

@@ -47,6 +47,7 @@ import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_K
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
@@ -84,7 +85,7 @@ public class H2DatabaseTest extends BrambleTestCase {
public H2DatabaseTest() throws Exception {
groupId = new GroupId(TestUtils.getRandomId());
clientId = new ClientId(TestUtils.getRandomString(5));
byte[] descriptor = new byte[0];
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
group = new Group(groupId, clientId, descriptor);
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
@@ -1316,7 +1317,7 @@ public class H2DatabaseTest extends BrambleTestCase {
// Add a second group
GroupId groupId1 = new GroupId(TestUtils.getRandomId());
Group group1 = new Group(groupId1, clientId,
TestUtils.getRandomBytes(42));
TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
db.addGroup(txn, group1);
// Add a message to the second group

View File

@@ -92,7 +92,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
oneOf(callbacks).connectionWaiting();
oneOf(transport).receiveKey();
will(returnValue(BOB_PUBKEY));
oneOf(callbacks).initialPacketReceived();
oneOf(callbacks).initialRecordReceived();
// Alice verifies Bob's public key
oneOf(crypto).deriveKeyCommitment(BOB_PUBKEY);
@@ -152,7 +152,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
// Bob receives Alice's public key
oneOf(transport).receiveKey();
will(returnValue(ALICE_PUBKEY));
oneOf(callbacks).initialPacketReceived();
oneOf(callbacks).initialRecordReceived();
// Bob verifies Alice's public key
oneOf(crypto).deriveKeyCommitment(ALICE_PUBKEY);
@@ -213,7 +213,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
oneOf(callbacks).connectionWaiting();
oneOf(transport).receiveKey();
will(returnValue(BAD_PUBKEY));
oneOf(callbacks).initialPacketReceived();
oneOf(callbacks).initialRecordReceived();
// Alice verifies Bob's public key
oneOf(crypto).deriveKeyCommitment(BAD_PUBKEY);
@@ -250,7 +250,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
// Bob receives a bad public key
oneOf(transport).receiveKey();
will(returnValue(BAD_PUBKEY));
oneOf(callbacks).initialPacketReceived();
oneOf(callbacks).initialRecordReceived();
// Bob verifies Alice's public key
oneOf(crypto).deriveKeyCommitment(BAD_PUBKEY);
@@ -296,7 +296,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
oneOf(callbacks).connectionWaiting();
oneOf(transport).receiveKey();
will(returnValue(BOB_PUBKEY));
oneOf(callbacks).initialPacketReceived();
oneOf(callbacks).initialRecordReceived();
// Alice verifies Bob's public key
oneOf(crypto).deriveKeyCommitment(BOB_PUBKEY);
@@ -357,7 +357,7 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
// Bob receives Alice's public key
oneOf(transport).receiveKey();
will(returnValue(ALICE_PUBKEY));
oneOf(callbacks).initialPacketReceived();
oneOf(callbacks).initialRecordReceived();
// Bob verifies Alice's public key
oneOf(crypto).deriveKeyCommitment(ALICE_PUBKEY);

View File

@@ -0,0 +1,251 @@
package org.briarproject.bramble.keyagreement;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.ByteUtils;
import org.jmock.Expectations;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_LENGTH;
import static org.briarproject.bramble.api.keyagreement.RecordTypes.ABORT;
import static org.briarproject.bramble.api.keyagreement.RecordTypes.CONFIRM;
import static org.briarproject.bramble.api.keyagreement.RecordTypes.KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class KeyAgreementTransportTest extends BrambleMockTestCase {
private final DuplexTransportConnection duplexTransportConnection =
context.mock(DuplexTransportConnection.class);
private final TransportConnectionReader transportConnectionReader =
context.mock(TransportConnectionReader.class);
private final TransportConnectionWriter transportConnectionWriter =
context.mock(TransportConnectionWriter.class);
private final TransportId transportId = new TransportId("test");
private final KeyAgreementConnection keyAgreementConnection =
new KeyAgreementConnection(duplexTransportConnection, transportId);
private ByteArrayInputStream inputStream;
private ByteArrayOutputStream outputStream;
private KeyAgreementTransport kat;
@Test
public void testSendKey() throws Exception {
setup(new byte[0]);
byte[] key = TestUtils.getRandomBytes(123);
kat.sendKey(key);
assertRecordSent(KEY, key);
}
@Test
public void testSendConfirm() throws Exception {
setup(new byte[0]);
byte[] confirm = TestUtils.getRandomBytes(123);
kat.sendConfirm(confirm);
assertRecordSent(CONFIRM, confirm);
}
@Test
public void testSendAbortWithException() throws Exception {
setup(new byte[0]);
context.checking(new Expectations() {{
oneOf(transportConnectionReader).dispose(true, true);
oneOf(transportConnectionWriter).dispose(true);
}});
kat.sendAbort(true);
assertRecordSent(ABORT, new byte[0]);
}
@Test
public void testSendAbortWithoutException() throws Exception {
setup(new byte[0]);
context.checking(new Expectations() {{
oneOf(transportConnectionReader).dispose(false, true);
oneOf(transportConnectionWriter).dispose(false);
}});
kat.sendAbort(false);
assertRecordSent(ABORT, new byte[0]);
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfAtEndOfStream()
throws Exception {
setup(new byte[0]);
kat.receiveKey();
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfHeaderIsTooShort()
throws Exception {
byte[] input = new byte[RECORD_HEADER_LENGTH - 1];
input[0] = PROTOCOL_VERSION;
input[1] = KEY;
setup(input);
kat.receiveKey();
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfPayloadIsTooShort()
throws Exception {
int payloadLength = 123;
byte[] input = new byte[RECORD_HEADER_LENGTH + payloadLength - 1];
input[0] = PROTOCOL_VERSION;
input[1] = KEY;
ByteUtils.writeUint16(payloadLength, input, 2);
setup(input);
kat.receiveKey();
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfProtocolVersionIsUnrecognised()
throws Exception {
setup(createRecord((byte) (PROTOCOL_VERSION + 1), KEY, new byte[123]));
kat.receiveKey();
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfAbortIsReceived()
throws Exception {
setup(createRecord(PROTOCOL_VERSION, ABORT, new byte[0]));
kat.receiveKey();
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfConfirmIsReceived()
throws Exception {
setup(createRecord(PROTOCOL_VERSION, CONFIRM, new byte[123]));
kat.receiveKey();
}
@Test
public void testReceiveKeySkipsUnrecognisedRecordTypes() throws Exception {
byte[] skip1 = createRecord(PROTOCOL_VERSION, (byte) (ABORT + 1),
new byte[123]);
byte[] skip2 = createRecord(PROTOCOL_VERSION, (byte) (ABORT + 2),
new byte[0]);
byte[] payload = TestUtils.getRandomBytes(123);
byte[] key = createRecord(PROTOCOL_VERSION, KEY, payload);
ByteArrayOutputStream input = new ByteArrayOutputStream();
input.write(skip1);
input.write(skip2);
input.write(key);
setup(input.toByteArray());
assertArrayEquals(payload, kat.receiveKey());
}
@Test(expected = AbortException.class)
public void testReceiveConfirmThrowsExceptionIfAtEndOfStream()
throws Exception {
setup(new byte[0]);
kat.receiveConfirm();
}
@Test(expected = AbortException.class)
public void testReceiveConfirmThrowsExceptionIfHeaderIsTooShort()
throws Exception {
byte[] input = new byte[RECORD_HEADER_LENGTH - 1];
input[0] = PROTOCOL_VERSION;
input[1] = CONFIRM;
setup(input);
kat.receiveConfirm();
}
@Test(expected = AbortException.class)
public void testReceiveConfirmThrowsExceptionIfPayloadIsTooShort()
throws Exception {
int payloadLength = 123;
byte[] input = new byte[RECORD_HEADER_LENGTH + payloadLength - 1];
input[0] = PROTOCOL_VERSION;
input[1] = CONFIRM;
ByteUtils.writeUint16(payloadLength, input, 2);
setup(input);
kat.receiveConfirm();
}
@Test(expected = AbortException.class)
public void testReceiveConfirmThrowsExceptionIfProtocolVersionIsUnrecognised()
throws Exception {
setup(createRecord((byte) (PROTOCOL_VERSION + 1), CONFIRM,
new byte[123]));
kat.receiveConfirm();
}
@Test(expected = AbortException.class)
public void testReceiveConfirmThrowsExceptionIfAbortIsReceived()
throws Exception {
setup(createRecord(PROTOCOL_VERSION, ABORT, new byte[0]));
kat.receiveConfirm();
}
@Test(expected = AbortException.class)
public void testReceiveKeyThrowsExceptionIfKeyIsReceived()
throws Exception {
setup(createRecord(PROTOCOL_VERSION, KEY, new byte[123]));
kat.receiveConfirm();
}
@Test
public void testReceiveConfirmSkipsUnrecognisedRecordTypes()
throws Exception {
byte[] skip1 = createRecord(PROTOCOL_VERSION, (byte) (ABORT + 1),
new byte[123]);
byte[] skip2 = createRecord(PROTOCOL_VERSION, (byte) (ABORT + 2),
new byte[0]);
byte[] payload = TestUtils.getRandomBytes(123);
byte[] confirm = createRecord(PROTOCOL_VERSION, CONFIRM, payload);
ByteArrayOutputStream input = new ByteArrayOutputStream();
input.write(skip1);
input.write(skip2);
input.write(confirm);
setup(input.toByteArray());
assertArrayEquals(payload, kat.receiveConfirm());
}
private void setup(byte[] input) throws Exception {
inputStream = new ByteArrayInputStream(input);
outputStream = new ByteArrayOutputStream();
context.checking(new Expectations() {{
allowing(duplexTransportConnection).getReader();
will(returnValue(transportConnectionReader));
allowing(transportConnectionReader).getInputStream();
will(returnValue(inputStream));
allowing(duplexTransportConnection).getWriter();
will(returnValue(transportConnectionWriter));
allowing(transportConnectionWriter).getOutputStream();
will(returnValue(outputStream));
}});
kat = new KeyAgreementTransport(keyAgreementConnection);
}
private void assertRecordSent(byte expectedType, byte[] expectedPayload) {
byte[] output = outputStream.toByteArray();
assertEquals(RECORD_HEADER_LENGTH + expectedPayload.length,
output.length);
assertEquals(PROTOCOL_VERSION, output[0]);
assertEquals(expectedType, output[1]);
assertEquals(expectedPayload.length, ByteUtils.readUint16(output, 2));
byte[] payload = new byte[output.length - RECORD_HEADER_LENGTH];
System.arraycopy(output, RECORD_HEADER_LENGTH, payload, 0,
payload.length);
assertArrayEquals(expectedPayload, payload);
}
private byte[] createRecord(byte version, byte type, byte[] payload) {
byte[] b = new byte[RECORD_HEADER_LENGTH + payload.length];
b[0] = version;
b[1] = type;
ByteUtils.writeUint16(payload.length, b, 2);
System.arraycopy(payload, 0, b, RECORD_HEADER_LENGTH, payload.length);
return b;
}
}

View File

@@ -1,162 +0,0 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.ByteUtils;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import static org.briarproject.bramble.api.sync.PacketTypes.ACK;
import static org.briarproject.bramble.api.sync.PacketTypes.OFFER;
import static org.briarproject.bramble.api.sync.PacketTypes.REQUEST;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PACKET_HEADER_LENGTH;
import static org.junit.Assert.assertEquals;
public class PacketReaderImplTest extends BrambleTestCase {
@Test(expected = FormatException.class)
public void testFormatExceptionIfAckIsTooLarge() throws Exception {
byte[] b = createAck(true);
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readAck();
}
@Test
public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception {
byte[] b = createAck(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readAck();
}
@Test(expected = FormatException.class)
public void testEmptyAck() throws Exception {
byte[] b = createEmptyAck();
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readAck();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfOfferIsTooLarge() throws Exception {
byte[] b = createOffer(true);
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readOffer();
}
@Test
public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception {
byte[] b = createOffer(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readOffer();
}
@Test(expected = FormatException.class)
public void testEmptyOffer() throws Exception {
byte[] b = createEmptyOffer();
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readOffer();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfRequestIsTooLarge() throws Exception {
byte[] b = createRequest(true);
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readRequest();
}
@Test
public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception {
byte[] b = createRequest(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readRequest();
}
@Test(expected = FormatException.class)
public void testEmptyRequest() throws Exception {
byte[] b = createEmptyRequest();
ByteArrayInputStream in = new ByteArrayInputStream(b);
PacketReaderImpl reader = new PacketReaderImpl(null, in);
reader.readRequest();
}
private byte[] createAck(boolean tooBig) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(new byte[PACKET_HEADER_LENGTH]);
while (out.size() + UniqueId.LENGTH <= PACKET_HEADER_LENGTH
+ MAX_PACKET_PAYLOAD_LENGTH) {
out.write(TestUtils.getRandomId());
}
if (tooBig) out.write(TestUtils.getRandomId());
assertEquals(tooBig, out.size() > PACKET_HEADER_LENGTH +
MAX_PACKET_PAYLOAD_LENGTH);
byte[] packet = out.toByteArray();
packet[1] = ACK;
ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2);
return packet;
}
private byte[] createEmptyAck() throws Exception {
byte[] packet = new byte[PACKET_HEADER_LENGTH];
packet[1] = ACK;
ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2);
return packet;
}
private byte[] createOffer(boolean tooBig) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(new byte[PACKET_HEADER_LENGTH]);
while (out.size() + UniqueId.LENGTH <= PACKET_HEADER_LENGTH
+ MAX_PACKET_PAYLOAD_LENGTH) {
out.write(TestUtils.getRandomId());
}
if (tooBig) out.write(TestUtils.getRandomId());
assertEquals(tooBig, out.size() > PACKET_HEADER_LENGTH +
MAX_PACKET_PAYLOAD_LENGTH);
byte[] packet = out.toByteArray();
packet[1] = OFFER;
ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2);
return packet;
}
private byte[] createEmptyOffer() throws Exception {
byte[] packet = new byte[PACKET_HEADER_LENGTH];
packet[1] = OFFER;
ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2);
return packet;
}
private byte[] createRequest(boolean tooBig) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(new byte[PACKET_HEADER_LENGTH]);
while (out.size() + UniqueId.LENGTH <= PACKET_HEADER_LENGTH
+ MAX_PACKET_PAYLOAD_LENGTH) {
out.write(TestUtils.getRandomId());
}
if (tooBig) out.write(TestUtils.getRandomId());
assertEquals(tooBig, out.size() > PACKET_HEADER_LENGTH +
MAX_PACKET_PAYLOAD_LENGTH);
byte[] packet = out.toByteArray();
packet[1] = REQUEST;
ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2);
return packet;
}
private byte[] createEmptyRequest() throws Exception {
byte[] packet = new byte[PACKET_HEADER_LENGTH];
packet[1] = REQUEST;
ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2);
return packet;
}
}

View File

@@ -0,0 +1,219 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.ByteUtils;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.api.sync.SyncConstants.RECORD_HEADER_LENGTH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class RecordReaderImplTest extends BrambleMockTestCase {
private final MessageFactory messageFactory =
context.mock(MessageFactory.class);
@Test(expected = FormatException.class)
public void testFormatExceptionIfAckIsTooLarge() throws Exception {
byte[] b = createAck(true);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readAck();
}
@Test
public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception {
byte[] b = createAck(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readAck();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfAckIsEmpty() throws Exception {
byte[] b = createEmptyAck();
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readAck();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfOfferIsTooLarge() throws Exception {
byte[] b = createOffer(true);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readOffer();
}
@Test
public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception {
byte[] b = createOffer(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readOffer();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfOfferIsEmpty() throws Exception {
byte[] b = createEmptyOffer();
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readOffer();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfRequestIsTooLarge() throws Exception {
byte[] b = createRequest(true);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readRequest();
}
@Test
public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception {
byte[] b = createRequest(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readRequest();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfRequestIsEmpty() throws Exception {
byte[] b = createEmptyRequest();
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.readRequest();
}
@Test
public void testEofReturnsTrueWhenAtEndOfStream() throws Exception {
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
assertTrue(reader.eof());
}
@Test
public void testEofReturnsFalseWhenNotAtEndOfStream() throws Exception {
byte[] b = createAck(false);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
assertFalse(reader.eof());
}
@Test(expected = FormatException.class)
public void testThrowsExceptionIfHeaderIsTooShort() throws Exception {
byte[] b = new byte[RECORD_HEADER_LENGTH - 1];
b[0] = PROTOCOL_VERSION;
b[1] = ACK;
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.eof();
}
@Test(expected = FormatException.class)
public void testThrowsExceptionIfPayloadIsTooShort() throws Exception {
int payloadLength = 123;
byte[] b = new byte[RECORD_HEADER_LENGTH + payloadLength - 1];
b[0] = PROTOCOL_VERSION;
b[1] = ACK;
ByteUtils.writeUint16(payloadLength, b, 2);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.eof();
}
@Test(expected = FormatException.class)
public void testThrowsExceptionIfProtocolVersionIsUnrecognised()
throws Exception {
byte version = (byte) (PROTOCOL_VERSION + 1);
byte[] b = createRecord(version, ACK, new byte[0]);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.eof();
}
@Test(expected = FormatException.class)
public void testThrowsExceptionIfPayloadIsTooLong() throws Exception {
byte[] payload = new byte[MAX_RECORD_PAYLOAD_LENGTH + 1];
byte[] b = createRecord(PROTOCOL_VERSION, ACK, payload);
ByteArrayInputStream in = new ByteArrayInputStream(b);
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
reader.eof();
}
@Test
public void testSkipsUnrecognisedRecordTypes() throws Exception {
byte[] skip1 = createRecord(PROTOCOL_VERSION, (byte) (REQUEST + 1),
new byte[123]);
byte[] skip2 = createRecord(PROTOCOL_VERSION, (byte) (REQUEST + 2),
new byte[0]);
byte[] ack = createAck(false);
ByteArrayOutputStream input = new ByteArrayOutputStream();
input.write(skip1);
input.write(skip2);
input.write(ack);
ByteArrayInputStream in = new ByteArrayInputStream(input.toByteArray());
RecordReaderImpl reader = new RecordReaderImpl(messageFactory, in);
assertTrue(reader.hasAck());
Ack a = reader.readAck();
assertEquals(MAX_MESSAGE_IDS, a.getMessageIds().size());
}
private byte[] createAck(boolean tooBig) throws Exception {
return createRecord(PROTOCOL_VERSION, ACK, createPayload(tooBig));
}
private byte[] createEmptyAck() throws Exception {
return createRecord(PROTOCOL_VERSION, ACK, new byte[0]);
}
private byte[] createOffer(boolean tooBig) throws Exception {
return createRecord(PROTOCOL_VERSION, OFFER, createPayload(tooBig));
}
private byte[] createEmptyOffer() throws Exception {
return createRecord(PROTOCOL_VERSION, OFFER, new byte[0]);
}
private byte[] createRequest(boolean tooBig) throws Exception {
return createRecord(PROTOCOL_VERSION, REQUEST, createPayload(tooBig));
}
private byte[] createEmptyRequest() throws Exception {
return createRecord(PROTOCOL_VERSION, REQUEST, new byte[0]);
}
private byte[] createRecord(byte version, byte type, byte[] payload) {
byte[] b = new byte[RECORD_HEADER_LENGTH + payload.length];
b[0] = version;
b[1] = type;
ByteUtils.writeUint16(payload.length, b, 2);
System.arraycopy(payload, 0, b, RECORD_HEADER_LENGTH, payload.length);
return b;
}
private byte[] createPayload(boolean tooBig) throws Exception {
ByteArrayOutputStream payload = new ByteArrayOutputStream();
while (payload.size() + UniqueId.LENGTH <= MAX_RECORD_PAYLOAD_LENGTH) {
payload.write(TestUtils.getRandomId());
}
if (tooBig) payload.write(TestUtils.getRandomId());
assertEquals(tooBig, payload.size() > MAX_RECORD_PAYLOAD_LENGTH);
return payload.toByteArray();
}
}

View File

@@ -6,10 +6,10 @@ import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.junit.Test;
@@ -29,14 +29,14 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
private final ContactId contactId;
private final MessageId messageId;
private final int maxLatency;
private final PacketWriter packetWriter;
private final RecordWriter recordWriter;
public SimplexOutgoingSessionTest() {
context = new Mockery();
db = context.mock(DatabaseComponent.class);
dbExecutor = new ImmediateExecutor();
eventBus = context.mock(EventBus.class);
packetWriter = context.mock(PacketWriter.class);
recordWriter = context.mock(RecordWriter.class);
contactId = new ContactId(234);
messageId = new MessageId(TestUtils.getRandomId());
maxLatency = Integer.MAX_VALUE;
@@ -45,7 +45,7 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
@Test
public void testNothingToSend() throws Exception {
final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
dbExecutor, eventBus, contactId, maxLatency, packetWriter);
dbExecutor, eventBus, contactId, maxLatency, recordWriter);
final Transaction noAckTxn = new Transaction(null, false);
final Transaction noMsgTxn = new Transaction(null, false);
@@ -68,7 +68,7 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
oneOf(db).commitTransaction(noMsgTxn);
oneOf(db).endTransaction(noMsgTxn);
// Flush the output stream
oneOf(packetWriter).flush();
oneOf(recordWriter).flush();
// Remove listener
oneOf(eventBus).removeListener(session);
}});
@@ -83,7 +83,7 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
final Ack ack = new Ack(Collections.singletonList(messageId));
final byte[] raw = new byte[1234];
final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
dbExecutor, eventBus, contactId, maxLatency, packetWriter);
dbExecutor, eventBus, contactId, maxLatency, recordWriter);
final Transaction ackTxn = new Transaction(null, false);
final Transaction noAckTxn = new Transaction(null, false);
final Transaction msgTxn = new Transaction(null, false);
@@ -99,7 +99,7 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
will(returnValue(ack));
oneOf(db).commitTransaction(ackTxn);
oneOf(db).endTransaction(ackTxn);
oneOf(packetWriter).writeAck(ack);
oneOf(recordWriter).writeAck(ack);
// One message to send
oneOf(db).startTransaction(false);
will(returnValue(msgTxn));
@@ -108,7 +108,7 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
will(returnValue(Arrays.asList(raw)));
oneOf(db).commitTransaction(msgTxn);
oneOf(db).endTransaction(msgTxn);
oneOf(packetWriter).writeMessage(raw);
oneOf(recordWriter).writeMessage(raw);
// No more acks
oneOf(db).startTransaction(false);
will(returnValue(noAckTxn));
@@ -125,7 +125,7 @@ public class SimplexOutgoingSessionTest extends BrambleTestCase {
oneOf(db).commitTransaction(noMsgTxn);
oneOf(db).endTransaction(noMsgTxn);
// Flush the output stream
oneOf(packetWriter).flush();
oneOf(recordWriter).flush();
// Remove listener
oneOf(eventBus).removeListener(session);
}});

View File

@@ -12,10 +12,10 @@ 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.Offer;
import org.briarproject.bramble.api.sync.PacketReader;
import org.briarproject.bramble.api.sync.PacketReaderFactory;
import org.briarproject.bramble.api.sync.PacketWriter;
import org.briarproject.bramble.api.sync.PacketWriterFactory;
import org.briarproject.bramble.api.sync.RecordReader;
import org.briarproject.bramble.api.sync.RecordReaderFactory;
import org.briarproject.bramble.api.sync.RecordWriter;
import org.briarproject.bramble.api.sync.RecordWriterFactory;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.transport.StreamContext;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
@@ -33,6 +33,7 @@ import java.util.Collection;
import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -50,9 +51,9 @@ public class SyncIntegrationTest extends BrambleTestCase {
@Inject
StreamWriterFactory streamWriterFactory;
@Inject
PacketReaderFactory packetReaderFactory;
RecordReaderFactory recordReaderFactory;
@Inject
PacketWriterFactory packetWriterFactory;
RecordWriterFactory recordWriterFactory;
@Inject
CryptoComponent crypto;
@@ -77,7 +78,7 @@ public class SyncIntegrationTest extends BrambleTestCase {
streamNumber = 123;
// Create a group
ClientId clientId = new ClientId(TestUtils.getRandomString(5));
byte[] descriptor = new byte[0];
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
Group group = groupFactory.createGroup(clientId, descriptor);
// Add two messages to the group
long timestamp = System.currentTimeMillis();
@@ -98,14 +99,14 @@ public class SyncIntegrationTest extends BrambleTestCase {
headerKey, streamNumber);
OutputStream streamWriter = streamWriterFactory.createStreamWriter(out,
ctx);
PacketWriter packetWriter = packetWriterFactory.createPacketWriter(
RecordWriter recordWriter = recordWriterFactory.createRecordWriter(
streamWriter);
packetWriter.writeAck(new Ack(messageIds));
packetWriter.writeMessage(message.getRaw());
packetWriter.writeMessage(message1.getRaw());
packetWriter.writeOffer(new Offer(messageIds));
packetWriter.writeRequest(new Request(messageIds));
recordWriter.writeAck(new Ack(messageIds));
recordWriter.writeMessage(message.getRaw());
recordWriter.writeMessage(message1.getRaw());
recordWriter.writeOffer(new Offer(messageIds));
recordWriter.writeRequest(new Request(messageIds));
streamWriter.flush();
return out.toByteArray();
@@ -127,31 +128,31 @@ public class SyncIntegrationTest extends BrambleTestCase {
headerKey, streamNumber);
InputStream streamReader = streamReaderFactory.createStreamReader(in,
ctx);
PacketReader packetReader = packetReaderFactory.createPacketReader(
RecordReader recordReader = recordReaderFactory.createRecordReader(
streamReader);
// Read the ack
assertTrue(packetReader.hasAck());
Ack a = packetReader.readAck();
assertTrue(recordReader.hasAck());
Ack a = recordReader.readAck();
assertEquals(messageIds, a.getMessageIds());
// Read the messages
assertTrue(packetReader.hasMessage());
Message m = packetReader.readMessage();
assertTrue(recordReader.hasMessage());
Message m = recordReader.readMessage();
checkMessageEquality(message, m);
assertTrue(packetReader.hasMessage());
m = packetReader.readMessage();
assertTrue(recordReader.hasMessage());
m = recordReader.readMessage();
checkMessageEquality(message1, m);
assertFalse(packetReader.hasMessage());
assertFalse(recordReader.hasMessage());
// Read the offer
assertTrue(packetReader.hasOffer());
Offer o = packetReader.readOffer();
assertTrue(recordReader.hasOffer());
Offer o = recordReader.readOffer();
assertEquals(messageIds, o.getMessageIds());
// Read the request
assertTrue(packetReader.hasRequest());
Request req = packetReader.readRequest();
assertTrue(recordReader.hasRequest());
Request req = recordReader.readRequest();
assertEquals(messageIds, req.getMessageIds());
in.close();

View File

@@ -2,12 +2,9 @@
<manifest
package="org.briarproject.briar"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:versionCode="13"
android:versionName="0.13">
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
<uses-feature android:name="android.hardware.bluetooth"/>
<uses-feature android:name="android.hardware.camera" />
@@ -47,7 +44,7 @@
android:label="@string/crash_report_title"
android:launchMode="singleInstance"
android:process=":briar_error_handler"
android:theme="@style/BriarThemeNoActionBar.Default"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="stateHidden">
</activity>
@@ -70,7 +67,7 @@
<activity
android:name=".android.splash.SplashScreenActivity"
android:theme="@style/BriarThemeNoActionBar.Default"
android:theme="@style/BriarTheme.NoActionBar"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -80,14 +77,14 @@
<activity
android:name=".android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarThemeNoActionBar.Default"
android:theme="@style/BriarTheme.NoActionBar"
android:launchMode="singleTop">
</activity>
<activity
android:name=".android.contact.ConversationActivity"
android:label="@string/app_name"
android:theme="@style/BriarThemeNoActionBar.Default"
android:theme="@style/BriarTheme.NoActionBar"
android:parentActivityName=".android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data
@@ -111,6 +108,7 @@
android:name=".android.privategroup.conversation.GroupActivity"
android:label="@string/app_name"
android:parentActivityName=".android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
@@ -194,6 +192,7 @@
android:name=".android.forum.ForumActivity"
android:label="@string/app_name"
android:parentActivityName=".android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
@@ -245,11 +244,11 @@
<activity
android:name=".android.blog.BlogActivity"
android:parentActivityName=".android.navdrawer.NavDrawerActivity">
android:parentActivityName=".android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.navdrawer.NavDrawerActivity"
/>
android:value=".android.navdrawer.NavDrawerActivity"/>
</activity>
<activity
@@ -308,7 +307,7 @@
<activity
android:name=".android.keyagreement.KeyAgreementActivity"
android:label="@string/add_contact_title"
android:theme="@style/BriarThemeNoActionBar.Default"
android:theme="@style/BriarTheme.NoActionBar"
android:parentActivityName=".android.navdrawer.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

View File

@@ -2,10 +2,12 @@ package org.briarproject.briar.android.activity;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.UiThread;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.DestroyableContext;
@@ -116,4 +118,10 @@ public abstract class BaseActivity extends AppCompatActivity
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
}
@UiThread
public void handleDbException(DbException e) {
supportFinishAfterTransition();
}
}

View File

@@ -3,6 +3,9 @@ package org.briarproject.briar.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.transition.Slide;
import android.transition.Transition;
import android.view.Gravity;
@@ -25,6 +28,7 @@ import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
@SuppressLint("Registered")
public abstract class BriarActivity extends BaseActivity {
@@ -33,8 +37,6 @@ public abstract class BriarActivity extends BaseActivity {
public static final String GROUP_ID = "briar.GROUP_ID";
public static final String GROUP_NAME = "briar.GROUP_NAME";
public static final int REQUEST_PASSWORD = 1;
private static final Logger LOG =
Logger.getLogger(BriarActivity.class.getName());
@@ -94,6 +96,27 @@ public abstract class BriarActivity extends BaseActivity {
window.setBackgroundDrawableResource(android.R.color.transparent);
}
/**
* This should be called after the content view has been added in onCreate()
*
* @param ownLayout true if the custom toolbar brings its own layout
* @return the Toolbar object or null if content view did not contain one
*/
@Nullable
protected Toolbar setUpCustomToolbar(boolean ownLayout) {
// Custom Toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(true);
ab.setDisplayHomeAsUpEnabled(true);
ab.setDisplayShowCustomEnabled(ownLayout);
ab.setDisplayShowTitleEnabled(!ownLayout);
}
return toolbar;
}
protected void signOut(final boolean removeFromRecentApps) {
briarController.signOut(new UiResultHandler<Void>(this) {
@Override

View File

@@ -0,0 +1,14 @@
package org.briarproject.briar.android.activity;
public interface RequestCodes {
int REQUEST_PASSWORD = 1;
int REQUEST_BLUETOOTH = 2;
int REQUEST_INTRODUCTION = 3;
int REQUEST_GROUP_INVITE = 4;
int REQUEST_SHARE_FORUM = 5;
int REQUEST_WRITE_BLOG_POST = 6;
int REQUEST_SHARE_BLOG = 7;
int REQUEST_RINGTONE = 8;
}

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.LayoutInflater;
@@ -20,7 +22,7 @@ import javax.annotation.Nullable;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.util.UiUtils.MIN_RESOLUTION;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@UiThread
@MethodsNotNullByDefault
@@ -32,8 +34,9 @@ abstract class BasePostFragment extends BaseFragment {
private static final Logger LOG =
Logger.getLogger(BasePostFragment.class.getName());
private final Handler handler = new Handler(Looper.getMainLooper());
protected MessageId postId;
private View view;
private ProgressBar progressBar;
private BlogPostViewHolder ui;
private BlogPostItem post;
@@ -50,7 +53,7 @@ abstract class BasePostFragment extends BaseFragment {
if (p == null) throw new IllegalStateException("No post ID in args");
postId = new MessageId(p);
view = inflater.inflate(R.layout.fragment_blog_post, container,
View view = inflater.inflate(R.layout.fragment_blog_post, container,
false);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
@@ -83,21 +86,19 @@ abstract class BasePostFragment extends BaseFragment {
refresher = new Runnable() {
@Override
public void run() {
if (ui == null) return;
LOG.info("Updating Content...");
ui.updateDate(post.getTimestamp());
view.postDelayed(refresher, MIN_RESOLUTION);
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
};
LOG.info("Adding Handler Callback");
view.postDelayed(refresher, MIN_RESOLUTION);
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
private void stopPeriodicUpdate() {
if (refresher != null && ui != null) {
if (refresher != null) {
LOG.info("Removing Handler Callback");
view.removeCallbacks(refresher);
handler.removeCallbacks(refresher);
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.Toolbar;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -21,9 +22,6 @@ import javax.inject.Inject;
public class BlogActivity extends BriarActivity
implements BaseFragmentListener {
static final int REQUEST_WRITE_POST = 2;
static final int REQUEST_SHARE = 3;
@Inject
BlogController blogController;
@@ -38,12 +36,12 @@ public class BlogActivity extends BriarActivity
final GroupId groupId = new GroupId(b);
blogController.setGroupId(groupId);
setContentView(R.layout.activity_fragment_container);
setContentView(R.layout.activity_fragment_container_toolbar);
Toolbar toolbar = setUpCustomToolbar(false);
// Open Sharing Status on ActionBar click
View actionBar = findViewById(R.id.action_bar);
if (actionBar != null) {
actionBar.setOnClickListener(
// Open Sharing Status on Toolbar click
if (toolbar != null) {
toolbar.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {

View File

@@ -46,8 +46,8 @@ import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BlogActivity.REQUEST_SHARE;
import static org.briarproject.briar.android.blog.BlogActivity.REQUEST_WRITE_POST;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
import static org.briarproject.briar.android.controller.SharingController.SharingListener;
@UiThread
@@ -145,13 +145,13 @@ public class BlogFragment extends BaseFragment
Intent i = new Intent(getActivity(),
WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_WRITE_POST);
startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
return true;
case R.id.action_blog_share:
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE);
startActivityForResult(i2, REQUEST_SHARE_BLOG);
return true;
case R.id.action_blog_sharing_status:
Intent i3 = new Intent(getActivity(),
@@ -172,10 +172,10 @@ public class BlogFragment extends BaseFragment
public void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_WRITE_POST && result == RESULT_OK) {
if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) {
displaySnackbar(R.string.blogs_blog_post_created, true);
loadBlogPosts(true);
} else if (request == REQUEST_SHARE && result == RESULT_OK) {
} else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
displaySnackbar(R.string.blogs_sharing_snackbar, false);
}
}
@@ -205,8 +205,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
handleDbException(exception);
}
}
);
@@ -234,8 +233,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
handleDbException(exception);
}
});
}
@@ -254,8 +252,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
handleDbException(exception);
}
});
}
@@ -277,8 +274,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
handleDbException(exception);
}
});
}
@@ -373,8 +369,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
handleDbException(exception);
}
});
}

View File

@@ -55,8 +55,7 @@ public class BlogPostFragment extends BasePostFragment {
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
handleDbException(exception);
}
});
}

View File

@@ -36,7 +36,7 @@ import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BlogActivity.REQUEST_WRITE_POST;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@UiThread
@MethodsNotNullByDefault
@@ -96,7 +96,7 @@ public class FeedFragment extends BaseFragment implements
super.onActivityResult(requestCode, resultCode, data);
// The BlogPostAddedEvent arrives when the controller is not listening
if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
if (requestCode == REQUEST_WRITE_BLOG_POST && resultCode == RESULT_OK) {
showSnackBar(R.string.blogs_blog_post_created);
}
}
@@ -105,6 +105,7 @@ public class FeedFragment extends BaseFragment implements
public void onStart() {
super.onStart();
feedController.onStart();
list.startPeriodicUpdate();
loadPersonalBlog();
loadBlogPosts(false);
}
@@ -129,7 +130,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
handleDbException(exception);
}
});
}
@@ -153,11 +154,10 @@ public class FeedFragment extends BaseFragment implements
}
@Override
public void onExceptionUi(DbException e) {
// TODO: Decide how to handle errors in the UI
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
list.startPeriodicUpdate();
}
@Override
@@ -174,7 +174,7 @@ public class FeedFragment extends BaseFragment implements
Intent i1 =
new Intent(getActivity(), WriteBlogPostActivity.class);
i1.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivityForResult(i1, REQUEST_WRITE_POST);
startActivityForResult(i1, REQUEST_WRITE_BLOG_POST);
return true;
case R.id.action_rss_feeds_import:
Intent i2 =
@@ -211,7 +211,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
handleDbException(exception);
}
}
);

View File

@@ -79,7 +79,7 @@ public class FeedPostFragment extends BasePostFragment {
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
handleDbException(exception);
}
});
}

View File

@@ -99,8 +99,7 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
@Override
public void onExceptionUi(DbException exception) {
// TODO
finish();
handleDbException(exception);
}
});
}
@@ -130,8 +129,7 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
// do nothing, this fragment is gone already
handleDbException(exception);
}
});
finish();

View File

@@ -6,7 +6,6 @@ import android.os.Bundle;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.ActionMenuView;
import android.support.v7.widget.LinearLayoutManager;
@@ -50,8 +49,11 @@ import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.contact.ConversationAdapter.ConversationListener;
import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
@@ -77,6 +79,8 @@ import org.briarproject.briar.api.sharing.InvitationRequest;
import org.briarproject.briar.api.sharing.InvitationResponse;
import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
import org.thoughtcrime.securesms.components.util.FutureTaskListener;
import org.thoughtcrime.securesms.components.util.ListenableFutureTask;
import java.util.ArrayList;
import java.util.Collection;
@@ -84,8 +88,10 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -101,6 +107,7 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
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;
@@ -115,7 +122,6 @@ public class ConversationActivity extends BriarActivity
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
private static final int REQUEST_CODE_INTRODUCTION = 2;
private static final String SHOW_ONBOARDING_INTRODUCTION =
"showOnboardingIntroduction";
@@ -137,6 +143,18 @@ public class ConversationActivity extends BriarActivity
private BriarRecyclerView list;
private TextInputView textInputView;
private final ListenableFutureTask<String> contactNameTask =
new ListenableFutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
Contact c = contactManager.getContact(contactId);
contactName = c.getAuthor().getName();
return c.getAuthor().getName();
}
});
private final AtomicBoolean contactNameTaskStarted =
new AtomicBoolean(false);
// Fields that are accessed from background threads must be volatile
@Inject
volatile ContactManager contactManager;
@@ -158,8 +176,11 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager;
private volatile ContactId contactId;
@Nullable
private volatile String contactName;
@Nullable
private volatile AuthorId contactAuthorId;
@Nullable
private volatile GroupId messagingGroupId;
@SuppressWarnings("ConstantConditions")
@@ -176,21 +197,13 @@ public class ConversationActivity extends BriarActivity
setContentView(R.layout.activity_conversation);
// Custom Toolbar
toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar = setUpCustomToolbar(true);
if (toolbar != null) {
toolbarAvatar =
(CircleImageView) toolbar.findViewById(R.id.contactAvatar);
toolbarStatus =
(ImageView) toolbar.findViewById(R.id.contactStatus);
toolbarTitle = (TextView) toolbar.findViewById(R.id.contactName);
setSupportActionBar(toolbar);
}
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(true);
ab.setDisplayHomeAsUpEnabled(true);
ab.setDisplayShowCustomEnabled(true);
ab.setDisplayShowTitleEnabled(false);
}
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
@@ -215,7 +228,7 @@ public class ConversationActivity extends BriarActivity
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_CODE_INTRODUCTION && result == RESULT_OK) {
if (request == REQUEST_INTRODUCTION && result == RESULT_OK) {
Snackbar snackbar = Snackbar.make(list, R.string.introduction_sent,
Snackbar.LENGTH_SHORT);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
@@ -229,8 +242,8 @@ public class ConversationActivity extends BriarActivity
eventBus.addListener(this);
notificationManager.blockContactNotification(contactId);
notificationManager.clearContactNotification(contactId);
loadContactDetails();
loadMessages();
displayContactOnlineStatus();
loadContactDetailsAndMessages();
list.startPeriodicUpdate();
}
@@ -265,7 +278,7 @@ public class ConversationActivity extends BriarActivity
if (contactId == null) return false;
Intent intent = new Intent(this, IntroductionActivity.class);
intent.putExtra(CONTACT_ID, contactId.getInt());
startActivityForResult(intent, REQUEST_CODE_INTRODUCTION);
startActivityForResult(intent, REQUEST_INTRODUCTION);
return true;
case R.id.action_social_remove_person:
askToRemoveContact();
@@ -275,7 +288,7 @@ public class ConversationActivity extends BriarActivity
}
}
private void loadContactDetails() {
private void loadContactDetailsAndMessages() {
runOnDbThread(new Runnable() {
@Override
public void run() {
@@ -289,6 +302,7 @@ public class ConversationActivity extends BriarActivity
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading contact took " + duration + " ms");
loadMessages();
displayContactDetails();
} catch (NoSuchContactException e) {
finishOnUiThread();
@@ -304,10 +318,18 @@ public class ConversationActivity extends BriarActivity
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
//noinspection ConstantConditions
toolbarAvatar.setImageDrawable(
new IdenticonDrawable(contactAuthorId.getBytes()));
toolbarTitle.setText(contactName);
}
});
}
private void displayContactOnlineStatus() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
if (connectionRegistry.isConnected(contactId)) {
toolbarStatus.setImageDrawable(ContextCompat
.getDrawable(ConversationActivity.this,
@@ -347,7 +369,8 @@ public class ConversationActivity extends BriarActivity
groupInvitationManager
.getInvitationMessages(contactId);
List<InvitationMessage> invitations = new ArrayList<>(
forumInvitations.size() + blogInvitations.size());
forumInvitations.size() + blogInvitations.size() +
groupInvitations.size());
invitations.addAll(forumInvitations);
invitations.addAll(blogInvitations);
invitations.addAll(groupInvitations);
@@ -390,6 +413,12 @@ public class ConversationActivity extends BriarActivity
});
}
/**
* Creates ConversationItems from headers loaded from the database.
*
* Attention: Call this only after contactName has been initialized.
*/
@SuppressWarnings("ConstantConditions")
private List<ConversationItem> createItems(
Collection<PrivateMessageHeader> headers,
Collection<IntroductionMessage> introductions,
@@ -468,18 +497,6 @@ public class ConversationActivity extends BriarActivity
});
}
private void addConversationItem(final ConversationItem item) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
adapter.incrementRevision();
adapter.add(item);
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
}
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactRemovedEvent) {
@@ -512,13 +529,13 @@ public class ConversationActivity extends BriarActivity
ContactConnectedEvent c = (ContactConnectedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact connected");
displayContactDetails();
displayContactOnlineStatus();
}
} else if (e instanceof ContactDisconnectedEvent) {
ContactDisconnectedEvent c = (ContactDisconnectedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact disconnected");
displayContactDetails();
displayContactOnlineStatus();
}
} else if (e instanceof IntroductionRequestReceivedEvent) {
IntroductionRequestReceivedEvent event =
@@ -526,9 +543,7 @@ public class ConversationActivity extends BriarActivity
if (event.getContactId().equals(contactId)) {
LOG.info("Introduction request received, adding...");
IntroductionRequest ir = event.getIntroductionRequest();
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addConversationItem(item);
handleIntroductionRequest(ir);
}
} else if (e instanceof IntroductionResponseReceivedEvent) {
IntroductionResponseReceivedEvent event =
@@ -536,9 +551,7 @@ public class ConversationActivity extends BriarActivity
if (event.getContactId().equals(contactId)) {
LOG.info("Introduction response received, adding...");
IntroductionResponse ir = event.getIntroductionResponse();
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addConversationItem(item);
handleIntroductionResponse(ir);
}
} else if (e instanceof InvitationRequestReceivedEvent) {
InvitationRequestReceivedEvent event =
@@ -546,9 +559,7 @@ public class ConversationActivity extends BriarActivity
if (event.getContactId().equals(contactId)) {
LOG.info("Invitation received, adding...");
InvitationRequest ir = event.getRequest();
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addConversationItem(item);
handleInvitationRequest(ir);
}
} else if (e instanceof InvitationResponseReceivedEvent) {
InvitationResponseReceivedEvent event =
@@ -556,13 +567,127 @@ public class ConversationActivity extends BriarActivity
if (event.getContactId().equals(contactId)) {
LOG.info("Invitation response received, adding...");
InvitationResponse ir = event.getResponse();
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addConversationItem(item);
handleInvitationResponse(ir);
}
}
}
private void addConversationItem(final ConversationItem item) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
adapter.incrementRevision();
adapter.add(item);
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
}
});
}
private void handleIntroductionRequest(final IntroductionRequest m) {
getContactNameTask().addListener(new FutureTaskListener<String>() {
@Override
public void onSuccess(final String contactName) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
ConversationItem item = ConversationItem
.from(ConversationActivity.this, contactName,
m);
addConversationItem(item);
}
});
}
@Override
public void onFailure(final Throwable exception) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
handleDbException((DbException) exception);
}
});
}
});
}
private void handleIntroductionResponse(final IntroductionResponse m) {
getContactNameTask().addListener(new FutureTaskListener<String>() {
@Override
public void onSuccess(final String contactName) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
ConversationItem item = ConversationItem
.from(ConversationActivity.this, contactName,
m);
addConversationItem(item);
}
});
}
@Override
public void onFailure(final Throwable exception) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
handleDbException((DbException) exception);
}
});
}
});
}
private void handleInvitationRequest(final InvitationRequest m) {
getContactNameTask().addListener(new FutureTaskListener<String>() {
@Override
public void onSuccess(final String contactName) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
ConversationItem item = ConversationItem
.from(ConversationActivity.this, contactName,
m);
addConversationItem(item);
}
});
}
@Override
public void onFailure(final Throwable exception) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
handleDbException((DbException) exception);
}
});
}
});
}
private void handleInvitationResponse(final InvitationResponse m) {
getContactNameTask().addListener(new FutureTaskListener<String>() {
@Override
public void onSuccess(final String contactName) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
ConversationItem item = ConversationItem
.from(ConversationActivity.this, contactName,
m);
addConversationItem(item);
}
});
}
@Override
public void onFailure(final Throwable exception) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
handleDbException((DbException) exception);
}
});
}
});
}
private void markMessages(final Collection<MessageId> messageIds,
final boolean sent, final boolean seen) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@@ -623,6 +748,7 @@ public class ConversationActivity extends BriarActivity
@Override
public void run() {
try {
//noinspection ConstantConditions init in loadGroupId()
storeMessage(privateMessageFactory.createPrivateMessage(
messagingGroupId, timestamp, body), body);
} catch (FormatException e) {
@@ -854,11 +980,33 @@ public class ConversationActivity extends BriarActivity
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@UiThread
@Override
public void openRequestedShareable(ConversationRequestItem item) {
if (item.getRequestedGroupId() == null)
throw new IllegalArgumentException();
Intent i;
switch (item.getRequestType()) {
case FORUM:
i = new Intent(this, ForumActivity.class);
break;
case BLOG:
i = new Intent(this, BlogActivity.class);
break;
case GROUP:
i = new Intent(this, GroupActivity.class);
break;
default:
throw new IllegalArgumentException("Unknown Request Type");
}
i.putExtra(GROUP_ID, item.getRequestedGroupId().getBytes());
startActivity(i);
}
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException, FormatException {
@@ -872,13 +1020,13 @@ public class ConversationActivity extends BriarActivity
@DatabaseExecutor
private void respondToForumRequest(SessionId id, boolean accept)
throws DbException {
forumSharingManager.respondToInvitation(id, accept);
forumSharingManager.respondToInvitation(contactId, id, accept);
}
@DatabaseExecutor
private void respondToBlogRequest(SessionId id, boolean accept)
throws DbException {
blogSharingManager.respondToInvitation(id, accept);
blogSharingManager.respondToInvitation(contactId, id, accept);
}
@DatabaseExecutor
@@ -902,4 +1050,10 @@ public class ConversationActivity extends BriarActivity
});
}
private ListenableFutureTask<String> getContactNameTask() {
if (!contactNameTaskStarted.getAndSet(true))
runOnDbThread(contactNameTask);
return contactNameTask;
}
}

View File

@@ -139,6 +139,9 @@ class ConversationAdapter
void onItemVisible(ConversationItem item);
void respondToRequest(ConversationRequestItem item, boolean accept);
void openRequestedShareable(ConversationRequestItem item);
}
}

View File

@@ -101,9 +101,6 @@ abstract class ConversationItem {
text = ctx.getString(
R.string.introduction_request_answered_received,
contactName, ir.getName());
return new ConversationNoticeInItem(ir.getMessageId(),
ir.getGroupId(), text, ir.getMessage(), ir.getTimestamp(),
ir.isRead());
} else if (ir.contactExists()){
text = ctx.getString(
R.string.introduction_request_exists_received,
@@ -114,8 +111,8 @@ abstract class ConversationItem {
}
return new ConversationRequestItem(ir.getMessageId(),
ir.getGroupId(), INTRODUCTION, ir.getSessionId(), text,
ir.getMessage(), ir.getTimestamp(), ir.isRead(),
ir.wasAnswered());
ir.getMessage(), ir.getTimestamp(), ir.isRead(), null,
ir.wasAnswered(), false);
}
}
@@ -173,8 +170,7 @@ abstract class ConversationItem {
} else if (ir instanceof GroupInvitationRequest) {
text = ctx.getString(
R.string.groups_invitations_invitation_sent,
contactName,
((GroupInvitationRequest) ir).getGroupName());
contactName, ir.getShareable().getName());
} else {
throw new IllegalArgumentException("Unknown InvitationRequest");
}
@@ -197,20 +193,16 @@ abstract class ConversationItem {
} else if (ir instanceof GroupInvitationRequest) {
text = ctx.getString(
R.string.groups_invitations_invitation_received,
contactName,
((GroupInvitationRequest) ir).getGroupName());
contactName, ir.getShareable().getName());
type = GROUP;
} else {
throw new IllegalArgumentException("Unknown InvitationRequest");
}
if (!ir.isAvailable()) {
return new ConversationNoticeInItem(ir.getId(), ir.getGroupId(),
text, ir.getMessage(), ir.getTimestamp(), ir.isRead());
}
return new ConversationRequestItem(ir.getId(),
ir.getGroupId(), type, ir.getSessionId(), text,
ir.getMessage(), ir.getTimestamp(), ir.isRead(),
!ir.isAvailable());
ir.getShareable().getId(), !ir.isAvailable(),
ir.canBeOpened());
}
}

View File

@@ -17,18 +17,23 @@ class ConversationRequestItem extends ConversationNoticeInItem {
enum RequestType { INTRODUCTION, FORUM, BLOG, GROUP }
@Nullable
private final GroupId requestedGroupId;
private final RequestType requestType;
private final SessionId sessionId;
private boolean answered;
private final boolean answered, canBeOpened;
ConversationRequestItem(MessageId id, GroupId groupId,
RequestType requestType, SessionId sessionId, String text,
@Nullable String msgText, long time, boolean read,
boolean answered) {
@Nullable GroupId requestedGroupId, boolean answered,
boolean canBeOpened) {
super(id, groupId, text, msgText, time, read);
this.requestType = requestType;
this.sessionId = sessionId;
this.requestedGroupId = requestedGroupId;
this.answered = answered;
this.canBeOpened = canBeOpened;
}
RequestType getRequestType() {
@@ -39,12 +44,17 @@ class ConversationRequestItem extends ConversationNoticeInItem {
return sessionId;
}
@Nullable
public GroupId getRequestedGroupId() {
return requestedGroupId;
}
boolean wasAnswered() {
return answered;
}
void setAnswered(boolean answered) {
this.answered = answered;
public boolean canBeOpened() {
return canBeOpened;
}
@LayoutRes

View File

@@ -32,15 +32,25 @@ class ConversationRequestViewHolder extends ConversationNoticeInViewHolder {
final ConversationRequestItem item =
(ConversationRequestItem) conversationItem;
if (item.wasAnswered()) {
if (item.wasAnswered() && item.canBeOpened()) {
acceptButton.setVisibility(VISIBLE);
acceptButton.setText(R.string.open);
acceptButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.openRequestedShareable(item);
}
});
declineButton.setVisibility(GONE);
} else if (item.wasAnswered()) {
acceptButton.setVisibility(GONE);
declineButton.setVisibility(GONE);
} else {
acceptButton.setVisibility(VISIBLE);
acceptButton.setText(R.string.accept);
acceptButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
item.setAnswered(true);
listener.respondToRequest(item, true);
}
});
@@ -48,7 +58,6 @@ class ConversationRequestViewHolder extends ConversationNoticeInViewHolder {
declineButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
item.setAnswered(true);
listener.respondToRequest(item, false);
}
});

View File

@@ -130,8 +130,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
@Override
public void onExceptionUi(DbException exception) {
// TODO error handling
finish();
handleDbException(exception);
}
});
}

View File

@@ -4,10 +4,10 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -36,6 +36,7 @@ import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
@MethodsNotNullByDefault
@@ -44,8 +45,6 @@ public class ForumActivity extends
ThreadListActivity<Forum, ThreadItemAdapter<ForumItem>, ForumItem, ForumPostHeader>
implements ForumListener {
private static final int REQUEST_FORUM_SHARED = 3;
@Inject
ForumController forumController;
@@ -63,15 +62,16 @@ public class ForumActivity extends
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
Toolbar toolbar = setUpCustomToolbar(false);
Intent i = getIntent();
String groupName = i.getStringExtra(GROUP_NAME);
if (groupName != null) setTitle(groupName);
else loadNamedGroup();
// Open Sharing Status on ActionBar click
View actionBar = findViewById(R.id.action_bar);
if (actionBar != null) {
actionBar.setOnClickListener(
// Open member list on Toolbar click
if (toolbar != null) {
toolbar.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -89,12 +89,6 @@ public class ForumActivity extends
setTitle(forum.getName());
}
@Override
@LayoutRes
protected int getLayout() {
return R.layout.activity_forum;
}
@Override
protected ThreadItemAdapter<ForumItem> createAdapter(
LinearLayoutManager layoutManager) {
@@ -105,8 +99,8 @@ public class ForumActivity extends
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
displaySnackbarShort(R.string.forum_shared_snackbar);
if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) {
displaySnackbar(R.string.forum_shared_snackbar);
}
}
@@ -130,7 +124,7 @@ public class ForumActivity extends
Intent i2 = new Intent(this, ShareForumActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_FORUM_SHARED);
startActivityForResult(i2, REQUEST_SHARE_FORUM);
return true;
case R.id.action_forum_sharing_status:
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
@@ -157,12 +151,6 @@ public class ForumActivity extends
return R.string.forum_new_entry_posted;
}
@Override
@StringRes
protected int getItemReceivedString() {
return R.string.forum_new_entry_received;
}
private void showUnsubscribeDialog() {
OnClickListener okListener = new OnClickListener() {
@Override
@@ -190,8 +178,7 @@ public class ForumActivity extends
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}

View File

@@ -8,6 +8,7 @@ import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.activity.ActivityComponent;
@@ -71,6 +72,9 @@ public abstract class BaseFragment extends Fragment
@UiThread
void showNextFragment(BaseFragment f);
@UiThread
void handleDbException(DbException e);
}
@CallSuper
@@ -95,4 +99,9 @@ public abstract class BaseFragment extends Fragment
listener.showNextFragment(f);
}
@UiThread
protected void handleDbException(DbException e) {
listener.handleDbException(e);
}
}

View File

@@ -25,6 +25,7 @@ import javax.inject.Inject;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.CONNECTED;
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.DETAILS;
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.WAIT_FOR_CONTACT;
@@ -32,8 +33,6 @@ import static org.briarproject.briar.android.invitation.ConfirmationCodeView.Con
public class AddContactActivity extends BriarActivity
implements InvitationListener {
static final int REQUEST_BLUETOOTH = 1;
private static final Logger LOG =
Logger.getLogger(AddContactActivity.class.getName());

View File

@@ -11,7 +11,7 @@ import org.briarproject.briar.R;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static org.briarproject.briar.android.invitation.AddContactActivity.REQUEST_BLUETOOTH;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
class ChooseIdentityView extends AddContactView implements OnClickListener {

View File

@@ -12,7 +12,7 @@ import org.briarproject.briar.R;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static org.briarproject.briar.android.invitation.AddContactActivity.REQUEST_BLUETOOTH;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
class ErrorView extends AddContactView implements OnClickListener {

View File

@@ -60,7 +60,7 @@ public class KeyAgreementActivity extends BriarActivity implements
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_plain);
setContentView(R.layout.activity_fragment_container_toolbar);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);

View File

@@ -86,6 +86,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
private BluetoothStateReceiver receiver;
private boolean gotRemotePayload, waitingForBluetooth;
private volatile boolean gotLocalPayload;
private KeyAgreementTask task;
public static ShowQrCodeFragment newInstance() {
@@ -202,6 +203,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
statusView.setVisibility(INVISIBLE);
cameraView.setVisibility(VISIBLE);
gotRemotePayload = false;
gotLocalPayload = false;
startListening();
}
@@ -227,6 +229,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
public void eventOccurred(Event e) {
if (e instanceof KeyAgreementListeningEvent) {
KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
gotLocalPayload = true;
setQrCode(event.getLocalPayload());
} else if (e instanceof KeyAgreementFailedEvent) {
keyAgreementFailed();
@@ -341,6 +344,10 @@ public class ShowQrCodeFragment extends BaseEventFragment
@Override
public void run() {
LOG.info("Got result from decoder");
// Ignore results until the KeyAgreementTask is ready
if (!gotLocalPayload) {
return;
}
if (!gotRemotePayload) {
gotRemotePayload = true;
qrCodeScanned(result.getText());

View File

@@ -19,6 +19,7 @@ import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
@@ -74,13 +75,13 @@ public class NavDrawerActivity extends BriarActivity implements
exitIfStartupFailed(intent);
// TODO don't create new instances if they are on the stack (#606)
if (intent.getBooleanExtra(INTENT_GROUPS, false)) {
startFragment(GroupListFragment.newInstance());
startFragment(GroupListFragment.newInstance(), R.id.nav_btn_groups);
} else if (intent.getBooleanExtra(INTENT_FORUMS, false)) {
startFragment(ForumListFragment.newInstance());
startFragment(ForumListFragment.newInstance(), R.id.nav_btn_forums);
} else if (intent.getBooleanExtra(INTENT_CONTACTS, false)) {
startFragment(ContactListFragment.newInstance());
startFragment(ContactListFragment.newInstance(), R.id.nav_btn_contacts);
} else if (intent.getBooleanExtra(INTENT_BLOGS, false)) {
startFragment(FeedFragment.newInstance());
startFragment(FeedFragment.newInstance(), R.id.nav_btn_blogs);
}
setIntent(null);
}
@@ -116,8 +117,7 @@ public class NavDrawerActivity extends BriarActivity implements
transportsView.setAdapter(transportsAdapter);
if (state == null) {
navigation.setCheckedItem(R.id.nav_btn_contacts);
startFragment(ContactListFragment.newInstance());
startFragment(ContactListFragment.newInstance(), R.id.nav_btn_contacts);
}
if (getIntent() != null) {
onNewIntent(getIntent());
@@ -194,8 +194,7 @@ public class NavDrawerActivity extends BriarActivity implements
* exiting. This models the typical Google navigation behaviour such
* as in Gmail/Inbox.
*/
navigation.setCheckedItem(R.id.nav_btn_contacts);
startFragment(ContactListFragment.newInstance());
startFragment(ContactListFragment.newInstance(), R.id.nav_btn_contacts);
} else {
super.onBackPressed();
}
@@ -220,6 +219,11 @@ public class NavDrawerActivity extends BriarActivity implements
super.signOut();
}
private void startFragment(BaseFragment fragment, int itemId){
navigation.setCheckedItem(itemId);
startFragment(fragment);
}
private void startFragment(BaseFragment fragment) {
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
startFragment(fragment, false);
@@ -246,6 +250,11 @@ public class NavDrawerActivity extends BriarActivity implements
POP_BACK_STACK_INCLUSIVE);
}
@Override
public void handleDbException(DbException e) {
// Do nothing for now
}
private void initializeTransports(final LayoutInflater inflater) {
transports = new ArrayList<>(3);

View File

@@ -4,10 +4,10 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -37,6 +37,7 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.view.View.GONE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_GROUP_INVITE;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
@MethodsNotNullByDefault
@@ -45,8 +46,6 @@ public class GroupActivity extends
ThreadListActivity<PrivateGroup, GroupMessageAdapter, GroupMessageItem, GroupMessageHeader>
implements GroupListener, OnClickListener {
private final static int REQUEST_INVITE = 2;
@Inject
GroupController controller;
@@ -68,15 +67,16 @@ public class GroupActivity extends
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
Toolbar toolbar = setUpCustomToolbar(false);
Intent i = getIntent();
String groupName = i.getStringExtra(GROUP_NAME);
if (groupName != null) setTitle(groupName);
loadNamedGroup();
// Open member list on ActionBar click
View actionBar = findViewById(R.id.action_bar);
if (actionBar != null) {
actionBar.setOnClickListener(
// Open member list on Toolbar click
if (toolbar != null) {
toolbar.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -91,12 +91,6 @@ public class GroupActivity extends
setGroupEnabled(false);
}
@Override
@LayoutRes
protected int getLayout() {
return R.layout.activity_forum;
}
@Override
protected GroupMessageAdapter createAdapter(
LinearLayoutManager layoutManager) {
@@ -115,8 +109,7 @@ public class GroupActivity extends
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}
@@ -135,8 +128,7 @@ public class GroupActivity extends
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}
@@ -176,7 +168,7 @@ public class GroupActivity extends
case R.id.action_group_invite:
Intent i3 = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_INVITE);
startActivityForResult(i3, REQUEST_GROUP_INVITE);
return true;
case R.id.action_group_leave:
showLeaveGroupDialog();
@@ -190,8 +182,8 @@ public class GroupActivity extends
@Override
protected void onActivityResult(int request, int result, Intent data) {
if (request == REQUEST_INVITE && result == RESULT_OK) {
displaySnackbarShort(R.string.groups_invitation_sent);
if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) {
displaySnackbar(R.string.groups_invitation_sent);
} else super.onActivityResult(request, result, data);
}
@@ -206,12 +198,6 @@ public class GroupActivity extends
return R.string.groups_message_sent;
}
@Override
@StringRes
protected int getItemReceivedString() {
return R.string.groups_message_received;
}
@Override
public void onReplyClick(GroupMessageItem item) {
if (!isDissolved) super.onReplyClick(item);
@@ -274,8 +260,7 @@ public class GroupActivity extends
// GroupRemovedEvent being fired
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}

View File

@@ -31,9 +31,8 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
@LayoutRes
@Override
public int getItemViewType(int position) {
GroupMessageItem item = getVisibleItem(position);
if (item != null) return item.getLayout();
return R.layout.list_item_thread;
GroupMessageItem item = items.get(position);
return item.getLayout();
}
@Override
@@ -58,7 +57,7 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
GroupMessageItem item = items.get(position);
if (item instanceof JoinMessageItem) {
((JoinMessageItem) item).setVisibility(v);
notifyItemChanged(getVisiblePos(item), item);
notifyItemChanged(findItemPosition(item), item);
}
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.privategroup.conversation;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.identity.Author;
@@ -20,9 +21,8 @@ class GroupMessageItem extends ThreadItem {
private final GroupId groupId;
private GroupMessageItem(MessageId messageId, GroupId groupId,
MessageId parentId,
String text, long timestamp, Author author, Status status,
boolean isRead) {
@Nullable MessageId parentId, String text, long timestamp,
Author author, Status status, boolean isRead) {
super(messageId, parentId, text, timestamp, author, status, isRead);
this.groupId = groupId;
}

View File

@@ -27,11 +27,6 @@ class JoinMessageItem extends GroupMessageItem {
return 0;
}
@Override
public boolean hasDescendants() {
return false;
}
@Override
@LayoutRes
public int getLayout() {

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder;
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import static org.briarproject.bramble.api.identity.Author.Status.OURSELVES;
@@ -41,10 +40,9 @@ class JoinMessageItemViewHolder
}
@Override
public void bind(ThreadItemAdapter<GroupMessageItem> adapter,
ThreadItemListener<GroupMessageItem> listener,
GroupMessageItem item, int pos) {
super.bind(adapter, listener, item, pos);
public void bind(GroupMessageItem item,
ThreadItemListener<GroupMessageItem> listener) {
super.bind(item, listener);
if (isCreator) bindForCreator((JoinMessageItem) item);
else bind((JoinMessageItem) item);

View File

@@ -43,9 +43,8 @@ public abstract class BaseGroupInviteActivity
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
setResult(RESULT_CANCELED);
finish();
handleDbException(exception);
}
});
return true;

View File

@@ -58,8 +58,7 @@ public class CreateGroupActivity extends BaseGroupInviteActivity implements
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}

View File

@@ -132,7 +132,7 @@ public class GroupListFragment extends BaseFragment implements
// result handled by GroupRemovedEvent and onGroupRemoved()
@Override
public void onExceptionUi(DbException exception) {
// TODO handle error
handleDbException(exception);
}
});
}
@@ -202,7 +202,7 @@ public class GroupListFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
// TODO handle this error
handleDbException(exception);
}
});
}
@@ -224,8 +224,7 @@ public class GroupListFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
// TODO handle this error
finish();
handleDbException(exception);
}
});
}

View File

@@ -67,8 +67,7 @@ public class GroupMemberListActivity extends BriarActivity {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
list.startPeriodicUpdate();

View File

@@ -80,8 +80,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}
@@ -132,7 +131,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
handleDbException(exception);
}
});
}
@@ -149,8 +148,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
supportFinishAfterTransition();

View File

@@ -45,13 +45,13 @@ import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SettingsFragment extends PreferenceFragmentCompat
implements EventListener, Preference.OnPreferenceChangeListener {
private static final int REQUEST_RINGTONE = 2;
public static final String SETTINGS_NAMESPACE = "android-ui";
public static final String PREF_NOTIFY_GROUP = "notifyGroupMessages";
public static final String PREF_NOTIFY_BLOG = "notifyBlogPosts";

View File

@@ -97,8 +97,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}
@@ -111,8 +110,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
handleDbException(exception);
}
});
}

View File

@@ -56,10 +56,10 @@ public class ShareBlogActivity extends ShareActivity {
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
Toast.makeText(ShareBlogActivity.this,
R.string.blogs_sharing_error, LENGTH_SHORT)
.show();
handleDbException(exception);
}
});

View File

@@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.messaging.ConversationManager;
import java.util.Collection;
import java.util.concurrent.Executor;
@@ -22,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@Immutable
@NotNullByDefault
@@ -31,14 +34,19 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
private final static Logger LOG =
Logger.getLogger(ShareBlogControllerImpl.class.getName());
private final ConversationManager conversationManager;
private final BlogSharingManager blogSharingManager;
private final Clock clock;
@Inject
ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
BlogSharingManager blogSharingManager) {
ConversationManager conversationManager,
BlogSharingManager blogSharingManager, Clock clock) {
super(dbExecutor, lifecycleManager, contactManager);
this.conversationManager = conversationManager;
this.blogSharingManager = blogSharingManager;
this.clock = clock;
}
@Override
@@ -48,15 +56,19 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
@Override
public void share(final GroupId g, final Collection<ContactId> contacts,
final String msg,
final String message,
final ExceptionHandler<DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
String msg = isNullOrEmpty(message) ? null : message;
for (ContactId c : contacts) {
try {
blogSharingManager.sendInvitation(g, c, msg);
long time = Math.max(clock.currentTimeMillis(),
conversationManager.getGroupCount(c)
.getLatestMsgTime() + 1);
blogSharingManager.sendInvitation(g, c, msg, time);
} catch (NoSuchContactException | NoSuchGroupException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);

View File

@@ -56,10 +56,10 @@ public class ShareForumActivity extends ShareActivity {
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
Toast.makeText(ShareForumActivity.this,
R.string.forum_share_error, LENGTH_SHORT)
.show();
handleDbException(exception);
}
});
}

View File

@@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.messaging.ConversationManager;
import java.util.Collection;
import java.util.concurrent.Executor;
@@ -22,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@Immutable
@NotNullByDefault
@@ -31,14 +34,19 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
private final static Logger LOG =
Logger.getLogger(ShareForumControllerImpl.class.getName());
private final ConversationManager conversationManager;
private final ForumSharingManager forumSharingManager;
private final Clock clock;
@Inject
ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
ForumSharingManager forumSharingManager) {
ConversationManager conversationManager,
ForumSharingManager forumSharingManager, Clock clock) {
super(dbExecutor, lifecycleManager, contactManager);
this.conversationManager = conversationManager;
this.forumSharingManager = forumSharingManager;
this.clock = clock;
}
@Override
@@ -48,15 +56,19 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
@Override
public void share(final GroupId g, final Collection<ContactId> contacts,
final String msg,
final String message,
final ExceptionHandler<DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
String msg = isNullOrEmpty(message) ? null : message;
for (ContactId c : contacts) {
try {
forumSharingManager.sendInvitation(g, c, msg);
long time = Math.max(clock.currentTimeMillis(),
conversationManager.getGroupCount(c)
.getLatestMsgTime() + 1);
forumSharingManager.sendInvitation(g, c, msg, time);
} catch (NoSuchContactException | NoSuchGroupException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);

View File

@@ -30,8 +30,8 @@ public class SplashScreenActivity extends BaseActivity {
private static final Logger LOG =
Logger.getLogger(SplashScreenActivity.class.getName());
// This build expires on 1 January 2017
private static final long EXPIRY_DATE = 1483225200 * 1000L;
// This build expires on 1 May 2017
private static final long EXPIRY_DATE = 1493593200 * 1000L;
@Inject
protected ConfigController configController;

View File

@@ -11,6 +11,7 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -29,7 +30,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
protected final TextView textView;
private final ViewGroup layout;
private final AuthorView author;
private final View topDivider;
public BaseThreadItemViewHolder(View v) {
super(v);
@@ -37,71 +37,52 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
layout = (ViewGroup) v.findViewById(R.id.layout);
textView = (TextView) v.findViewById(R.id.text);
author = (AuthorView) v.findViewById(R.id.author);
topDivider = v.findViewById(R.id.top_divider);
}
// TODO improve encapsulation, so we don't need to pass the adapter here
@CallSuper
public void bind(final ThreadItemAdapter<I> adapter,
final ThreadItemListener<I> listener, final I item, int pos) {
public void bind(final I item, final ThreadItemListener<I> listener) {
textView.setText(StringUtils.trim(item.getText()));
if (pos == 0) {
topDivider.setVisibility(View.INVISIBLE);
} else {
topDivider.setVisibility(View.VISIBLE);
}
author.setAuthor(item.getAuthor());
author.setDate(item.getTimestamp());
author.setAuthorStatus(item.getStatus());
if (item.equals(adapter.getReplyItem())) {
layout.setBackgroundColor(ContextCompat
.getColor(getContext(), R.color.forum_cell_highlight));
} else if (item.equals(adapter.getAddedItem())) {
layout.setBackgroundColor(ContextCompat
.getColor(getContext(), R.color.forum_cell_highlight));
animateFadeOut(adapter, adapter.getAddedItem());
adapter.clearAddedItem();
if (item.isHighlighted()) {
layout.setActivated(true);
} else if (!item.isRead()) {
layout.setActivated(true);
animateFadeOut();
listener.onUnreadItemVisible(item);
} else {
layout.setBackgroundColor(ContextCompat
.getColor(getContext(), R.color.window_background));
layout.setActivated(false);
}
}
private void animateFadeOut(final ThreadItemAdapter<I> adapter,
final I addedItem) {
private void animateFadeOut() {
setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator();
adapter.addAnimatingItem(addedItem, anim);
ColorDrawable viewColor = (ColorDrawable) layout.getBackground();
ColorDrawable viewColor = new ColorDrawable(ContextCompat
.getColor(getContext(), R.color.forum_cell_highlight));
anim.setIntValues(viewColor.getColor(), ContextCompat
.getColor(getContext(), R.color.window_background));
anim.setEvaluator(new ArgbEvaluator());
anim.setInterpolator(new AccelerateInterpolator());
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
layout.setBackgroundResource(
R.drawable.list_item_thread_background);
layout.setActivated(false);
setIsRecyclable(true);
adapter.removeAnimatingItem(addedItem);
}
@Override
public void onAnimationCancel(Animator animation) {
setIsRecyclable(true);
adapter.removeAnimatingItem(addedItem);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

View File

@@ -23,9 +23,7 @@ public abstract class ThreadItem implements MessageNode {
private final Author author;
private final Status status;
private int level = UNDEFINED;
private boolean isShowingDescendants = true;
private int descendantCount = 0;
private boolean isRead;
private boolean isRead, highlighted;
public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
String text, long timestamp, Author author, Status status,
@@ -37,6 +35,7 @@ public abstract class ThreadItem implements MessageNode {
this.author = author;
this.status = status;
this.isRead = isRead;
this.highlighted = false;
}
public String getText() {
@@ -71,19 +70,11 @@ public abstract class ThreadItem implements MessageNode {
return status;
}
public boolean isShowingDescendants() {
return isShowingDescendants;
}
@Override
public void setLevel(int level) {
this.level = level;
}
public void setShowingDescendants(boolean showingDescendants) {
this.isShowingDescendants = showingDescendants;
}
public boolean isRead() {
return isRead;
}
@@ -92,13 +83,12 @@ public abstract class ThreadItem implements MessageNode {
isRead = read;
}
public boolean hasDescendants() {
return descendantCount > 0;
public void setHighlighted(boolean highlighted) {
this.highlighted = highlighted;
}
@Override
public void setDescendantCount(int descendantCount) {
this.descendantCount = descendantCount;
public boolean isHighlighted() {
return highlighted;
}
}

View File

@@ -1,6 +1,6 @@
package org.briarproject.briar.android.threaded;
import android.animation.ValueAnimator;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -12,16 +12,11 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.VersionedAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
@UiThread
public class ThreadItemAdapter<I extends ThreadItem>
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
implements VersionedAdapter {
@@ -29,15 +24,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
static final int UNDEFINED = -1;
protected final NestedTreeList<I> items = new NestedTreeList<>();
private final Map<I, ValueAnimator> animatingItems = new HashMap<>();
private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager;
// highlight not dependant on time
private I replyItem;
// temporary highlight
private I addedItem;
private volatile int revision = 0;
public ThreadItemAdapter(ThreadItemListener<I> listener,
@@ -56,24 +45,23 @@ public class ThreadItemAdapter<I extends ThreadItem>
@Override
public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) {
I item = getVisibleItem(position);
if (item == null) return;
listener.onItemVisible(item);
ui.bind(this, listener, item, position);
I item = items.get(position);
ui.bind(item, listener);
}
/**
* Contrary to the super class adapter,
* this returns the number of <b>visible</b> items,
* not all items in the dataset.
*/
@Override
public int getItemCount() {
return getVisiblePos(null);
return items.size();
}
I getReplyItem() {
return replyItem;
@Override
public int getRevision() {
return revision;
}
@Override
public void incrementRevision() {
revision++;
}
public void setItems(Collection<I> items) {
@@ -84,249 +72,116 @@ public class ThreadItemAdapter<I extends ThreadItem>
public void add(I item) {
items.add(item);
addedItem = item;
if (item.getParentId() == null) {
notifyItemInserted(getVisiblePos(item));
} else {
// Try to find the item's parent and perform the proper ui update if
// it's present and visible.
for (int i = items.indexOf(item) - 1; i >= 0; i--) {
I higherItem = items.get(i);
if (higherItem.getLevel() < item.getLevel()) {
// parent found
if (higherItem.isShowingDescendants()) {
int parentVisiblePos = getVisiblePos(higherItem);
if (parentVisiblePos != NO_POSITION) {
// parent is visible, we need to update its ui
notifyItemChanged(parentVisiblePos);
// new item insert ui
int visiblePos = getVisiblePos(item);
notifyItemInserted(visiblePos);
break;
}
} else {
// do not show the new item if its parent is not showing
// descendants (this can be overridden by the user by
// pressing the snack bar)
break;
}
}
}
}
notifyItemInserted(findItemPosition(item));
}
void scrollTo(I item) {
int visiblePos = getVisiblePos(item);
MessageId parentId = item.getParentId();
if (visiblePos == NO_POSITION && parentId != null) {
// The item is not visible due to being hidden by its parent item.
// Find the parent and make it visible and traverse up the parent
// chain if necessary to make the item visible
for (int i = items.indexOf(item) - 1; i >= 0; i--) {
I higherItem = items.get(i);
if (higherItem.getId().equals(parentId)) {
// parent found
showDescendants(higherItem);
int parentPos = getVisiblePos(higherItem);
if (parentPos != NO_POSITION) {
// parent or ancestor is visible, item's visibility
// is ensured
notifyItemChanged(parentPos);
visiblePos = parentPos;
break;
}
// parent or ancestor is hidden, we need to continue up the
// dependency chain
parentId = higherItem.getParentId();
if (parentId == null) throw new AssertionError();
}
}
@Nullable
public I getItemAt(int position) {
if (position == NO_POSITION || position >= items.size()) {
return null;
}
if (visiblePos != NO_POSITION)
layoutManager.scrollToPositionWithOffset(visiblePos, 0);
return items.get(position);
}
int getReplyCount(I item) {
int counter = 0;
int pos = items.indexOf(item);
if (pos >= 0) {
int ancestorLvl = item.getLevel();
for (int i = pos + 1; i < items.size(); i++) {
int descendantLvl = items.get(i).getLevel();
if (descendantLvl <= ancestorLvl)
break;
if (descendantLvl == ancestorLvl + 1)
counter++;
}
protected int findItemPosition(@Nullable I item) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).equals(item)) return i;
}
return counter;
return NO_POSITION; // Not found
}
void setReplyItem(@Nullable I item) {
if (replyItem != null) {
notifyItemChanged(getVisiblePos(replyItem));
}
replyItem = item;
if (replyItem != null) {
notifyItemChanged(getVisiblePos(replyItem));
}
}
void setReplyItemById(MessageId id) {
for (I item : items) {
if (item.getId().equals(id)) {
setReplyItem(item);
break;
}
}
}
private List<Integer> getSubTreeIndexes(int pos, int levelLimit) {
List<Integer> indexList = new ArrayList<>();
for (int i = pos + 1; i < getItemCount(); i++) {
I item = getVisibleItem(i);
if (item != null && item.getLevel() > levelLimit) {
indexList.add(i);
} else {
break;
}
}
return indexList;
}
public void showDescendants(I item) {
item.setShowingDescendants(true);
int visiblePos = getVisiblePos(item);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, item.getLevel());
if (!indexList.isEmpty()) {
if (indexList.size() == 1) {
notifyItemInserted(indexList.get(0));
} else {
notifyItemRangeInserted(indexList.get(0),
indexList.size());
}
}
}
public void hideDescendants(I item) {
int visiblePos = getVisiblePos(item);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, item.getLevel());
if (!indexList.isEmpty()) {
// stop animating children
for (int index : indexList) {
ValueAnimator anim = animatingItems.get(items.get(index));
if (anim != null && anim.isRunning()) {
anim.cancel();
}
}
if (indexList.size() == 1) {
notifyItemRemoved(indexList.get(0));
} else {
notifyItemRangeRemoved(indexList.get(0),
indexList.size());
}
}
item.setShowingDescendants(false);
}
/**
* Returns the visible item at the given position
* Highlights the item with the given {@link MessageId}
* and disables the highlight for a previously highlighted item, if any.
*
* @param position is visible item index
* @return the visible item at index 'position' from an ordered list of
* visible items, or null if 'position' is larger than the number of
* visible items.
* Only one item can be highlighted at a time.
*/
void setHighlightedItem(@Nullable MessageId id) {
for (int i = 0; i < items.size(); i++) {
I item = items.get(i);
if (id != null && item.getId().equals(id)) {
item.setHighlighted(true);
notifyItemChanged(i, item);
} else if (item.isHighlighted()) {
item.setHighlighted(false);
notifyItemChanged(i, item);
}
}
}
@Nullable
public I getVisibleItem(int position) {
int levelLimit = UNDEFINED;
for (I item : items) {
if (levelLimit >= 0) {
// skip hidden items that their parent is hiding
if (item.getLevel() > levelLimit) {
continue;
}
levelLimit = UNDEFINED;
}
if (!item.isShowingDescendants()) {
levelLimit = item.getLevel();
}
if (position-- == 0) {
return item;
}
I getHighlightedItem() {
for (I i : items) {
if (i.isHighlighted()) return i;
}
return null;
}
boolean isVisible(I item) {
return getVisiblePos(item) != NO_POSITION;
/**
* Gets the number of unread items above and below the current view port.
*
* Attention: Do not call this when the list is still scrolling,
* because then the view port is unknown.
*/
public UnreadCount getUnreadCount() {
final int positionTop = layoutManager.findFirstVisibleItemPosition();
final int positionBottom = layoutManager.findLastVisibleItemPosition();
if (positionTop == NO_POSITION && positionBottom == NO_POSITION)
return new UnreadCount(0, 0);
int unreadCounterTop = 0, unreadCounterBottom = 0;
for (int i = 0; i < items.size(); i++) {
I item = items.get(i);
if (i < positionTop && !item.isRead()) {
unreadCounterTop++;
} else if (i > positionBottom && !item.isRead()) {
unreadCounterBottom++;
}
}
return new UnreadCount(unreadCounterTop, unreadCounterBottom);
}
/**
* Returns the visible position of the given item.
*
* @param item the item to find the visible position of, or null to
* return the total count of visible items.
* @return the visible position of 'item', or the total number of visible
* items if 'item' is null. If 'item' is not visible, NO_POSITION is
* returned.
* Returns the position of the first unread item below the current viewport
*/
protected int getVisiblePos(@Nullable I item) {
int visibleCounter = 0;
int levelLimit = UNDEFINED;
for (I i : items) {
if (levelLimit >= 0) {
if (i.getLevel() > levelLimit) {
// skip all the items below a non visible branch
continue;
}
levelLimit = UNDEFINED;
}
if (item != null && item.equals(i)) {
return visibleCounter;
} else if (!i.isShowingDescendants()) {
levelLimit = i.getLevel();
}
visibleCounter++;
public int getVisibleUnreadPosBottom() {
final int positionBottom = layoutManager.findLastVisibleItemPosition();
if (positionBottom == NO_POSITION) return NO_POSITION;
for (int i = positionBottom + 1; i < items.size(); i++) {
if (!items.get(i).isRead()) return i;
}
return item == null ? visibleCounter : NO_POSITION;
return NO_POSITION;
}
I getAddedItem() {
return addedItem;
/**
* Returns the position of the first unread item above the current viewport
*/
public int getVisibleUnreadPosTop() {
final int positionTop = layoutManager.findFirstVisibleItemPosition();
int position = NO_POSITION;
for (int i = 0; i < items.size(); i++) {
if (i < positionTop && !items.get(i).isRead()) {
position = i;
} else if (i >= positionTop) {
return position;
}
}
return NO_POSITION;
}
void clearAddedItem() {
addedItem = null;
}
static class UnreadCount {
final int top, bottom;
void addAnimatingItem(I item, ValueAnimator anim) {
animatingItems.put(item, anim);
}
void removeAnimatingItem(I item) {
animatingItems.remove(item);
}
@Override
public int getRevision() {
return revision;
}
@UiThread
@Override
public void incrementRevision() {
revision++;
private UnreadCount(int top, int bottom) {
this.top = top;
this.bottom = bottom;
}
}
public interface ThreadItemListener<I> {
void onItemVisible(I item);
void onUnreadItemVisible(I item);
void onReplyClick(I item);
}
}

View File

@@ -3,13 +3,13 @@ package org.briarproject.briar.android.threaded;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.MenuItem;
import android.view.View;
@@ -30,18 +30,23 @@ import org.briarproject.briar.android.threaded.ThreadListController.ThreadListLi
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.client.PostHeader;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import java.util.Collection;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.make;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.INFO;
import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCount;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -58,8 +63,11 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
protected A adapter;
protected BriarRecyclerView list;
private LinearLayoutManager layoutManager;
protected TextInputView textInput;
protected GroupId groupId;
private UnreadMessageButton upButton, downButton;
@Nullable
private MessageId replyId;
protected abstract ThreadListController<G, I, H> getController();
@@ -72,7 +80,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
setContentView(getLayout());
setContentView(R.layout.activity_threaded_conversation);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
@@ -84,11 +92,52 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
textInput.setVisibility(GONE);
textInput.setListener(this);
list = (BriarRecyclerView) findViewById(R.id.list);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
list.setLayoutManager(linearLayoutManager);
adapter = createAdapter(linearLayoutManager);
layoutManager = new LinearLayoutManager(this);
list.setLayoutManager(layoutManager);
adapter = createAdapter(layoutManager);
list.setAdapter(adapter);
list.getRecyclerView().addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx,
int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dx == 0 && dy == 0) {
// scrollToPosition has been called and finished
updateUnreadCount();
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView,
int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
updateUnreadCount();
}
}
});
upButton = (UnreadMessageButton) findViewById(R.id.upButton);
downButton = (UnreadMessageButton) findViewById(R.id.downButton);
upButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = adapter.getVisibleUnreadPosTop();
if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position);
}
}
});
downButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = adapter.getVisibleUnreadPosBottom();
if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position);
}
}
});
if (state != null) {
byte[] replyIdBytes = state.getByteArray(KEY_REPLY_ID);
if (replyIdBytes != null) replyId = new MessageId(replyIdBytes);
@@ -99,9 +148,6 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
loadSharingContacts();
}
@LayoutRes
protected abstract int getLayout();
protected abstract A createAdapter(LinearLayoutManager layoutManager);
protected void loadNamedGroup() {
@@ -114,8 +160,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
// TODO Proper error handling
finish();
handleDbException(exception);
}
});
}
@@ -137,7 +182,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
adapter.setItems(items);
list.showData();
if (replyId != null)
adapter.setReplyItemById(replyId);
adapter.setHighlightedItem(replyId);
}
} else {
LOG.info("Concurrent update, reloading");
@@ -147,8 +192,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
// TODO Proper error handling
finish();
handleDbException(exception);
}
});
}
@@ -165,9 +209,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
}
@Override
public void onExceptionUi(DbException e) {
// TODO Proper error handling
finish();
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
@@ -200,7 +243,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
super.onSaveInstanceState(outState);
boolean visible = textInput.getVisibility() == VISIBLE;
outState.putBoolean(KEY_INPUT_VISIBILITY, visible);
ThreadItem replyItem = adapter.getReplyItem();
ThreadItem replyItem = adapter.getHighlightedItem();
if (replyItem != null) {
outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes());
}
@@ -221,14 +264,14 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
public void onBackPressed() {
if (textInput.getVisibility() == VISIBLE) {
textInput.setVisibility(GONE);
adapter.setReplyItem(null);
adapter.setHighlightedItem(null);
} else {
super.onBackPressed();
}
}
@Override
public void onItemVisible(I item) {
public void onUnreadItemVisible(I item) {
if (!item.isRead()) {
item.setRead(true);
getController().markItemRead(item);
@@ -236,8 +279,21 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
}
@Override
public void onReplyClick(I item) {
public void onReplyClick(final I item) {
showTextInput(item);
if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item);
} else {
// wait with scrolling until keyboard opened
textInput.addOnKeyboardShownListener(
new KeyboardAwareLinearLayout.OnKeyboardShownListener() {
@Override
public void onKeyboardShown() {
scrollToItemAtTop(item);
textInput.removeOnKeyboardShownListener(this);
}
});
}
}
@Override
@@ -260,7 +316,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
}
}
protected void displaySnackbarShort(@StringRes int stringId) {
private void scrollToItemAtTop(I item) {
int position = adapter.findItemPosition(item);
if (position != NO_POSITION) {
layoutManager
.scrollToPositionWithOffset(position, 0);
}
}
protected void displaySnackbar(@StringRes int stringId) {
Snackbar snackbar = make(list, stringId, Snackbar.LENGTH_SHORT);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show();
@@ -278,7 +342,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
textInput.showSoftKeyboard();
textInput.setHint(replyItem == null ? R.string.forum_new_message_hint :
R.string.forum_message_reply_hint);
adapter.setReplyItem(replyItem);
adapter.setHighlightedItem(
replyItem == null ? null : replyItem.getId());
}
@Override
@@ -286,10 +351,10 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
if (text.trim().length() == 0)
return;
if (StringUtils.utf8IsTooLong(text, getMaxBodyLength())) {
displaySnackbarShort(R.string.text_too_long);
displaySnackbar(R.string.text_too_long);
return;
}
I replyItem = adapter.getReplyItem();
I replyItem = adapter.getHighlightedItem();
UiResultExceptionHandler<I, DbException> handler =
new UiResultExceptionHandler<I, DbException>(this) {
@Override
@@ -299,15 +364,14 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
// TODO add proper exception handling
finish();
handleDbException(exception);
}
};
getController().createAndStoreMessage(text, replyItem, handler);
textInput.hideSoftKeyboard();
textInput.setVisibility(GONE);
textInput.setText("");
adapter.setReplyItem(null);
adapter.setHighlightedItem(null);
}
protected abstract int getMaxBodyLength();
@@ -323,8 +387,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
// TODO add proper exception handling
finish();
handleDbException(exception);
}
});
}
@@ -334,34 +397,29 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
supportFinishAfterTransition();
}
protected void addItem(final I item, boolean isLocal) {
protected void addItem(I item, boolean isLocal) {
adapter.incrementRevision();
adapter.add(item);
if (isLocal && adapter.isVisible(item)) {
displaySnackbarShort(getItemPostedString());
if (isLocal) {
displaySnackbar(getItemPostedString());
scrollToItemAtTop(item);
} else {
Snackbar snackbar = Snackbar.make(list,
isLocal ? getItemPostedString() : getItemReceivedString(),
Snackbar.LENGTH_LONG);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setActionTextColor(ContextCompat
.getColor(ThreadListActivity.this,
R.color.briar_button_positive));
snackbar.setAction(R.string.show, new View.OnClickListener() {
@Override
public void onClick(View v) {
adapter.scrollTo(item);
}
});
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show();
updateUnreadCount();
}
}
private void updateUnreadCount() {
UnreadCount unreadCount = adapter.getUnreadCount();
if (LOG.isLoggable(INFO)) {
LOG.info("Updating unread count: top=" + unreadCount.top +
" bottom=" + unreadCount.bottom);
}
upButton.setUnreadCount(unreadCount.top);
downButton.setUnreadCount(unreadCount.bottom);
}
@StringRes
protected abstract int getItemPostedString();
@StringRes
protected abstract int getItemReceivedString();
}

View File

@@ -9,7 +9,6 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@UiThread
@@ -17,32 +16,29 @@ import static android.view.View.VISIBLE;
public class ThreadPostViewHolder<I extends ThreadItem>
extends BaseThreadItemViewHolder<I> {
private final TextView lvlText, repliesText;
private final TextView lvlText;
private final View[] lvls;
private final View chevron, replyButton;
private final View replyButton;
private final static int[] nestedLineIds = {
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
R.id.nested_line_4, R.id.nested_line_5
};
public ThreadPostViewHolder(View v) {
super(v);
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
repliesText = (TextView) v.findViewById(R.id.replies);
int[] nestedLineIds = {
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
R.id.nested_line_4, R.id.nested_line_5
};
lvls = new View[nestedLineIds.length];
for (int i = 0; i < lvls.length; i++) {
lvls[i] = v.findViewById(nestedLineIds[i]);
}
chevron = v.findViewById(R.id.chevron);
replyButton = v.findViewById(R.id.btn_reply);
}
// TODO improve encapsulation, so we don't need to pass the adapter here
@Override
public void bind(final ThreadItemAdapter<I> adapter,
final ThreadItemListener<I> listener, final I item, int pos) {
super.bind(adapter, listener, item, pos);
public void bind(final I item, final ThreadItemListener<I> listener) {
super.bind(item, listener);
for (int i = 0; i < lvls.length; i++) {
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
@@ -54,41 +50,10 @@ public class ThreadPostViewHolder<I extends ThreadItem>
lvlText.setVisibility(GONE);
}
int replies = adapter.getReplyCount(item);
if (replies == 0) {
repliesText.setText("");
} else {
repliesText.setText(getContext().getResources()
.getQuantityString(R.plurals.message_replies, replies,
replies));
}
if (item.hasDescendants()) {
chevron.setVisibility(VISIBLE);
if (item.isShowingDescendants()) {
chevron.setSelected(false);
} else {
chevron.setSelected(true);
}
chevron.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
chevron.setSelected(!chevron.isSelected());
if (chevron.isSelected()) {
adapter.hideDescendants(item);
} else {
adapter.showDescendants(item);
}
}
});
} else {
chevron.setVisibility(INVISIBLE);
}
replyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onReplyClick(item);
adapter.scrollTo(item);
}
});
}

View File

@@ -33,7 +33,7 @@ import static android.text.format.DateUtils.WEEK_IN_MILLIS;
public class UiUtils {
public static final long MIN_RESOLUTION = MINUTE_IN_MILLIS;
public static final long MIN_DATE_RESOLUTION = MINUTE_IN_MILLIS;
public static final int TEASER_LENGTH = 320;
public static final float GREY_OUT = 0.5f;
@@ -51,15 +51,16 @@ public class UiUtils {
FORMAT_SHOW_DATE | FORMAT_ABBREV_TIME | FORMAT_ABBREV_MONTH;
long diff = System.currentTimeMillis() - time;
if (diff < MIN_RESOLUTION) return ctx.getString(R.string.now);
if (diff < MIN_DATE_RESOLUTION) return ctx.getString(R.string.now);
if (diff >= DAY_IN_MILLIS && diff < WEEK_IN_MILLIS) {
// also show time when older than a day, but newer than a week
return DateUtils.getRelativeDateTimeString(ctx, time,
MIN_RESOLUTION, WEEK_IN_MILLIS, flags).toString();
MIN_DATE_RESOLUTION, WEEK_IN_MILLIS, flags).toString();
}
// otherwise just show "...ago" or date string
return DateUtils.getRelativeTimeSpanString(time,
System.currentTimeMillis(), MIN_RESOLUTION, flags).toString();
System.currentTimeMillis(),
MIN_DATE_RESOLUTION, flags).toString();
}
public static SpannableStringBuilder getTeaser(Context ctx, Spanned body) {

View File

@@ -2,6 +2,8 @@ package org.briarproject.briar.android.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.util.AttributeSet;
@@ -17,14 +19,15 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import static org.briarproject.briar.android.util.UiUtils.MIN_RESOLUTION;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
public class BriarRecyclerView extends FrameLayout {
private static final long DEFAULT_REFRESH_INTERVAL = MIN_RESOLUTION;
private static final Logger LOG =
Logger.getLogger(BriarRecyclerView.class.getName());
private final Handler handler = new Handler(Looper.getMainLooper());
private RecyclerView recyclerView;
private TextView emptyView;
private ProgressBar progressBar;
@@ -192,18 +195,20 @@ public class BriarRecyclerView extends FrameLayout {
@Override
public void run() {
LOG.info("Updating Content...");
recyclerView.getAdapter().notifyDataSetChanged();
postDelayed(refresher, DEFAULT_REFRESH_INTERVAL);
Adapter adapter = recyclerView.getAdapter();
adapter.notifyItemRangeChanged(0, adapter.getItemCount());
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
};
LOG.info("Adding Handler Callback");
postDelayed(refresher, DEFAULT_REFRESH_INTERVAL);
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
public void stopPeriodicUpdate() {
if (refresher != null) {
LOG.info("Removing Handler Callback");
removeCallbacks(refresher);
handler.removeCallbacks(refresher);
refresher = null;
}
}

View File

@@ -0,0 +1,80 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
@UiThread
@NotNullByDefault
public class UnreadMessageButton extends FrameLayout {
private final static int UP = 0, DOWN = 1;
private final FloatingActionButton fab;
private final TextView unread;
public UnreadMessageButton(Context context) {
this(context, null);
}
public UnreadMessageButton(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public UnreadMessageButton(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater
.inflate(R.layout.unread_message_button, this, true);
fab = (FloatingActionButton) findViewById(R.id.fab);
unread = (TextView) findViewById(R.id.unreadCountView);
TypedArray attributes = context.obtainStyledAttributes(attrs,
R.styleable.UnreadMessageButton);
int direction = attributes
.getInteger(R.styleable.UnreadMessageButton_direction, DOWN);
setDirection(direction);
attributes.recycle();
setUnreadCount(0);
}
private void setDirection(int direction) {
if (direction == UP) {
fab.setImageResource(R.drawable.chevron_up_white);
} else if (direction == DOWN) {
fab.setImageResource(R.drawable.chevron_down_white);
} else {
throw new IllegalArgumentException();
}
}
public void setUnreadCount(int count) {
if (count == 0) {
fab.setVisibility(GONE);
// fab.hide();
unread.setVisibility(GONE);
} else {
// FIXME: Use animations when upgrading to support library 24.2.0
// https://code.google.com/p/android/issues/detail?id=216469
fab.setVisibility(VISIBLE);
// if (!fab.isShown()) fab.show();
unread.setVisibility(VISIBLE);
unread.setText(String.valueOf(count));
}
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/forum_cell_highlight"
android:state_activated="true"/>
<item
android:drawable="@color/window_background"/>
</selector>

View File

@@ -11,17 +11,7 @@
android:orientation="vertical"
tools:context=".android.reporting.DevReportActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/toolbar"/>
<RelativeLayout
android:layout_width="match_parent"

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical">
<org.briarproject.briar.android.view.BriarRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:emptyText="@string/no_forum_posts"
app:scrollToEnd="false"/>
<org.briarproject.briar.android.view.TextInputView
android:id="@+id/text_input_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/button_bar_background"
android:elevation="@dimen/margin_tiny"
app:hint="@string/forum_new_message_hint"/>
</LinearLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical"
tools:context=".android.keyagreement.KeyAgreementActivity">
<include layout="@layout/toolbar"/>
<FrameLayout
android:id="@+id/fragmentContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View File

@@ -2,8 +2,10 @@
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_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:layout_height="match_parent"
tools:context=".android.navdrawer.NavDrawerActivity">
<!-- The first child(root) is the content view -->
<LinearLayout
@@ -11,17 +13,7 @@
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/toolbar"/>
<FrameLayout
android:id="@+id/fragmentContainer"

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background"/>
</LinearLayout>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"
tools:context=".android.forum.ForumActivity">
<include layout="@layout/toolbar"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<org.briarproject.briar.android.view.BriarRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:emptyText="@string/no_forum_posts"
app:scrollToEnd="false"/>
<org.briarproject.briar.android.view.UnreadMessageButton
android:id="@+id/upButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
app:direction="up"/>
<org.briarproject.briar.android.view.UnreadMessageButton
android:id="@+id/downButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
app:direction="down"/>
</FrameLayout>
<org.briarproject.briar.android.view.TextInputView
android:id="@+id/text_input_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/button_bar_background"
android:elevation="@dimen/margin_tiny"
app:hint="@string/forum_new_message_hint"/>
</LinearLayout>

View File

@@ -43,7 +43,7 @@
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_below="@+id/declineButton"
android:layout_below="@+id/acceptButton"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
@@ -57,6 +57,7 @@
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_below="@+id/text"
android:layout_marginBottom="-10dp"
android:text="@string/accept"/>
<Button
@@ -64,8 +65,8 @@
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/text"
android:layout_marginBottom="-15dp"
android:layout_alignBottom="@+id/acceptButton"
android:layout_alignTop="@+id/acceptButton"
android:layout_toLeftOf="@+id/acceptButton"
android:layout_toStartOf="@+id/acceptButton"
android:text="@string/decline"/>

View File

@@ -60,7 +60,7 @@
tools:text="Dec 24"/>
<View
style="@style/Divider.ForumList"
style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/postCountView"/>

View File

@@ -97,7 +97,7 @@
<View
android:id="@+id/divider"
style="@style/Divider.ForumList"
style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/statusView"

View File

@@ -6,23 +6,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_medium"
android:baselineAligned="false"
android:orientation="vertical">
<View
android:id="@+id/top_divider"
style="@style/Divider.ForumList"
android:layout_alignParentTop="true"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/top_divider"
android:layout_marginBottom="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_medium"
android:layout_marginTop="@dimen/margin_medium"
android:layout_margin="@dimen/margin_medium"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
@@ -32,9 +23,12 @@
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/info"
android:layout_alignLeft="@+id/text"
android:layout_alignTop="@+id/info"
android:layout_below="@+id/text"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_medium"
android:scaleType="center"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_visibility"/>
@@ -45,6 +39,7 @@
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_below="@+id/text"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_toRightOf="@+id/icon"
android:gravity="center_vertical"
android:minHeight="24dp"
@@ -57,10 +52,10 @@
<org.briarproject.briar.android.view.AuthorView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="@dimen/button_size"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/text"
android:layout_below="@+id/info"
android:gravity="center"
android:layout_toLeftOf="@+id/optionsButton"
app:persona="commenter"/>
<Button
@@ -68,11 +63,16 @@
style="@style/BriarButtonFlat.Positive.Tiny"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/author"
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_alignTop="@+id/author"
android:layout_toRightOf="@+id/author"
android:layout_below="@+id/info"
android:gravity="right|center_vertical"
android:text="@string/options"/>
<View
style="@style/Divider.ThreadItem"
android:layout_below="@+id/author"
android:layout_marginTop="@dimen/margin_medium"/>
</RelativeLayout>

View File

@@ -81,7 +81,7 @@
android:text="@string/decline"/>
<View
style="@style/Divider.ForumList"
style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/acceptButton"/>

View File

@@ -113,7 +113,7 @@
tools:text="This is a description of the RSS feed. It can be several lines long, but it can also not exist at all if it is not present in the feed itself."/>
<View
style="@style/Divider.ForumList"
style="@style/Divider.ThreadItem"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/descriptionView"

View File

@@ -6,6 +6,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_thread_background"
android:baselineAligned="false"
android:orientation="horizontal">
@@ -61,23 +62,20 @@
android:background="@drawable/level_indicator_circle"
android:gravity="center"
android:textSize="@dimen/text_size_small"
android:visibility="gone"
/>
android:visibility="gone"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_weight="1">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="@dimen/margin_medium"
android:paddingTop="@dimen/margin_medium"
android:padding="@dimen/margin_medium"
android:textColor="@color/briar_text_primary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
@@ -86,26 +84,12 @@
<org.briarproject.briar.android.view.AuthorView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="@dimen/button_size"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/text"
android:layout_below="@id/text"
android:gravity="center"
app:persona="commenter"/>
<TextView
android:id="@+id/replies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/author"
android:layout_alignTop="@+id/author"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toLeftOf="@+id/btn_reply"
android:layout_toRightOf="@+id/author"
android:ellipsize="end"
android:gravity="right|end|center_vertical"
android:maxLines="1"
android:padding="@dimen/margin_medium"
android:textSize="@dimen/text_size_tiny"
tools:text="2 replies"/>
app:persona="commenter"/>
<TextView
android:id="@+id/btn_reply"
@@ -113,31 +97,18 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/author"
android:layout_toLeftOf="@+id/chevron"
android:layout_alignParentRight="true"
android:layout_below="@+id/text"
android:layout_marginRight="@dimen/margin_medium"
android:text="@string/btn_reply"
android:textSize="@dimen/text_size_tiny"/>
<ImageView
android:id="@+id/chevron"
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
android:layout_alignBottom="@+id/author"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="@dimen/margin_medium"
android:scaleType="center"
android:src="@drawable/selector_chevron"/>
<View
android:id="@+id/top_divider"
style="@style/Divider.ForumList"
android:layout_width="match_parent"
android:layout_height="@dimen/margin_separator"
style="@style/Divider.ThreadItem"
android:layout_alignLeft="@id/text"
android:layout_alignParentTop="true"/>
android:layout_below="@+id/author"
android:layout_marginTop="@dimen/margin_medium"/>
</RelativeLayout>
</LinearLayout>
</LinearLayout>

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