Compare commits

..

86 Commits

Author SHA1 Message Date
akwizgran
d145a082f5 Bump client minor version to avoid triggering crash. 2019-06-28 14:07:28 +01:00
akwizgran
4fd012c31a Merge branch 'compress-images' into 'master'
Compress images

See merge request briar/briar!1147
2019-06-26 14:21:24 +00:00
akwizgran
95d06770bf Rename 'scale' to 'inSampleSize' for clarity. 2019-06-26 14:36:40 +01:00
akwizgran
428247b7b2 Initialise result LiveData before starting task. 2019-06-26 14:31:40 +01:00
akwizgran
a921361a56 Inject ImageSizeCalculator. 2019-06-26 12:40:28 +01:00
akwizgran
fe7dfa721e Compress image attachments. 2019-06-25 16:55:09 +01:00
akwizgran
92eb06a9e9 Refactor attachment creation to use injection. 2019-06-25 16:29:54 +01:00
Torsten Grote
5beed1a748 Merge branch '1594-preview-fails-to-load' into 'master'
Use a fresh LiveData for each attachment creation task

Closes #1594

See merge request briar/briar!1144
2019-06-20 14:05:43 +00:00
Torsten Grote
774047d856 Merge branch '1585-check-attachment-content-type' into 'master'
Improve handling of missing attachments in UI

See merge request briar/briar!1142
2019-06-20 14:04:02 +00:00
Torsten Grote
fc28e7aa88 Merge branch 'nickname-nitpicks' into 'master'
Nickname nitpicks

See merge request briar/briar!1143
2019-06-20 13:41:25 +00:00
Torsten Grote
78459499b2 Merge branch '1593-qr-code-assertion-error' into 'master'
Keep enum methods used by ZXing

Closes #1593

See merge request briar/briar!1146
2019-06-19 23:45:49 +00:00
akwizgran
c2973608d7 Keep enum methods used by ZXing. 2019-06-19 16:36:39 +01:00
akwizgran
be1c33cb42 Use a fresh LiveData for each attachment creation task. 2019-06-19 13:43:04 +01:00
akwizgran
c955466bda Load missing attachments when they arrive. 2019-06-19 12:47:18 +01:00
akwizgran
593a0c4632 Improve handling of missing and invalid attachments. 2019-06-19 11:23:57 +01:00
akwizgran
ed20b2d8d6 Use attachment header to retrieve attachment. 2019-06-19 10:57:13 +01:00
akwizgran
34583e6d2d Merge branch '1054-crash-scroll' into 'master'
Improve crash screen and reporter

Closes #1426, #1061, #1390, #1012, and #1054

See merge request briar/briar!1049
2019-06-18 16:47:02 +00:00
Torsten Grote
ea5a862242 [android] Fix send button in ReportForm's action bar 2019-06-18 13:28:28 -03:00
akwizgran
9ab9e02f8a Trim whitespace from nicknames (useful for auto-complete). 2019-06-18 17:24:08 +01:00
akwizgran
3f70ae3c8c Use same input type for nicknames everywhere. 2019-06-18 17:19:39 +01:00
Torsten Grote
3f60098099 [android] don't cancel crash reports after sending them 2019-06-18 12:21:04 -03:00
Torsten Grote
e965021e3d [android] don't clear task when submitting feedback, only after crash 2019-06-18 12:21:04 -03:00
Torsten Grote
7d9380d3d6 [android] go to homescreen after pressing back in crash reporter
Fixes #1390
2019-06-18 12:21:04 -03:00
Torsten Grote
3c8c0e579e [android] point ACRA to correct BuildConfig class
Fixes #1061
2019-06-18 12:21:03 -03:00
Torsten Grote
bd2bbe9268 [android] don't show JSON in feedback/crash report
use key-value pairs instead

Closes #1426
2019-06-18 12:21:03 -03:00
Torsten Grote
89d24b1753 [android] Make entire report form scrollable, not only the hidden data 2019-06-18 12:21:03 -03:00
Torsten Grote
861dbe20b1 [android] Fix crash screen buttons to the bottom of the screen
and resize crash icon to the available screen space
2019-06-18 12:21:02 -03:00
Torsten Grote
197800de8b [android] split crash report screen into two fragments 2019-06-18 12:21:02 -03:00
Torsten Grote
07e824ad68 [android] Make crash screen scrollable and add icon 2019-06-18 12:21:01 -03:00
Torsten Grote
d210215bd1 Merge branch '1585-new-messaging-client' into 'master'
Add support for image attachments to messaging client

Closes #1585

See merge request briar/briar!1133
2019-06-18 14:55:40 +00:00
akwizgran
00705447ec Use feature flag to decide which version to advertise. 2019-06-18 13:39:01 +01:00
akwizgran
9095ccef85 Filter attachment URIs in controller. 2019-06-18 13:10:52 +01:00
akwizgran
3196204094 Send legacy private messages from headless app. 2019-06-18 13:03:50 +01:00
akwizgran
2bae639105 Upgrade messaging client to support attachments. 2019-06-18 13:03:49 +01:00
akwizgran
f73d298752 Merge branch 'inject-feature-flags' into 'master'
Use injection to provide feature flags

See merge request briar/briar!1140
2019-06-18 11:51:09 +00:00
Torsten Grote
bc3a443276 Merge branch '1590-create-private-messages-on-ui-thread' into 'master'
Move private message creation off the crypto executor

Closes #1590

See merge request briar/briar!1141
2019-06-18 11:22:45 +00:00
akwizgran
2a29d33303 Move private message creation off the crypto executor. 2019-06-18 12:14:10 +01:00
akwizgran
30e0be9f43 Merge branch '1580-show-snackbar' into 'master'
Show snackbar when there is no internet connection

Closes #1580

See merge request briar/briar!1139
2019-06-18 09:54:34 +00:00
akwizgran
3828d16971 Use injection to provide feature flags. 2019-06-18 10:52:21 +01:00
akwizgran
a54eb64eb5 Merge branch '1468-reject-unsupported-images' into 'master'
Reject unsupported images

Closes #1468

See merge request briar/briar!1038
2019-06-17 16:39:26 +00:00
Torsten Grote
ad2d3e70d6 [android] address thread-safety issues of attachment creation 2019-06-17 13:22:38 -03:00
Torsten Grote
1f91842c52 [android] re-use the same LiveData for AttachmentResults 2019-06-17 13:11:16 -03:00
Torsten Grote
c07a0a2fd7 [android] address review comments for rejecting unsupported images 2019-06-17 13:11:16 -03:00
Torsten Grote
4ee4905e06 [android] migrate added conversation header to new LiveEvent 2019-06-17 13:11:16 -03:00
Torsten Grote
67b7517f2b [android] refactor AttachmentCreator to return a single LiveData 2019-06-17 13:11:16 -03:00
Torsten Grote
cd3174a643 [android] Fix view recycling issue of image previews 2019-06-17 13:11:15 -03:00
Torsten Grote
9d9bc4ca84 [android] Let AttachmentCreator return same LiveData after configuration changes 2019-06-17 13:11:15 -03:00
Torsten Grote
7358091699 [android] Address first round of review comments for attachments 2019-06-17 13:11:15 -03:00
Torsten Grote
11eefaedcf Refactor attachment creation 2019-06-17 13:11:14 -03:00
Torsten Grote
bb5a6c0241 [android] Add assertions to TextAttachmentController 2019-06-17 13:11:14 -03:00
Torsten Grote
70d29af2ba [android] Allow sending message with attachments before previews are loaded 2019-06-17 13:11:14 -03:00
Torsten Grote
baedb14e2b [android] allow attaching only of images with supported mime type 2019-06-17 13:11:13 -03:00
Torsten Grote
2796926709 [android] Load image preview from database instead of content Uri 2019-06-17 13:11:13 -03:00
Torsten Grote
fc6275b037 [android] reject invalid mime types for image attachments 2019-06-17 13:11:13 -03:00
Torsten Grote
f76f9be4ed Reject attachments that exceed the allowed size
Closes #1468
2019-06-17 13:11:13 -03:00
Torsten Grote
6167ba5c46 [android] move unsent attachment cache logic into AttachmentController 2019-06-17 13:11:12 -03:00
Torsten Grote
55f4600a69 [android] Create attachments before showing previews 2019-06-17 13:11:12 -03:00
Torsten Grote
c73801c7e8 [android] Show snackbar when there is no internet connection 2019-06-17 10:11:02 -03:00
Torsten Grote
249e1e28fe Merge branch '1580-offline-state' into 'master'
Add offline state for pending contacts

Closes #1580

See merge request briar/briar!1138
2019-06-17 13:10:41 +00:00
akwizgran
f0cea28aeb Don't show a message for the offline state. 2019-06-17 13:45:22 +01:00
Torsten Grote
32e8ea9888 Merge branch '1565-strings-duplicate-handshake-links' into 'master'
Add strings for duplicate pending contacts

See merge request briar/briar!1137
2019-06-17 12:29:22 +00:00
akwizgran
5a1caed89f Rename endpoints field. 2019-06-17 13:22:36 +01:00
akwizgran
22f5c42fc1 Resolve merge conflicts.
# Conflicts:
#   briar-android/src/main/res/values/strings.xml
2019-06-17 12:13:19 +00:00
akwizgran
aab46040a5 Add comments for translators. 2019-06-17 13:12:11 +01:00
akwizgran
18fd238aa1 Merge branch '1580-strings-offline-state' into 'master'
Add string for pending contact offline state

See merge request briar/briar!1136
2019-06-17 11:12:50 +00:00
akwizgran
3a837b3c5a Resolve merge conflicts.
# Conflicts:
#   briar-android/src/main/res/values/strings.xml
2019-06-17 11:04:11 +00:00
akwizgran
ac2597865c Merge branch '1587-version-negotiation' into 'master'
Add version negotiation to sync protocol

Closes #1587

See merge request briar/briar!1134
2019-06-17 10:54:39 +00:00
akwizgran
4a67cf3ce7 Don't cache default state when adding pending contact.
This can overwrite the initial state broadcast by the
rendezvous poller.
2019-06-17 10:22:08 +01:00
Torsten Grote
a5041e651e Merge branch '1230-strings-adding-contact-slow' into 'master'
Add strings for warning when adding a contact is slow

See merge request briar/briar!1135
2019-06-15 13:37:13 +00:00
akwizgran
b0e97d787f Add offline state for pending contacts. 2019-06-15 12:27:24 +01:00
akwizgran
0d8af780a3 Add strings for duplicate pending contacts. 2019-06-15 11:31:18 +01:00
akwizgran
9c20e6b333 Add string for pending contact offline state. 2019-06-15 11:04:22 +01:00
akwizgran
ab14976c96 Add strings for warning when adding a contact is slow. 2019-06-15 11:01:09 +01:00
akwizgran
ec3f821ba6 Update test expectations. 2019-06-13 17:17:50 +01:00
akwizgran
1d546da781 Store sync versions received from contacts. 2019-06-13 17:07:12 +01:00
akwizgran
f2c951b70b Add DB methods for getting and setting sync versions. 2019-06-13 17:06:57 +01:00
akwizgran
1e259c100d Add sync versions column to contacts table. 2019-06-13 16:35:48 +01:00
akwizgran
3636aeba9a Use HyperSQL-compatible syntax in migration. 2019-06-13 16:34:20 +01:00
akwizgran
132e20a6ce Send versions record at start of each session. 2019-06-13 16:16:02 +01:00
akwizgran
c228e5c219 Add versions record to sync protocol. 2019-06-13 16:16:02 +01:00
akwizgran
ae1d1fc5a7 Add thread safety and null safety annotations. 2019-06-13 16:16:01 +01:00
Torsten Grote
37f02a40e9 Merge branch '1585-temporary-messages' into 'master'
Add support for temporary messages

See merge request briar/briar!1132
2019-06-12 15:39:02 +00:00
akwizgran
3c8b8c39e1 Turn commonly used variables into fields. 2019-06-12 16:29:24 +01:00
akwizgran
8f839e2c30 Remove temporary messages at startup. 2019-06-12 15:21:48 +01:00
akwizgran
da4b63f20f Clean up ValidationManagerImplTest. 2019-06-12 15:17:13 +01:00
akwizgran
cd40e771d2 Allow messages to be marked as temporary. 2019-06-12 15:11:10 +01:00
158 changed files with 4545 additions and 1729 deletions

View File

@@ -0,0 +1,11 @@
package org.briarproject.bramble.api;
/**
* Interface for specifying which features are enabled in a build.
*/
public interface FeatureFlags {
boolean shouldEnableImageAttachments();
boolean shouldEnableRemoteContacts();
}

View File

@@ -25,7 +25,10 @@ public interface ClientHelper {
throws DbException, FormatException;
void addLocalMessage(Transaction txn, Message m, BdfDictionary metadata,
boolean shared) throws DbException, FormatException;
boolean shared, boolean temporary)
throws DbException, FormatException;
Message createMessage(GroupId g, long timestamp, byte[] body);
Message createMessage(GroupId g, long timestamp, BdfList body)
throws FormatException;
@@ -108,7 +111,7 @@ public interface ClientHelper {
Author parseAndValidateAuthor(BdfList author) throws FormatException;
PublicKey parseAndValidateAgreementPublicKey(byte[] publicKeyBytes)
throws FormatException;
throws FormatException;
TransportProperties parseAndValidateTransportProperties(
BdfDictionary properties) throws FormatException;

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.contact;
public enum PendingContactState {
WAITING_FOR_CONNECTION,
OFFLINE,
CONNECTING,
ADDING_CONTACT,
FAILED

View File

@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.transport.TransportKeySet;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -77,7 +78,7 @@ public interface DatabaseComponent extends TransactionManager {
* Stores a local message.
*/
void addLocalMessage(Transaction txn, Message m, Metadata meta,
boolean shared) throws DbException;
boolean shared, boolean temporary) throws DbException;
/**
* Stores a pending contact.
@@ -427,6 +428,13 @@ public interface DatabaseComponent extends TransactionManager {
*/
Settings getSettings(Transaction txn, String namespace) throws DbException;
/**
* Returns the versions of the sync protocol supported by the given contact.
* <p/>
* Read-only.
*/
List<Byte> getSyncVersions(Transaction txn, ContactId c) throws DbException;
/**
* Returns all transport keys for the given transport.
* <p/>
@@ -510,6 +518,12 @@ public interface DatabaseComponent extends TransactionManager {
void removePendingContact(Transaction txn, PendingContactId p)
throws DbException;
/**
* Removes all temporary messages (and all associated state) from the
* database.
*/
void removeTemporaryMessages(Transaction txn) throws DbException;
/**
* Removes a transport (and all associated state) from the database.
*/
@@ -538,6 +552,11 @@ public interface DatabaseComponent extends TransactionManager {
void setGroupVisibility(Transaction txn, ContactId c, GroupId g,
Visibility v) throws DbException;
/**
* Marks the given message as permanent, i.e. not temporary.
*/
void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
/**
* Marks the given message as shared.
*/
@@ -568,6 +587,12 @@ public interface DatabaseComponent extends TransactionManager {
void setReorderingWindow(Transaction txn, KeySetId k, TransportId t,
long timePeriod, long base, byte[] bitmap) throws DbException;
/**
* Sets the versions of the sync protocol supported by the given contact.
*/
void setSyncVersions(Transaction txn, ContactId c, List<Byte> supported)
throws DbException;
/**
* Marks the given transport keys as usable for outgoing streams.
*/

View File

@@ -1,10 +1,16 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
/**
* A record acknowledging receipt of one or more {@link Message Messages}.
*/
@Immutable
@NotNullByDefault
public class Ack {
private final Collection<MessageId> acked;

View File

@@ -1,8 +1,14 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@Immutable
@NotNullByDefault
public class Message {
/**

View File

@@ -1,10 +1,16 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
/**
* A record offering the recipient one or more {@link Message Messages}.
*/
@Immutable
@NotNullByDefault
public class Offer {
private final Collection<MessageId> offered;

View File

@@ -9,5 +9,5 @@ public interface RecordTypes {
byte MESSAGE = 1;
byte OFFER = 2;
byte REQUEST = 3;
byte VERSIONS = 4;
}

View File

@@ -1,10 +1,16 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
/**
* A record requesting one or more {@link Message Messages} from the recipient.
*/
@Immutable
@NotNullByDefault
public class Request {
private final Collection<MessageId> requested;

View File

@@ -2,6 +2,9 @@ package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.UniqueId;
import java.util.List;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
public interface SyncConstants {
@@ -11,6 +14,11 @@ public interface SyncConstants {
*/
byte PROTOCOL_VERSION = 0;
/**
* The versions of the sync protocol this peer supports.
*/
List<Byte> SUPPORTED_VERSIONS = singletonList(PROTOCOL_VERSION);
/**
* The maximum length of a group descriptor in bytes.
*/
@@ -35,4 +43,10 @@ public interface SyncConstants {
* The maximum number of message IDs in an ack, offer or request record.
*/
int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH;
/**
* The maximum number of versions of the sync protocol a peer may support
* simultaneously.
*/
int MAX_SUPPORTED_VERSIONS = 10;
}

View File

@@ -25,4 +25,7 @@ public interface SyncRecordReader {
Request readRequest() throws IOException;
boolean hasVersions() throws IOException;
Versions readVersions() throws IOException;
}

View File

@@ -15,5 +15,7 @@ public interface SyncRecordWriter {
void writeRequest(Request r) throws IOException;
void writeVersions(Versions v) throws IOException;
void flush() throws IOException;
}

View File

@@ -0,0 +1,26 @@
package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.List;
import javax.annotation.concurrent.Immutable;
/**
* A record telling the recipient which versions of the sync protocol the
* sender supports.
*/
@Immutable
@NotNullByDefault
public class Versions {
private final List<Byte> supported;
public Versions(List<Byte> supported) {
this.supported = supported;
}
public List<Byte> getSupportedVersions() {
return supported;
}
}

View File

@@ -0,0 +1,34 @@
package org.briarproject.bramble.api.sync.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.List;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when the versions of the sync protocol supported
* by a contact are updated.
*/
@Immutable
@NotNullByDefault
public class SyncVersionsUpdatedEvent extends Event {
private final ContactId contactId;
private final List<Byte> supported;
public SyncVersionsUpdatedEvent(ContactId contactId, List<Byte> supported) {
this.contactId = contactId;
this.supported = supported;
}
public ContactId getContactId() {
return contactId;
}
public List<Byte> getSupportedVersions() {
return supported;
}
}

View File

@@ -85,14 +85,21 @@ class ClientHelperImpl implements ClientHelper {
@Override
public void addLocalMessage(Message m, BdfDictionary metadata,
boolean shared) throws DbException, FormatException {
db.transaction(false, txn -> addLocalMessage(txn, m, metadata, shared));
db.transaction(false, txn -> addLocalMessage(txn, m, metadata, shared,
false));
}
@Override
public void addLocalMessage(Transaction txn, Message m,
BdfDictionary metadata, boolean shared)
BdfDictionary metadata, boolean shared, boolean temporary)
throws DbException, FormatException {
db.addLocalMessage(txn, m, metadataEncoder.encode(metadata), shared);
db.addLocalMessage(txn, m, metadataEncoder.encode(metadata), shared,
temporary);
}
@Override
public Message createMessage(GroupId g, long timestamp, byte[] body) {
return messageFactory.createMessage(g, timestamp, body);
}
@Override

View File

@@ -147,7 +147,6 @@ class ContactManagerImpl implements ContactManager, EventListener {
} finally {
db.endTransaction(txn);
}
states.put(p.getId(), WAITING_FOR_CONNECTION);
return p;
}

View File

@@ -33,6 +33,7 @@ import org.briarproject.bramble.api.transport.TransportKeySet;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -115,7 +116,7 @@ interface Database<T> {
* if the message was created locally.
*/
void addMessage(T txn, Message m, MessageState state, boolean shared,
@Nullable ContactId sender) throws DbException;
boolean temporary, @Nullable ContactId sender) throws DbException;
/**
* Adds a dependency between two messages, where the dependent message is
@@ -528,6 +529,13 @@ interface Database<T> {
*/
Settings getSettings(T txn, String namespace) throws DbException;
/**
* Returns the versions of the sync protocol supported by the given contact.
* <p/>
* Read-only.
*/
List<Byte> getSyncVersions(T txn, ContactId c) throws DbException;
/**
* Returns all transport keys for the given transport.
* <p/>
@@ -630,6 +638,12 @@ interface Database<T> {
*/
void removePendingContact(T txn, PendingContactId p) throws DbException;
/**
* Removes all temporary messages (and all associated state) from the
* database.
*/
void removeTemporaryMessages(T txn) throws DbException;
/**
* Removes a transport (and all associated state) from the database.
*/
@@ -671,6 +685,11 @@ interface Database<T> {
void setHandshakeKeyPair(T txn, AuthorId local, PublicKey publicKey,
PrivateKey privateKey) throws DbException;
/**
* Marks the given message as permanent, i.e. not temporary.
*/
void setMessagePermanent(T txn, MessageId m) throws DbException;
/**
* Marks the given message as shared.
*/
@@ -689,6 +708,12 @@ interface Database<T> {
void setReorderingWindow(T txn, KeySetId k, TransportId t,
long timePeriod, long base, byte[] bitmap) throws DbException;
/**
* Sets the versions of the sync protocol supported by the given contact.
*/
void setSyncVersions(T txn, ContactId c, List<Byte> supported)
throws DbException;
/**
* Marks the given transport keys as usable for outgoing streams.
*/

View File

@@ -65,6 +65,7 @@ import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
import org.briarproject.bramble.api.sync.event.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.sync.event.SyncVersionsUpdatedEvent;
import org.briarproject.bramble.api.sync.validation.MessageState;
import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeySet;
@@ -273,13 +274,14 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Override
public void addLocalMessage(Transaction transaction, Message m,
Metadata meta, boolean shared) throws DbException {
Metadata meta, boolean shared, boolean temporary)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsGroup(txn, m.getGroupId()))
throw new NoSuchGroupException();
if (!db.containsMessage(txn, m.getId())) {
db.addMessage(txn, m, DELIVERED, shared, null);
db.addMessage(txn, m, DELIVERED, shared, temporary, null);
transaction.attach(new MessageAddedEvent(m, null));
transaction.attach(new MessageStateChangedEvent(m.getId(), true,
DELIVERED));
@@ -715,6 +717,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getSettings(txn, namespace);
}
@Override
public List<Byte> getSyncVersions(Transaction transaction, ContactId c)
throws DbException {
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
return db.getSyncVersions(txn, c);
}
@Override
public Collection<TransportKeySet> getTransportKeys(Transaction transaction,
TransportId t) throws DbException {
@@ -800,7 +811,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.raiseSeenFlag(txn, c, m.getId());
db.raiseAckFlag(txn, c, m.getId());
} else {
db.addMessage(txn, m, UNKNOWN, false, c);
db.addMessage(txn, m, UNKNOWN, false, false, c);
transaction.attach(new MessageAddedEvent(m, c));
}
transaction.attach(new MessageToAckEvent(c));
@@ -908,6 +919,14 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
transaction.attach(new PendingContactRemovedEvent(p));
}
@Override
public void removeTemporaryMessages(Transaction transaction)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
db.removeTemporaryMessages(txn);
}
@Override
public void removeTransport(Transaction transaction, TransportId t)
throws DbException {
@@ -967,6 +986,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
}
@Override
public void setMessagePermanent(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.setMessagePermanent(txn, m);
}
@Override
public void setMessageShared(Transaction transaction, MessageId m)
throws DbException {
@@ -975,8 +1004,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
if (db.getMessageState(txn, m) != DELIVERED)
throw new IllegalArgumentException(
"Shared undelivered message");
throw new IllegalArgumentException("Shared undelivered message");
db.setMessageShared(txn, m);
transaction.attach(new MessageSharedEvent(m));
}
@@ -1028,6 +1056,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.setReorderingWindow(txn, k, t, timePeriod, base, bitmap);
}
@Override
public void setSyncVersions(Transaction transaction, ContactId c,
List<Byte> supported) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
db.setSyncVersions(txn, c, supported);
transaction.attach(new SyncVersionsUpdatedEvent(c, supported));
}
@Override
public void setTransportKeysActive(Transaction transaction, TransportId t,
KeySetId k) throws DbException {

View File

@@ -62,6 +62,7 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import static java.sql.Types.BINARY;
import static java.sql.Types.BOOLEAN;
@@ -97,7 +98,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing
static final int CODE_SCHEMA_VERSION = 45;
static final int CODE_SCHEMA_VERSION = 47;
// Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1;
@@ -134,6 +135,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " handshakePublicKey _BINARY," // Null if key is unknown
+ " localAuthorId _HASH NOT NULL,"
+ " verified BOOLEAN NOT NULL,"
+ " syncVersions _BINARY DEFAULT '00' NOT NULL,"
+ " PRIMARY KEY (contactId),"
+ " FOREIGN KEY (localAuthorId)"
+ " REFERENCES localAuthors (authorId)"
@@ -177,6 +179,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " timestamp BIGINT NOT NULL,"
+ " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL,"
+ " temporary BOOLEAN NOT NULL,"
+ " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId),"
@@ -336,25 +339,26 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final Logger LOG =
getLogger(JdbcDatabase.class.getName());
// Different database libraries use different names for certain types
private final MessageFactory messageFactory;
private final Clock clock;
private final DatabaseTypes dbTypes;
// Locking: connectionsLock
private final Lock connectionsLock = new ReentrantLock();
private final Condition connectionsChanged = connectionsLock.newCondition();
@GuardedBy("connectionsLock")
private final LinkedList<Connection> connections = new LinkedList<>();
private int openConnections = 0; // Locking: connectionsLock
private boolean closed = false; // Locking: connectionsLock
@GuardedBy("connectionsLock")
private int openConnections = 0;
@GuardedBy("connectionsLock")
private boolean closed = false;
protected abstract Connection createConnection()
throws DbException, SQLException;
protected abstract void compactAndClose() throws DbException;
private final Lock connectionsLock = new ReentrantLock();
private final Condition connectionsChanged = connectionsLock.newCondition();
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
Clock clock) {
this.dbTypes = databaseTypes;
@@ -457,7 +461,9 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration41_42(dbTypes),
new Migration42_43(dbTypes),
new Migration43_44(dbTypes),
new Migration44_45()
new Migration44_45(),
new Migration45_46(),
new Migration46_47(dbTypes)
);
}
@@ -777,22 +783,23 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override
public void addMessage(Connection txn, Message m, MessageState state,
boolean messageShared, @Nullable ContactId sender)
boolean shared, boolean temporary, @Nullable ContactId sender)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
+ " state, shared, length, raw)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
+ " state, shared, temporary, length, raw)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes());
ps.setBytes(2, m.getGroupId().getBytes());
ps.setLong(3, m.getTimestamp());
ps.setInt(4, state.getValue());
ps.setBoolean(5, messageShared);
ps.setBoolean(5, shared);
ps.setBoolean(6, temporary);
byte[] raw = messageFactory.getRawMessage(m);
ps.setInt(6, raw.length);
ps.setBytes(7, raw);
ps.setInt(7, raw.length);
ps.setBytes(8, raw);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
@@ -804,8 +811,7 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean offered = removeOfferedMessage(txn, c, m.getId());
boolean seen = offered || c.equals(sender);
addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(),
raw.length, state, e.getValue(), messageShared,
false, seen);
raw.length, state, e.getValue(), shared, false, seen);
}
// Update denormalised column in messageDependencies if dependency
// is in same group as dependent
@@ -2324,6 +2330,32 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public List<Byte> getSyncVersions(Connection txn, ContactId c)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT syncVersions FROM contacts"
+ " WHERE contactId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
byte[] bytes = rs.getBytes(1);
List<Byte> supported = new ArrayList<>(bytes.length);
for (byte b : bytes) supported.add(b);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return supported;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public Collection<TransportKeySet> getTransportKeys(Connection txn,
TransportId t) throws DbException {
@@ -2876,6 +2908,21 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void removeTemporaryMessages(Connection txn) throws DbException {
Statement s = null;
try {
String sql = "DELETE FROM messages WHERE temporary = TRUE";
s = txn.createStatement();
int affected = s.executeUpdate(sql);
if (affected < 0) throw new DbStateException();
s.close();
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void removeTransport(Connection txn, TransportId t)
throws DbException {
@@ -3021,6 +3068,24 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void setMessagePermanent(Connection txn, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET temporary = FALSE"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void setMessageShared(Connection txn, MessageId m)
throws DbException {
@@ -3124,6 +3189,29 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public void setSyncVersions(Connection txn, ContactId c,
List<Byte> supported) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE contacts SET syncVersions = ?"
+ " WHERE contactId = ?";
ps = txn.prepareStatement(sql);
byte[] bytes = new byte[supported.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = supported.get(i);
}
ps.setBytes(1, bytes);
ps.setInt(2, c.getInt());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void setTransportKeysActive(Connection txn, TransportId t,
KeySetId k) throws DbException {

View File

@@ -0,0 +1,40 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
class Migration45_46 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration45_46.class.getName());
@Override
public int getStartVersion() {
return 45;
}
@Override
public int getEndVersion() {
return 46;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
s.execute("ALTER TABLE messages"
+ " ADD COLUMN temporary BOOLEAN DEFAULT FALSE NOT NULL");
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -0,0 +1,47 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
class Migration46_47 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration46_47.class.getName());
private final DatabaseTypes dbTypes;
Migration46_47(DatabaseTypes dbTypes) {
this.dbTypes = dbTypes;
}
@Override
public int getStartVersion() {
return 46;
}
@Override
public int getEndVersion() {
return 47;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
s.execute(dbTypes.replaceTypes("ALTER TABLE contacts"
+ " ADD COLUMN syncVersions"
+ " _BINARY DEFAULT '00' NOT NULL"));
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -107,8 +107,11 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
else logDuration(LOG, "Creating database", start);
db.transaction(false, txn -> {
long start1 = now();
db.removeTemporaryMessages(txn);
logDuration(LOG, "Removing temporary messages", start1);
for (OpenDatabaseHook hook : openDatabaseHooks) {
long start1 = now();
start1 = now();
hook.onDatabaseOpened(txn);
if (LOG.isLoggable(FINE)) {
logDuration(LOG, "Calling open database hook "

View File

@@ -284,7 +284,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
meta.put("transportId", t.getString());
meta.put("version", version);
meta.put("local", local);
clientHelper.addLocalMessage(txn, m, meta, shared);
clientHelper.addLocalMessage(txn, m, meta, shared, false);
} catch (FormatException e) {
throw new RuntimeException(e);
}

View File

@@ -66,6 +66,7 @@ import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT;
import static org.briarproject.bramble.api.contact.PendingContactState.FAILED;
import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNull;
import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS;
@@ -158,9 +159,7 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
private void addPendingContact(PendingContact p) {
long now = clock.currentTimeMillis();
long expiry = p.getTimestamp() + RENDEZVOUS_TIMEOUT_MS;
if (expiry > now) {
broadcastState(p.getId(), WAITING_FOR_CONNECTION);
} else {
if (expiry <= now) {
broadcastState(p.getId(), FAILED);
return;
}
@@ -180,9 +179,13 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
for (PluginState ps : pluginStates.values()) {
RendezvousEndpoint endpoint =
createEndpoint(ps.plugin, p.getId(), cs);
if (endpoint != null)
if (endpoint != null) {
requireNull(ps.endpoints.put(p.getId(), endpoint));
cs.numEndpoints++;
}
}
if (cs.numEndpoints == 0) broadcastState(p.getId(), OFFLINE);
else broadcastState(p.getId(), WAITING_FOR_CONNECTION);
} catch (DbException | GeneralSecurityException e) {
logException(LOG, WARNING, e);
}
@@ -328,9 +331,14 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
TransportId t = plugin.getId();
Map<PendingContactId, RendezvousEndpoint> endpoints = new HashMap<>();
for (Entry<PendingContactId, CryptoState> e : cryptoStates.entrySet()) {
RendezvousEndpoint endpoint =
createEndpoint(plugin, e.getKey(), e.getValue());
if (endpoint != null) endpoints.put(e.getKey(), endpoint);
PendingContactId p = e.getKey();
CryptoState cs = e.getValue();
RendezvousEndpoint endpoint = createEndpoint(plugin, p, cs);
if (endpoint != null) {
endpoints.put(p, endpoint);
if (++cs.numEndpoints == 1)
broadcastState(p, WAITING_FOR_CONNECTION);
}
}
requireNull(pluginStates.put(t, new PluginState(plugin, endpoints)));
}
@@ -344,8 +352,11 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
private void removeTransport(TransportId t) {
PluginState ps = pluginStates.remove(t);
if (ps != null) {
for (RendezvousEndpoint endpoint : ps.endpoints.values()) {
tryToClose(endpoint, LOG, INFO);
for (Entry<PendingContactId, RendezvousEndpoint> e :
ps.endpoints.entrySet()) {
tryToClose(e.getValue(), LOG, INFO);
CryptoState cs = cryptoStates.get(e.getKey());
if (--cs.numEndpoints == 0) broadcastState(e.getKey(), OFFLINE);
}
}
}
@@ -391,6 +402,8 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
private final boolean alice;
private final long expiry;
private int numEndpoints = 0;
private CryptoState(SecretKey rendezvousKey, boolean alice,
long expiry) {
this.rendezvousKey = rendezvousKey;

View File

@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
@@ -39,9 +40,11 @@ import javax.annotation.concurrent.ThreadSafe;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.SUPPORTED_VERSIONS;
import static org.briarproject.bramble.util.LogUtils.logException;
/**
@@ -55,7 +58,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
class DuplexOutgoingSession implements SyncSession, EventListener {
private static final Logger LOG =
Logger.getLogger(DuplexOutgoingSession.class.getName());
getLogger(DuplexOutgoingSession.class.getName());
private static final ThrowingRunnable<IOException> CLOSE = () -> {
};
@@ -103,6 +106,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
public void run() throws IOException {
eventBus.addListener(this);
try {
// Send our supported protocol versions
recordWriter.writeVersions(new Versions(SUPPORTED_VERSIONS));
// Start a query for each type of record
generateAck();
generateBatch();

View File

@@ -18,14 +18,17 @@ import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -37,7 +40,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
class IncomingSession implements SyncSession, EventListener {
private static final Logger LOG =
Logger.getLogger(IncomingSession.class.getName());
getLogger(IncomingSession.class.getName());
private final DatabaseComponent db;
private final Executor dbExecutor;
@@ -80,6 +83,9 @@ class IncomingSession implements SyncSession, EventListener {
} else if (recordReader.hasRequest()) {
Request r = recordReader.readRequest();
dbExecutor.execute(new ReceiveRequest(r));
} else if (recordReader.hasVersions()) {
Versions v = recordReader.readVersions();
dbExecutor.execute(new ReceiveVersions(v));
} else {
// unknown records are ignored in RecordReader#eof()
throw new FormatException();
@@ -190,4 +196,26 @@ class IncomingSession implements SyncSession, EventListener {
}
}
}
private class ReceiveVersions implements Runnable {
private final Versions versions;
private ReceiveVersions(Versions versions) {
this.versions = versions;
}
@DatabaseExecutor
@Override
public void run() {
try {
List<Byte> supported = versions.getSupportedVersions();
db.transaction(false,
txn -> db.setSyncVersions(txn, contactId, supported));
} catch (DbException e) {
logException(LOG, WARNING, e);
interrupt();
}
}
}
}

View File

@@ -15,6 +15,7 @@ import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.transport.StreamWriter;
import java.io.IOException;
@@ -29,9 +30,11 @@ import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.SUPPORTED_VERSIONS;
import static org.briarproject.bramble.util.LogUtils.logException;
/**
@@ -44,7 +47,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
class SimplexOutgoingSession implements SyncSession, EventListener {
private static final Logger LOG =
Logger.getLogger(SimplexOutgoingSession.class.getName());
getLogger(SimplexOutgoingSession.class.getName());
private static final ThrowingRunnable<IOException> CLOSE = () -> {
};
@@ -80,6 +83,8 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
public void run() throws IOException {
eventBus.addListener(this);
try {
// Send our supported protocol versions
recordWriter.writeVersions(new Versions(SUPPORTED_VERSIONS));
// Start a query for each type of record
dbExecutor.execute(new GenerateAck());
dbExecutor.execute(new GenerateBatch());

View File

@@ -13,6 +13,7 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.util.ByteUtils;
import java.io.IOException;
@@ -26,6 +27,8 @@ 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.RecordTypes.VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_SUPPORTED_VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@@ -45,7 +48,7 @@ class SyncRecordReaderImpl implements SyncRecordReader {
private static boolean isKnownRecordType(byte type) {
return type == ACK || type == MESSAGE || type == OFFER ||
type == REQUEST;
type == REQUEST || type == VERSIONS;
}
private final MessageFactory messageFactory;
@@ -148,4 +151,27 @@ class SyncRecordReaderImpl implements SyncRecordReader {
if (!hasRequest()) throw new FormatException();
return new Request(readMessageIds());
}
@Override
public boolean hasVersions() throws IOException {
return !eof() && getNextRecordType() == VERSIONS;
}
@Override
public Versions readVersions() throws IOException {
if (!hasVersions()) throw new FormatException();
return new Versions(readSupportedVersions());
}
private List<Byte> readSupportedVersions() throws IOException {
if (nextRecord == null) throw new AssertionError();
byte[] payload = nextRecord.getPayload();
if (payload.length == 0) throw new FormatException();
if (payload.length > MAX_SUPPORTED_VERSIONS)
throw new FormatException();
List<Byte> supported = new ArrayList<>(payload.length);
for (byte b : payload) supported.add(b);
nextRecord = null;
return supported;
}
}

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.Versions;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -20,6 +21,7 @@ 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.RecordTypes.VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@NotThreadSafe
@@ -65,6 +67,12 @@ class SyncRecordWriterImpl implements SyncRecordWriter {
writeRecord(REQUEST);
}
@Override
public void writeVersions(Versions v) throws IOException {
for (byte b : v.getSupportedVersions()) payload.write(b);
writeRecord(VERSIONS);
}
@Override
public void flush() throws IOException {
writer.flush();

View File

@@ -314,6 +314,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
try {
shareMsg = hook.incomingMessage(txn, m, meta);
} catch (InvalidMessageException e) {
logException(LOG, INFO, e);
invalidateMessage(txn, m.getId());
return new DeliveryResult(false, false);
}

View File

@@ -243,7 +243,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
try {
Message m = clientHelper.createMessage(localGroup.getId(), now,
body);
db.addLocalMessage(txn, m, new Metadata(), false);
db.addLocalMessage(txn, m, new Metadata(), false, false);
} catch (FormatException e) {
throw new AssertionError(e);
}
@@ -438,7 +438,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_UPDATE_VERSION, updateVersion);
meta.put(MSG_KEY_LOCAL, true);
clientHelper.addLocalMessage(txn, m, meta, true);
clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) {
throw new RuntimeException(e);
}

View File

@@ -96,7 +96,7 @@ public class ClientHelperImplTest extends BrambleTestCase {
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(metadataEncoder).encode(dictionary);
will(returnValue(metadata));
oneOf(db).addLocalMessage(txn, message, metadata, shared);
oneOf(db).addLocalMessage(txn, message, metadata, shared, false);
}});
clientHelper.addLocalMessage(message, dictionary, shared);

View File

@@ -6,11 +6,15 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactState;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.Identity;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestDuplexTransportConnection;
@@ -27,7 +31,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.fail;
import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.test.TestDuplexTransportConnection.createPair;
import static org.briarproject.bramble.test.TestPluginConfigModule.DUPLEX_TRANSPORT_ID;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
@@ -188,9 +192,14 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
private PendingContact addPendingContact(
ContactExchangeIntegrationTestComponent local,
ContactExchangeIntegrationTestComponent remote) throws Exception {
EventWaiter waiter = new EventWaiter();
local.getEventBus().addListener(waiter);
String link = remote.getContactManager().getHandshakeLink();
String alias = remote.getIdentityManager().getLocalAuthor().getName();
return local.getContactManager().addPendingContact(link, alias);
PendingContact pendingContact =
local.getContactManager().addPendingContact(link, alias);
waiter.latch.await(TIMEOUT, MILLISECONDS);
return pendingContact;
}
private void assertContacts(boolean verified,
@@ -237,7 +246,7 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
assertEquals(1, pairs.size());
Pair<PendingContact, PendingContactState> pair =
pairs.iterator().next();
assertEquals(WAITING_FOR_CONNECTION, pair.getSecond());
assertEquals(OFFLINE, pair.getSecond());
PendingContact pendingContact = pair.getFirst();
assertEquals(expectedIdentity.getLocalAuthor().getName(),
pendingContact.getAlias());
@@ -261,4 +270,19 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
tearDown(bob);
deleteTestDirectory(testDir);
}
@NotNullByDefault
private static class EventWaiter implements EventListener {
private final CountDownLatch latch = new CountDownLatch(1);
@Override
public void eventOccurred(Event e) {
if (e instanceof PendingContactStateChangedEvent) {
PendingContactStateChangedEvent p =
(PendingContactStateChangedEvent) e;
if (p.getPendingContactState() == OFFLINE) latch.countDown();
}
}
}
}

View File

@@ -61,10 +61,12 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
@@ -120,6 +122,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
private final Contact contact;
private final KeySetId keySetId;
private final PendingContactId pendingContactId;
private final Random random = new Random();
private final boolean shared = random.nextBoolean();
private final boolean temporary = random.nextBoolean();
public DatabaseComponentImplTest() {
clientId = getClientId();
@@ -253,7 +258,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager);
db.transaction(false, transaction ->
db.addLocalMessage(transaction, message, metadata, true));
db.addLocalMessage(transaction, message, metadata, shared,
temporary));
}
@Test
@@ -265,20 +271,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
oneOf(database).addMessage(txn, message, DELIVERED, true, null);
oneOf(database).addMessage(txn, message, DELIVERED, shared,
temporary, null);
oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
oneOf(database).commitTransaction(txn);
// The message was added, so the listeners should be called
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
oneOf(eventBus)
.broadcast(with(any(MessageStateChangedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
oneOf(eventBus).broadcast(with(any(
MessageStateChangedEvent.class)));
if (shared)
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
db.transaction(false, transaction ->
db.addLocalMessage(transaction, message, metadata, true));
db.addLocalMessage(transaction, message, metadata, shared,
temporary));
}
@Test
@@ -286,11 +295,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not)
exactly(16).of(database).startTransaction();
exactly(18).of(database).startTransaction();
will(returnValue(txn));
exactly(16).of(database).containsContact(txn, contactId);
exactly(18).of(database).containsContact(txn, contactId);
will(returnValue(false));
exactly(16).of(database).abortTransaction(txn);
exactly(18).of(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
@@ -368,6 +377,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected
}
try {
db.transaction(false, transaction ->
db.getSyncVersions(transaction, contactId));
fail();
} catch (NoSuchContactException expected) {
// Expected
}
try {
Ack a = new Ack(singletonList(messageId));
db.transaction(false, transaction ->
@@ -427,6 +444,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} catch (NoSuchContactException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.setSyncVersions(transaction, contactId, emptyList()));
fail();
} catch (NoSuchContactException expected) {
// Expected
}
}
@Test
@@ -569,11 +594,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the message is in the DB (which it's not)
exactly(11).of(database).startTransaction();
exactly(12).of(database).startTransaction();
will(returnValue(txn));
exactly(11).of(database).containsMessage(txn, messageId);
exactly(12).of(database).containsMessage(txn, messageId);
will(returnValue(false));
exactly(11).of(database).abortTransaction(txn);
exactly(12).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -637,6 +662,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected
}
try {
db.transaction(false, transaction ->
db.setMessagePermanent(transaction, message.getId()));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.setMessageShared(transaction, message.getId()));
@@ -972,7 +1005,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(VISIBLE));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
oneOf(database).addMessage(txn, message, UNKNOWN, false, contactId);
oneOf(database).addMessage(txn, message, UNKNOWN, false, false,
contactId);
// Second time
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -1507,6 +1541,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
public void testMessageDependencies() throws Exception {
int shutdownHandle = 12345;
MessageId messageId2 = new MessageId(getRandomId());
context.checking(new Expectations() {{
// open()
oneOf(database).open(key, null);
@@ -1521,7 +1556,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true));
oneOf(database).containsMessage(txn, messageId);
will(returnValue(false));
oneOf(database).addMessage(txn, message, DELIVERED, true, null);
oneOf(database).addMessage(txn, message, DELIVERED, shared,
temporary, null);
oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
// addMessageDependencies()
oneOf(database).containsMessage(txn, messageId);
@@ -1544,7 +1580,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
oneOf(eventBus).broadcast(with(any(
MessageStateChangedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
if (shared)
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
// endTransaction()
oneOf(database).commitTransaction(txn);
// close()
@@ -1555,7 +1592,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
assertFalse(db.open(key, null));
db.transaction(false, transaction -> {
db.addLocalMessage(transaction, message, metadata, true);
db.addLocalMessage(transaction, message, metadata, shared,
temporary);
Collection<MessageId> dependencies = new ArrayList<>(2);
dependencies.add(messageId1);
dependencies.add(messageId2);

View File

@@ -567,8 +567,9 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
MessageState state =
MessageState.fromValue(random.nextInt(4));
boolean shared = random.nextBoolean();
boolean temporary = random.nextBoolean();
ContactId sender = random.nextBoolean() ? c : null;
db.addMessage(txn, m, state, shared, sender);
db.addMessage(txn, m, state, shared, temporary, sender);
if (random.nextBoolean())
db.raiseRequestedFlag(txn, c, m.getId());
Metadata mm = getMetadata(METADATA_KEYS_PER_MESSAGE);
@@ -597,7 +598,8 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
for (int j = 0; j < MESSAGES_PER_GROUP; j++) {
Message m = getMessage(g.getId());
messages.add(m);
db.addMessage(txn, m, DELIVERED, false, null);
boolean temporary = random.nextBoolean();
db.addMessage(txn, m, DELIVERED, false, temporary, null);
Metadata mm = getMetadata(METADATA_KEYS_PER_MESSAGE);
messageMeta.get(g.getId()).add(mm);
db.mergeMessageMetadata(txn, m.getId(), mm);

View File

@@ -41,7 +41,6 @@ import org.junit.Test;
import java.io.File;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -51,6 +50,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
@@ -153,7 +153,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addGroup(txn, group);
assertTrue(db.containsGroup(txn, groupId));
assertFalse(db.containsMessage(txn, messageId));
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
assertTrue(db.containsMessage(txn, messageId));
db.commitTransaction(txn);
db.close();
@@ -191,7 +191,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Removing the group should remove the message
assertTrue(db.containsMessage(txn, messageId));
@@ -213,7 +213,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// The contact has not seen the message, so it should be sendable
Collection<MessageId> ids =
@@ -244,7 +244,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, UNKNOWN, true, null);
db.addMessage(txn, message, UNKNOWN, true, false, null);
// The message has not been validated, so it should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -288,7 +288,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// The group is invisible, so the message should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -340,7 +340,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, false, null);
db.addMessage(txn, message, DELIVERED, false, false, null);
// The message is not shared, so it should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -371,7 +371,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// The message is sendable, but too large to send
Collection<MessageId> ids =
@@ -402,15 +402,15 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add some messages to ack
Message message1 = getMessage(groupId);
MessageId messageId1 = message1.getId();
db.addMessage(txn, message, DELIVERED, true, contactId);
db.addMessage(txn, message1, DELIVERED, true, contactId);
db.addMessage(txn, message, DELIVERED, true, false, contactId);
db.addMessage(txn, message1, DELIVERED, true, false, contactId);
// Both message IDs should be returned
Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
assertEquals(Arrays.asList(messageId, messageId1), ids);
assertEquals(asList(messageId, messageId1), ids);
// Remove both message IDs
db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1));
db.lowerAckFlag(txn, contactId, asList(messageId, messageId1));
// Both message IDs should have been removed
assertEquals(emptyList(), db.getMessagesToAck(txn,
@@ -422,7 +422,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Both message IDs should be returned
ids = db.getMessagesToAck(txn, contactId, 1234);
assertEquals(Arrays.asList(messageId, messageId1), ids);
assertEquals(asList(messageId, messageId1), ids);
db.commitTransaction(txn);
db.close();
@@ -439,7 +439,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Retrieve the message from the database and mark it as sent
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -608,7 +608,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// The group is not visible so the message should not be visible
assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
@@ -1223,7 +1223,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Attach some metadata to the message
Metadata metadata = new Metadata();
@@ -1294,7 +1294,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Attach some metadata to the message
Metadata metadata = new Metadata();
@@ -1355,8 +1355,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and two messages
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message1, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
db.addMessage(txn, message1, DELIVERED, true, false, null);
// Attach some metadata to the messages
Metadata metadata = new Metadata();
@@ -1459,8 +1459,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and two messages
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message1, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
db.addMessage(txn, message1, DELIVERED, true, false, null);
// Attach some metadata to the messages
Metadata metadata = new Metadata();
@@ -1536,9 +1536,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and some messages
db.addGroup(txn, group);
db.addMessage(txn, message, PENDING, true, contactId);
db.addMessage(txn, message1, PENDING, true, contactId);
db.addMessage(txn, message2, INVALID, true, contactId);
db.addMessage(txn, message, PENDING, true, false, contactId);
db.addMessage(txn, message1, PENDING, true, false, contactId);
db.addMessage(txn, message2, INVALID, true, false, contactId);
// Add dependencies
db.addMessageDependency(txn, message, messageId1, PENDING);
@@ -1589,7 +1589,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(0, dependents.size());
// Add message 3
db.addMessage(txn, message3, UNKNOWN, false, contactId);
db.addMessage(txn, message3, UNKNOWN, false, false, contactId);
// Message 3 has message 1 as a dependent
dependents = db.getMessageDependents(txn, messageId3);
@@ -1601,7 +1601,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(0, dependents.size());
// Add message 4
db.addMessage(txn, message4, UNKNOWN, false, contactId);
db.addMessage(txn, message4, UNKNOWN, false, false, contactId);
// Message 4 has message 2 as a dependent
dependents = db.getMessageDependents(txn, messageId4);
@@ -1619,7 +1619,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, PENDING, true, contactId);
db.addMessage(txn, message, PENDING, true, false, contactId);
// Add a second group
Group group1 = getGroup(clientId, 123);
@@ -1629,7 +1629,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a message to the second group
Message message1 = getMessage(groupId1);
MessageId messageId1 = message1.getId();
db.addMessage(txn, message1, DELIVERED, true, contactId);
db.addMessage(txn, message1, DELIVERED, true, false, contactId);
// Create an ID for a missing message
MessageId messageId2 = new MessageId(getRandomId());
@@ -1637,7 +1637,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add another message to the first group
Message message3 = getMessage(groupId);
MessageId messageId3 = message3.getId();
db.addMessage(txn, message3, DELIVERED, true, contactId);
db.addMessage(txn, message3, DELIVERED, true, false, contactId);
// Add dependencies between the messages
db.addMessageDependency(txn, message, messageId1, PENDING);
@@ -1680,10 +1680,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and some messages with different states
db.addGroup(txn, group);
db.addMessage(txn, message1, UNKNOWN, true, contactId);
db.addMessage(txn, message2, INVALID, true, contactId);
db.addMessage(txn, message3, PENDING, true, contactId);
db.addMessage(txn, message4, DELIVERED, true, contactId);
db.addMessage(txn, message1, UNKNOWN, true, false, contactId);
db.addMessage(txn, message2, INVALID, true, false, contactId);
db.addMessage(txn, message3, PENDING, true, false, contactId);
db.addMessage(txn, message4, DELIVERED, true, false, contactId);
Collection<MessageId> result;
@@ -1713,10 +1713,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and some messages
db.addGroup(txn, group);
db.addMessage(txn, message1, DELIVERED, true, contactId);
db.addMessage(txn, message2, DELIVERED, false, contactId);
db.addMessage(txn, message3, DELIVERED, false, contactId);
db.addMessage(txn, message4, DELIVERED, true, contactId);
db.addMessage(txn, message1, DELIVERED, true, false, contactId);
db.addMessage(txn, message2, DELIVERED, false, false, contactId);
db.addMessage(txn, message3, DELIVERED, false, false, contactId);
db.addMessage(txn, message4, DELIVERED, true, false, contactId);
// Introduce dependencies between the messages
db.addMessageDependency(txn, message1, message2.getId(), DELIVERED);
@@ -1744,7 +1744,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// The message should not be sent or seen
MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
@@ -1878,7 +1878,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// The message should be visible to the contact
assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
@@ -1961,7 +1961,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, UNKNOWN, false, contactId);
db.addMessage(txn, message, UNKNOWN, false, false, contactId);
// Walk the message through the validation and delivery states
assertEquals(UNKNOWN, db.getMessageState(txn, messageId));
@@ -1988,7 +1988,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addMessage(txn, message, UNKNOWN, false, null);
db.addMessage(txn, message, UNKNOWN, false, false, null);
// There should be no messages to send
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
@@ -2073,7 +2073,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Time: now
// Retrieve the message from the database
@@ -2118,7 +2118,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.addContact(txn, author, localAuthor.getId(), null, true));
db.addGroup(txn, group);
db.addGroupVisibility(txn, contactId, groupId, true);
db.addMessage(txn, message, DELIVERED, true, null);
db.addMessage(txn, message, DELIVERED, true, false, null);
// Time: now
// Retrieve the message from the database
@@ -2257,6 +2257,58 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close();
}
@Test
public void testTemporaryMessages() throws Exception {
Message message1 = getMessage(groupId);
MessageId messageId1 = message1.getId();
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a group and two temporary messages
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, false, true, null);
db.addMessage(txn, message1, DELIVERED, false, true, null);
// Mark one of the messages as permanent
db.setMessagePermanent(txn, messageId);
// Remove all temporary messages
db.removeTemporaryMessages(txn);
// The permanent message should not have been removed
assertTrue(db.containsMessage(txn, messageId));
// The temporary message should have been removed
assertFalse(db.containsMessage(txn, messageId1));
db.commitTransaction(txn);
db.close();
}
@Test
public void testSyncVersions() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact
db.addIdentity(txn, identity);
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), null, true));
// Only sync version 0 should be supported by default
List<Byte> defaultSupported = singletonList((byte) 0);
assertEquals(defaultSupported, db.getSyncVersions(txn, contactId));
// Set the supported versions and check that they're returned
List<Byte> supported = asList((byte) 0, (byte) 1);
db.setSyncVersions(txn, contactId, supported);
assertEquals(supported, db.getSyncVersions(txn, contactId));
db.commitTransaction(txn);
db.close();
}
private Database<Connection> open(boolean resume) throws Exception {
return open(resume, new TestMessageFactory(), new SystemClock());
}

View File

@@ -42,6 +42,7 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
oneOf(db).open(dbKey, lifecycleManager);
will(returnValue(false));
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(db).removeTemporaryMessages(txn);
allowing(eventBus).broadcast(with(any(LifecycleEvent.class)));
}});

View File

@@ -637,7 +637,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(timestamp));
oneOf(clientHelper).createMessage(g, timestamp, body);
will(returnValue(message));
oneOf(clientHelper).addLocalMessage(txn, message, meta, shared);
oneOf(clientHelper).addLocalMessage(txn, message, meta, shared,
false);
}});
}
}

View File

@@ -45,6 +45,7 @@ import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT;
import static org.briarproject.bramble.api.contact.PendingContactState.FAILED;
import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS;
import static org.briarproject.bramble.rendezvous.RendezvousConstants.RENDEZVOUS_TIMEOUT_MS;
@@ -120,7 +121,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
will(returnValue(beforeExpiry));
oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
PendingContactStateChangedEvent.class, e ->
e.getPendingContactState() == WAITING_FOR_CONNECTION)));
e.getPendingContactState() == OFFLINE)));
// Capture the poll task
oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)),
with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS),
@@ -184,7 +185,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied();
// Add the pending contact - endpoint should be created and polled
expectAddUnexpiredPendingContact(beforeExpiry);
expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION);
expectDeriveRendezvousKey();
expectCreateEndpoint();
@@ -205,9 +206,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied();
// Remove the pending contact - endpoint should be closed
context.checking(new Expectations() {{
oneOf(rendezvousEndpoint).close();
}});
expectCloseEndpoint();
rendezvousPoller.eventOccurred(
new PendingContactRemovedEvent(pendingContact.getId()));
@@ -238,7 +237,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied();
// Add the pending contact - endpoint should be created and polled
expectAddUnexpiredPendingContact(beforeExpiry);
expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION);
expectDeriveRendezvousKey();
expectCreateEndpoint();
@@ -260,10 +259,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
// Run the poll task - pending contact expires, endpoint is closed
expectPendingContactExpires(afterExpiry);
context.checking(new Expectations() {{
oneOf(rendezvousEndpoint).close();
}});
expectCloseEndpoint();
capturePollTask.get().run();
context.assertIsSatisfied();
@@ -289,7 +285,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied();
// Add the pending contact - no endpoints should be created yet
expectAddUnexpiredPendingContact(beforeExpiry);
expectAddPendingContact(beforeExpiry, OFFLINE);
expectDeriveRendezvousKey();
rendezvousPoller.eventOccurred(
@@ -299,14 +295,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
// Enable the transport - endpoint should be created
expectGetPlugin();
expectCreateEndpoint();
expectStateChangedEvent(WAITING_FOR_CONNECTION);
rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId));
context.assertIsSatisfied();
// Disable the transport - endpoint should be closed
context.checking(new Expectations() {{
oneOf(rendezvousEndpoint).close();
}});
expectCloseEndpoint();
expectStateChangedEvent(OFFLINE);
rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId));
context.assertIsSatisfied();
@@ -482,13 +478,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
return capturePollTask;
}
private void expectAddUnexpiredPendingContact(long now) {
private void expectAddPendingContact(long now,
PendingContactState initialState) {
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
PendingContactStateChangedEvent.class, e ->
e.getPendingContactState() == WAITING_FOR_CONNECTION)));
e.getPendingContactState() == initialState)));
}});
}
@@ -546,7 +543,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
will(returnValue(now));
oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
PendingContactStateChangedEvent.class, e ->
e.getPendingContactState() == WAITING_FOR_CONNECTION)));
e.getPendingContactState() == OFFLINE)));
// Capture the poll task
oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)),
with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS),
@@ -576,4 +573,10 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
e.getPendingContactState() == state)));
}});
}
private void expectCloseEndpoint() throws Exception {
context.checking(new Expectations() {{
oneOf(rendezvousEndpoint).close();
}});
}
}

View File

@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations;
@@ -49,6 +50,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{
// Add listener
oneOf(eventBus).addListener(session);
// Send the protocol versions
oneOf(recordWriter).writeVersions(with(any(Versions.class)));
// No acks to send
oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(noAckTxn));
@@ -83,6 +86,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{
// Add listener
oneOf(eventBus).addListener(session);
// Send the protocol versions
oneOf(recordWriter).writeVersions(with(any(Versions.class)));
// One ack to send
oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(ackTxn));

View File

@@ -10,11 +10,14 @@ import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.util.List;
import javax.annotation.Nullable;
@@ -22,7 +25,9 @@ import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTE
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.RecordTypes.VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_SUPPORTED_VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.junit.Assert.assertEquals;
@@ -35,12 +40,17 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
context.mock(MessageFactory.class);
private final RecordReader recordReader = context.mock(RecordReader.class);
private SyncRecordReader reader;
@Before
public void setUp() {
reader = new SyncRecordReaderImpl(messageFactory, recordReader);
}
@Test
public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception {
expectReadRecord(createAck());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
Ack ack = reader.readAck();
assertEquals(MAX_MESSAGE_IDS, ack.getMessageIds().size());
}
@@ -49,8 +59,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testFormatExceptionIfAckIsEmpty() throws Exception {
expectReadRecord(createEmptyAck());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
reader.readAck();
}
@@ -58,8 +66,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception {
expectReadRecord(createOffer());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
Offer offer = reader.readOffer();
assertEquals(MAX_MESSAGE_IDS, offer.getMessageIds().size());
}
@@ -68,8 +74,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testFormatExceptionIfOfferIsEmpty() throws Exception {
expectReadRecord(createEmptyOffer());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
reader.readOffer();
}
@@ -77,8 +81,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception {
expectReadRecord(createRequest());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
Request request = reader.readRequest();
assertEquals(MAX_MESSAGE_IDS, request.getMessageIds().size());
}
@@ -87,11 +89,36 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testFormatExceptionIfRequestIsEmpty() throws Exception {
expectReadRecord(createEmptyRequest());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
reader.readRequest();
}
@Test
public void testNoFormatExceptionIfVersionsIsMaximumSize()
throws Exception {
expectReadRecord(createVersions(MAX_SUPPORTED_VERSIONS));
Versions versions = reader.readVersions();
List<Byte> supported = versions.getSupportedVersions();
assertEquals(MAX_SUPPORTED_VERSIONS, supported.size());
for (int i = 0; i < supported.size(); i++) {
assertEquals(i, (int) supported.get(i));
}
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfVersionsIsEmpty() throws Exception {
expectReadRecord(createVersions(0));
reader.readVersions();
}
@Test(expected = FormatException.class)
public void testFormatExceptionIfVersionsIsTooLarge() throws Exception {
expectReadRecord(createVersions(MAX_SUPPORTED_VERSIONS + 1));
reader.readVersions();
}
@Test
public void testEofReturnsTrueWhenAtEndOfStream() throws Exception {
expectReadRecord(createAck());
@@ -140,6 +167,12 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
return new Record(PROTOCOL_VERSION, REQUEST, new byte[0]);
}
private Record createVersions(int numVersions) {
byte[] payload = new byte[numVersions];
for (int i = 0; i < payload.length; i++) payload[i] = (byte) i;
return new Record(PROTOCOL_VERSION, VERSIONS, payload);
}
private byte[] createPayload() throws Exception {
ByteArrayOutputStream payload = new ByteArrayOutputStream();
while (payload.size() + UniqueId.LENGTH <= MAX_RECORD_PAYLOAD_BYTES) {

View File

@@ -28,7 +28,6 @@ import java.util.Map;
import java.util.concurrent.Executor;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
@@ -80,24 +79,9 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test
public void testStartAndStop() throws Exception {
Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, true);
context.checking(new DbExpectations() {{
// validateOutstandingMessages()
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(emptyList()));
// deliverOutstandingMessages()
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getPendingMessages(txn1);
will(returnValue(emptyList()));
// shareOutstandingMessages()
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getMessagesToShare(txn2);
will(returnValue(emptyList()));
}});
expectGetMessagesToValidate();
expectGetPendingMessages();
expectGetMessagesToShare();
vm.startService();
vm.stopService();
@@ -106,167 +90,134 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test
public void testMessagesAreValidatedAtStartup() throws Exception {
Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, false);
Transaction txn3 = new Transaction(null, true);
Transaction txn4 = new Transaction(null, false);
Transaction txn5 = new Transaction(null, true);
Transaction txn6 = new Transaction(null, true);
Transaction txn1 = new Transaction(null, false);
Transaction txn2 = new Transaction(null, true);
Transaction txn3 = new Transaction(null, false);
expectGetMessagesToValidate(messageId, messageId1);
context.checking(new DbExpectations() {{
// Get messages to validate
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(asList(messageId, messageId1)));
// Load the first raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn1, messageId);
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessage(txn, messageId);
will(returnValue(message));
oneOf(db).getGroup(txn1, groupId);
oneOf(db).getGroup(txn, groupId);
will(returnValue(group));
// Validate the first message: valid
oneOf(validator).validateMessage(message, group);
will(returnValue(validResult));
// Store the validation result for the first message
oneOf(db).transaction(with(false), withDbRunnable(txn2));
oneOf(db).mergeMessageMetadata(txn2, messageId, metadata);
oneOf(db).transaction(with(false), withDbRunnable(txn1));
oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
// Deliver the first message
oneOf(hook).incomingMessage(txn2, message, metadata);
oneOf(hook).incomingMessage(txn1, message, metadata);
will(returnValue(false));
oneOf(db).setMessageState(txn2, messageId, DELIVERED);
oneOf(db).setMessageState(txn1, messageId, DELIVERED);
// Get any pending dependents
oneOf(db).getMessageDependents(txn2, messageId);
oneOf(db).getMessageDependents(txn1, messageId);
will(returnValue(emptyMap()));
// Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn3));
oneOf(db).getMessage(txn3, messageId1);
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getMessage(txn2, messageId1);
will(returnValue(message1));
oneOf(db).getGroup(txn3, groupId);
oneOf(db).getGroup(txn2, groupId);
will(returnValue(group));
// Validate the second message: invalid
oneOf(validator).validateMessage(message1, group);
will(throwException(new InvalidMessageException()));
// Store the validation result for the second message
oneOf(db).transaction(with(false), withDbRunnable(txn4));
oneOf(db).getMessageState(txn4, messageId1);
oneOf(db).transaction(with(false), withDbRunnable(txn3));
oneOf(db).getMessageState(txn3, messageId1);
will(returnValue(UNKNOWN));
oneOf(db).setMessageState(txn4, messageId1, INVALID);
oneOf(db).deleteMessage(txn4, messageId1);
oneOf(db).deleteMessageMetadata(txn4, messageId1);
oneOf(db).setMessageState(txn3, messageId1, INVALID);
oneOf(db).deleteMessage(txn3, messageId1);
oneOf(db).deleteMessageMetadata(txn3, messageId1);
// Recursively invalidate any dependents
oneOf(db).getMessageDependents(txn4, messageId1);
oneOf(db).getMessageDependents(txn3, messageId1);
will(returnValue(emptyMap()));
// Get pending messages to deliver
oneOf(db).transactionWithResult(with(true), withDbCallable(txn5));
oneOf(db).getPendingMessages(txn5);
will(returnValue(emptyList()));
// Get messages to share
oneOf(db).transactionWithResult(with(true), withDbCallable(txn6));
oneOf(db).getMessagesToShare(txn6);
will(returnValue(emptyList()));
}});
expectGetPendingMessages();
expectGetMessagesToShare();
vm.startService();
}
@Test
public void testPendingMessagesAreDeliveredAtStartup() throws Exception {
Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, false);
Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, true);
Transaction txn = new Transaction(null, false);
Transaction txn1 = new Transaction(null, false);
expectGetMessagesToValidate();
expectGetPendingMessages(messageId);
context.checking(new DbExpectations() {{
// Get messages to validate
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(emptyList()));
// Get pending messages to deliver
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getPendingMessages(txn1);
will(returnValue(singletonList(messageId)));
// Check whether the message is ready to deliver
oneOf(db).transaction(with(false), withDbRunnable(txn2));
oneOf(db).getMessageState(txn2, messageId);
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(db).getMessageState(txn, messageId);
will(returnValue(PENDING));
oneOf(db).getMessageDependencies(txn2, messageId);
oneOf(db).getMessageDependencies(txn, messageId);
will(returnValue(singletonMap(messageId1, DELIVERED)));
// Get the message and its metadata to deliver
oneOf(db).getMessage(txn2, messageId);
oneOf(db).getMessage(txn, messageId);
will(returnValue(message));
oneOf(db).getGroup(txn2, groupId);
oneOf(db).getGroup(txn, groupId);
will(returnValue(group));
oneOf(db).getMessageMetadataForValidator(txn2, messageId);
oneOf(db).getMessageMetadataForValidator(txn, messageId);
will(returnValue(new Metadata()));
// Deliver the message
oneOf(hook).incomingMessage(txn2, message, metadata);
oneOf(hook).incomingMessage(txn, message, metadata);
will(returnValue(false));
oneOf(db).setMessageState(txn2, messageId, DELIVERED);
oneOf(db).setMessageState(txn, messageId, DELIVERED);
// Get any pending dependents
oneOf(db).getMessageDependents(txn2, messageId);
oneOf(db).getMessageDependents(txn, messageId);
will(returnValue(singletonMap(messageId2, PENDING)));
// Check whether the dependent is ready to deliver
oneOf(db).transaction(with(false), withDbRunnable(txn3));
oneOf(db).getMessageState(txn3, messageId2);
oneOf(db).transaction(with(false), withDbRunnable(txn1));
oneOf(db).getMessageState(txn1, messageId2);
will(returnValue(PENDING));
oneOf(db).getMessageDependencies(txn3, messageId2);
oneOf(db).getMessageDependencies(txn1, messageId2);
will(returnValue(singletonMap(messageId1, DELIVERED)));
// Get the dependent and its metadata to deliver
oneOf(db).getMessage(txn3, messageId2);
oneOf(db).getMessage(txn1, messageId2);
will(returnValue(message2));
oneOf(db).getGroup(txn3, groupId);
oneOf(db).getGroup(txn1, groupId);
will(returnValue(group));
oneOf(db).getMessageMetadataForValidator(txn3, messageId2);
oneOf(db).getMessageMetadataForValidator(txn1, messageId2);
will(returnValue(metadata));
// Deliver the dependent
oneOf(hook).incomingMessage(txn3, message2, metadata);
oneOf(hook).incomingMessage(txn1, message2, metadata);
will(returnValue(false));
oneOf(db).setMessageState(txn3, messageId2, DELIVERED);
oneOf(db).setMessageState(txn1, messageId2, DELIVERED);
// Get any pending dependents
oneOf(db).getMessageDependents(txn3, messageId2);
oneOf(db).getMessageDependents(txn1, messageId2);
will(returnValue(emptyMap()));
// Get messages to share
oneOf(db).transactionWithResult(with(true), withDbCallable(txn4));
oneOf(db).getMessagesToShare(txn4);
will(returnValue(emptyList()));
}});
expectGetMessagesToShare();
vm.startService();
}
@Test
public void testMessagesAreSharedAtStartup() throws Exception {
Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, true);
Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, false);
Transaction txn = new Transaction(null, false);
Transaction txn1 = new Transaction(null, false);
expectGetMessagesToValidate();
expectGetPendingMessages();
expectGetMessagesToShare(messageId);
context.checking(new DbExpectations() {{
// No messages to validate
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(emptyList()));
// No pending messages to deliver
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getPendingMessages(txn1);
will(returnValue(emptyList()));
// Get messages to share
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getMessagesToShare(txn2);
will(returnValue(singletonList(messageId)));
// Share message and get dependencies
oneOf(db).transaction(with(false), withDbRunnable(txn3));
oneOf(db).setMessageShared(txn3, messageId);
oneOf(db).getMessageDependencies(txn3, messageId);
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(db).setMessageShared(txn, messageId);
oneOf(db).getMessageDependencies(txn, messageId);
will(returnValue(singletonMap(messageId2, DELIVERED)));
// Share dependency
oneOf(db).transaction(with(false), withDbRunnable(txn4));
oneOf(db).setMessageShared(txn4, messageId2);
oneOf(db).getMessageDependencies(txn4, messageId2);
oneOf(db).transaction(with(false), withDbRunnable(txn1));
oneOf(db).setMessageShared(txn1, messageId2);
oneOf(db).getMessageDependencies(txn1, messageId2);
will(returnValue(emptyMap()));
}});
@@ -318,49 +269,39 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
throws Exception {
Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, true);
Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, true);
Transaction txn5 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, false);
expectGetMessagesToValidate(messageId, messageId1);
context.checking(new DbExpectations() {{
// Get messages to validate
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(asList(messageId, messageId1)));
// Load the first raw message - *gasp* it's gone!
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn1, messageId);
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessage(txn, messageId);
will(throwException(new NoSuchMessageException()));
// Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getMessage(txn2, messageId1);
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn1, messageId1);
will(returnValue(message1));
oneOf(db).getGroup(txn2, groupId);
oneOf(db).getGroup(txn1, groupId);
will(returnValue(group));
// Validate the second message: invalid
oneOf(validator).validateMessage(message1, group);
will(throwException(new InvalidMessageException()));
// Invalidate the second message
oneOf(db).transaction(with(false), withDbRunnable(txn3));
oneOf(db).getMessageState(txn3, messageId1);
oneOf(db).transaction(with(false), withDbRunnable(txn2));
oneOf(db).getMessageState(txn2, messageId1);
will(returnValue(UNKNOWN));
oneOf(db).setMessageState(txn3, messageId1, INVALID);
oneOf(db).deleteMessage(txn3, messageId1);
oneOf(db).deleteMessageMetadata(txn3, messageId1);
oneOf(db).setMessageState(txn2, messageId1, INVALID);
oneOf(db).deleteMessage(txn2, messageId1);
oneOf(db).deleteMessageMetadata(txn2, messageId1);
// Recursively invalidate dependents
oneOf(db).getMessageDependents(txn3, messageId1);
oneOf(db).getMessageDependents(txn2, messageId1);
will(returnValue(emptyMap()));
// Get pending messages to deliver
oneOf(db).transactionWithResult(with(true), withDbCallable(txn4));
oneOf(db).getPendingMessages(txn4);
will(returnValue(emptyList()));
// Get messages to share
oneOf(db).transactionWithResult(with(true), withDbCallable(txn5));
oneOf(db).getMessagesToShare(txn5);
will(returnValue(emptyList()));
}});
expectGetPendingMessages();
expectGetMessagesToShare();
vm.startService();
}
@@ -369,52 +310,42 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
throws Exception {
Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, true);
Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, true);
Transaction txn5 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, false);
expectGetMessagesToValidate(messageId, messageId1);
context.checking(new DbExpectations() {{
// Get messages to validate
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(asList(messageId, messageId1)));
// Load the first raw message
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn1, messageId);
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessage(txn, messageId);
will(returnValue(message));
// Load the group - *gasp* it's gone!
oneOf(db).getGroup(txn1, groupId);
oneOf(db).getGroup(txn, groupId);
will(throwException(new NoSuchGroupException()));
// Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getMessage(txn2, messageId1);
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn1, messageId1);
will(returnValue(message1));
oneOf(db).getGroup(txn2, groupId);
oneOf(db).getGroup(txn1, groupId);
will(returnValue(group));
// Validate the second message: invalid
oneOf(validator).validateMessage(message1, group);
will(throwException(new InvalidMessageException()));
// Store the validation result for the second message
oneOf(db).transaction(with(false), withDbRunnable(txn3));
oneOf(db).getMessageState(txn3, messageId1);
oneOf(db).transaction(with(false), withDbRunnable(txn2));
oneOf(db).getMessageState(txn2, messageId1);
will(returnValue(UNKNOWN));
oneOf(db).setMessageState(txn3, messageId1, INVALID);
oneOf(db).deleteMessage(txn3, messageId1);
oneOf(db).deleteMessageMetadata(txn3, messageId1);
oneOf(db).setMessageState(txn2, messageId1, INVALID);
oneOf(db).deleteMessage(txn2, messageId1);
oneOf(db).deleteMessageMetadata(txn2, messageId1);
// Recursively invalidate dependents
oneOf(db).getMessageDependents(txn3, messageId1);
oneOf(db).getMessageDependents(txn2, messageId1);
will(returnValue(emptyMap()));
// Get pending messages to deliver
oneOf(db).transactionWithResult(with(true), withDbCallable(txn4));
oneOf(db).getPendingMessages(txn4);
will(returnValue(emptyList()));
// Get messages to share
oneOf(db).transactionWithResult(with(true), withDbCallable(txn5));
oneOf(db).getMessagesToShare(txn5);
will(returnValue(emptyList()));
}});
expectGetPendingMessages();
expectGetMessagesToShare();
vm.startService();
}
@@ -801,4 +732,35 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
vm.eventOccurred(new MessageAddedEvent(message, contactId));
}
private void expectGetMessagesToValidate(MessageId... ids)
throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToValidate(txn);
will(returnValue(asList(ids)));
}});
}
private void expectGetPendingMessages(MessageId... ids) throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getPendingMessages(txn);
will(returnValue(asList(ids)));
}});
}
private void expectGetMessagesToShare(MessageId... ids) throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessagesToShare(txn);
will(returnValue(asList(ids)));
}});
}
}

View File

@@ -1,9 +1,11 @@
package org.briarproject.bramble.test;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule;
import dagger.Module;
import dagger.Provides;
@Module(includes = {
DefaultBatteryManagerModule.class,
@@ -13,4 +15,20 @@ import dagger.Module;
TestSecureRandomModule.class
})
public class BrambleCoreIntegrationTestModule {
@Provides
FeatureFlags provideFeatureFlags() {
return new FeatureFlags() {
@Override
public boolean shouldEnableImageAttachments() {
return true;
}
@Override
public boolean shouldEnableRemoteContacts() {
return true;
}
};
}
}

View File

@@ -131,7 +131,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
localUpdateBody);
will(returnValue(localUpdate));
oneOf(clientHelper).addLocalMessage(txn, localUpdate,
localUpdateMeta, true);
localUpdateMeta, true, false);
}});
}
@@ -172,7 +172,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
localVersionsBody);
will(returnValue(localVersions));
oneOf(db).addLocalMessage(txn, localVersions, new Metadata(),
false);
false, false);
// Inform contacts that client versions have changed
oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact)));
@@ -259,7 +259,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalVersionsBody);
will(returnValue(newLocalVersions));
oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(),
false);
false, false);
// Inform contacts that client versions have changed
oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact)));
@@ -284,7 +284,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody);
will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true);
newLocalUpdateMeta, true, false);
// No visibilities have changed
}});
@@ -355,7 +355,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalVersionsBody);
will(returnValue(newLocalVersions));
oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(),
false);
false, false);
// Inform contacts that client versions have changed
oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact)));
@@ -382,7 +382,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody);
will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true);
newLocalUpdateMeta, true, false);
// The client's visibility has changed
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
}});
@@ -567,7 +567,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody);
will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true);
newLocalUpdateMeta, true, false);
// The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
@@ -640,7 +640,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody);
will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true);
newLocalUpdateMeta, true, false);
// The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());

View File

@@ -5,6 +5,10 @@
# QR codes
-keep class com.google.zxing.Result
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# RSS libraries
-keep,includedescriptorclasses class com.rometools.rome.feed.synd.impl.** { *; }

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
import javax.inject.Singleton;
@@ -13,6 +14,7 @@ import dagger.Component;
@Singleton
@Component(modules = {
AppModule.class,
AttachmentModule.class,
BriarCoreModule.class,
BrambleAndroidModule.class,
BriarAccountModule.class,

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.content.res.AssetManager;
import android.support.test.InstrumentationRegistry;
@@ -21,7 +21,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class AttachmentControllerIntegrationTest {
public class AttachmentRetrieverIntegrationTest {
private static final String smallKitten =
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg/160px-Kitten_in_Rizal_Park%2C_Manila.jpg";
@@ -47,15 +47,17 @@ public class AttachmentControllerIntegrationTest {
);
private final MessageId msgId = new MessageId(getRandomId());
private final AttachmentController controller =
new AttachmentController(null, dimensions);
private final ImageHelper imageHelper = new ImageHelperImpl();
private final AttachmentRetriever retriever =
new AttachmentRetrieverImpl(null, dimensions, imageHelper,
new ImageSizeCalculator(imageHelper));
@Test
public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
@@ -70,8 +72,8 @@ public class AttachmentControllerIntegrationTest {
public void testBigJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
@@ -86,8 +88,8 @@ public class AttachmentControllerIntegrationTest {
public void testSmallPngImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight());
@@ -102,8 +104,8 @@ public class AttachmentControllerIntegrationTest {
public void testUberGif() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -117,8 +119,8 @@ public class AttachmentControllerIntegrationTest {
public void testLottaPixels() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -132,8 +134,8 @@ public class AttachmentControllerIntegrationTest {
public void testImageIoCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -147,8 +149,8 @@ public class AttachmentControllerIntegrationTest {
public void testGimpCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -162,8 +164,8 @@ public class AttachmentControllerIntegrationTest {
public void testOptiPngAfl() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -177,8 +179,8 @@ public class AttachmentControllerIntegrationTest {
public void testLibrawError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertTrue(item.hasError());
}
@@ -186,8 +188,8 @@ public class AttachmentControllerIntegrationTest {
public void testSmallAnimatedGifMaxDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -201,8 +203,8 @@ public class AttachmentControllerIntegrationTest {
public void testSmallAnimatedGifHugeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -216,8 +218,8 @@ public class AttachmentControllerIntegrationTest {
public void testSmallGifLargeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -231,8 +233,8 @@ public class AttachmentControllerIntegrationTest {
public void testHighError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
@@ -246,8 +248,8 @@ public class AttachmentControllerIntegrationTest {
public void testWideError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());

View File

@@ -7,6 +7,7 @@ import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -29,6 +30,7 @@ import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender;
@@ -67,7 +69,8 @@ import dagger.Component;
BriarCoreModule.class,
BrambleAndroidModule.class,
BriarAccountModule.class,
AppModule.class
AppModule.class,
AttachmentModule.class
})
public interface AndroidComponent
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,
@@ -161,6 +164,8 @@ public interface AndroidComponent
ViewModelProvider.Factory viewModelFactory();
FeatureFlags featureFlags();
void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService);

View File

@@ -7,6 +7,7 @@ import android.os.StrictMode;
import com.vanniktech.emoji.RecentEmoji;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PublicKey;
@@ -59,6 +60,7 @@ import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = {ContactExchangeModule.class, ViewModelModule.class})
public class AppModule {
@@ -230,4 +232,20 @@ public class AppModule {
lifecycleManager.registerOpenDatabaseHook(recentEmoji);
return recentEmoji;
}
@Provides
FeatureFlags provideFeatureFlags() {
return new FeatureFlags() {
@Override
public boolean shouldEnableImageAttachments() {
return IS_DEBUG_BUILD;
}
@Override
public boolean shouldEnableRemoteContacts() {
return IS_DEBUG_BUILD;
}
};
}
}

View File

@@ -20,6 +20,7 @@ import org.acra.annotation.ReportsCrashes;
import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.reporting.BriarReportPrimer;
@@ -64,6 +65,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
reportDialogClass = DevReportActivity.class,
resDialogOkToast = R.string.dev_report_saved,
deleteOldUnsentReportsOnApplicationStart = false,
buildConfigClass = BuildConfig.class,
customReportContent = {
REPORT_ID,
APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME,

View File

@@ -30,15 +30,4 @@ public interface TestingConstants {
long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
Long.MAX_VALUE;
/**
* Feature flag for enabling image attachments.
*/
boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = false;
/**
* Feature flag for enabling adding contacts at a distance.
*/
boolean FEATURE_FLAG_REMOTE_CONTACTS = IS_DEBUG_BUILD;
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.account;
import android.os.Bundle;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -10,7 +11,6 @@ import android.widget.Button;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@@ -20,6 +20,7 @@ import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@@ -77,7 +78,7 @@ public class AuthorNameFragment extends SetupFragment {
@Override
public void onTextChanged(CharSequence authorName, int i, int i1, int i2) {
int authorNameLength = StringUtils.toUtf8(authorName.toString()).length;
int authorNameLength = toUtf8(authorName.toString().trim()).length;
boolean error = authorNameLength > MAX_AUTHOR_NAME_LENGTH;
setError(authorNameWrapper, getString(R.string.name_too_long), error);
boolean enabled = authorNameLength > 0 && !error;
@@ -89,8 +90,11 @@ public class AuthorNameFragment extends SetupFragment {
@Override
public void onClick(View view) {
setupController.setAuthorName(authorNameInput.getText().toString());
setupController.showPasswordFragment();
Editable text = authorNameInput.getText();
if (text != null) {
setupController.setAuthorName(text.toString().trim());
setupController.showPasswordFragment();
}
}
}

View File

@@ -0,0 +1,176 @@
package org.briarproject.briar.android.attachment;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.logging.Logger;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static android.graphics.BitmapFactory.decodeStream;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreationTask {
private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000;
private final MessagingManager messagingManager;
private final ContentResolver contentResolver;
private final ImageSizeCalculator imageSizeCalculator;
private final GroupId groupId;
private final Collection<Uri> uris;
private final boolean needsSize;
@Nullable
private volatile AttachmentCreator attachmentCreator;
private volatile boolean canceled = false;
AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver,
AttachmentCreator attachmentCreator,
ImageSizeCalculator imageSizeCalculator,
GroupId groupId, Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager;
this.contentResolver = contentResolver;
this.imageSizeCalculator = imageSizeCalculator;
this.groupId = groupId;
this.uris = uris;
this.needsSize = needsSize;
this.attachmentCreator = attachmentCreator;
}
void cancel() {
canceled = true;
attachmentCreator = null;
}
@IoExecutor
void storeAttachments() {
for (Uri uri : uris) processUri(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (!canceled && attachmentCreator != null)
attachmentCreator.onAttachmentCreationFinished();
this.attachmentCreator = null;
}
@IoExecutor
private void processUri(Uri uri) {
if (canceled) return;
try {
AttachmentHeader h = storeAttachment(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (attachmentCreator != null) {
attachmentCreator.onAttachmentHeaderReceived(uri, h, needsSize);
}
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (attachmentCreator != null) {
attachmentCreator.onAttachmentError(uri, e);
}
canceled = true;
}
}
@IoExecutor
private AttachmentHeader storeAttachment(Uri uri)
throws IOException, DbException {
long start = now();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
if (!isValidMimeType(contentType)) {
String uriString = uri.toString();
throw new UnsupportedMimeTypeException("", contentType, uriString);
}
InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
is = compressImage(is, contentType);
contentType = "image/jpeg";
long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
tryToClose(is, LOG, WARNING);
logDuration(LOG, "Storing attachment", start);
return h;
}
private boolean isValidMimeType(String mimeType) {
for (String supportedType : IMAGE_MIME_TYPES) {
if (supportedType.equals(mimeType)) return true;
}
return false;
}
private InputStream compressImage(InputStream is, String contentType)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Bitmap bitmap = createBitmap(is, contentType);
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out))
throw new IOException();
if (out.size() <= MAX_IMAGE_SIZE) {
if (LOG.isLoggable(INFO)) {
LOG.info("Compressed image to "
+ out.size() + " bytes, quality " + quality);
}
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
throw new IOException();
} finally {
tryToClose(is, LOG, WARNING);
}
}
private Bitmap createBitmap(InputStream is, String contentType)
throws IOException {
is = new BufferedInputStream(is);
Size size = imageSizeCalculator.getSize(is, contentType);
if (size.error) throw new IOException();
if (LOG.isLoggable(INFO))
LOG.info("Original image size: " + size.width + "x" + size.height);
int dimension = Math.max(size.width, size.height);
int inSampleSize = 1;
while (dimension > MAX_ATTACHMENT_DIMENSION) {
inSampleSize *= 2;
dimension /= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize);
Options options = new Options();
options.inSampleSize = inSampleSize;
Bitmap bitmap = decodeStream(is, null, options);
if (bitmap == null) throw new IOException();
return bitmap;
}
}

View File

@@ -0,0 +1,60 @@
package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import android.net.Uri;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection;
import java.util.List;
@NotNullByDefault
public interface AttachmentCreator {
@UiThread
LiveData<AttachmentResult> storeAttachments(LiveData<GroupId> groupId,
Collection<Uri> newUris);
/**
* This should be only called after configuration changes.
* In this case we should not create new attachments.
* They are already being created and returned by the existing LiveData.
*/
@UiThread
LiveData<AttachmentResult> getLiveAttachments();
@UiThread
List<AttachmentHeader> getAttachmentHeadersForSending();
/**
* Marks the attachments as sent and adds the items to the cache for display
*
* @param id The MessageId of the sent message.
*/
@UiThread
void onAttachmentsSent(MessageId id);
/**
* Needs to be called when created attachments will not be sent anymore.
*/
@UiThread
void cancel();
@UiThread
void deleteUnsentAttachments();
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize);
@IoExecutor
void onAttachmentError(Uri uri, Throwable t);
@IoExecutor
void onAttachmentCreationFinished();
}

View File

@@ -0,0 +1,233 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreatorImpl implements AttachmentCreator {
private static Logger LOG =
getLogger(AttachmentCreatorImpl.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final ImageSizeCalculator imageSizeCalculator;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
@Nullable
private AttachmentCreationTask task;
@Nullable
private volatile MutableLiveData<AttachmentResult> result;
@Inject
AttachmentCreatorImpl(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever,
ImageSizeCalculator imageSizeCalculator) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
this.imageSizeCalculator = imageSizeCalculator;
}
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || result != null || !uris.isEmpty())
throw new IllegalStateException();
MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
this.result = result;
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, imageSizeCalculator, id,
uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
@Override
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
MutableLiveData<AttachmentResult> result = this.result;
if (task == null || result == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@Override
@IoExecutor
public void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@Override
@IoExecutor
public void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@Override
@IoExecutor
public void onAttachmentCreationFinished() {
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(true));
}
@Override
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
@Override
@UiThread
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
@Override
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) {
result.setValue(null);
this.result = null;
}
}
@Override
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
}

View File

@@ -1,10 +1,15 @@
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.content.res.Resources;
import android.support.annotation.VisibleForTesting;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
class AttachmentDimensions {
final int defaultSize;
@@ -33,7 +38,7 @@ class AttachmentDimensions {
int maxHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_height);
return new AttachmentDimensions(defaultSize, minWidth, maxWidth,
minHeight, minHeight);
minHeight, maxHeight);
}
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.os.Parcel;
import android.os.Parcelable;
@@ -6,18 +6,21 @@ import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable;
import static java.util.Objects.requireNonNull;
@Immutable
@NotNullByDefault
public class AttachmentItem implements Parcelable {
private final MessageId messageId;
private final AttachmentHeader header;
private final int width, height;
private final String mimeType, extension;
private final String extension;
private final int thumbnailWidth, thumbnailHeight;
private final boolean hasError;
private final long instanceId;
@@ -37,13 +40,12 @@ public class AttachmentItem implements Parcelable {
private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0);
AttachmentItem(MessageId messageId, int width, int height, String mimeType,
AttachmentItem(AttachmentHeader header, int width, int height,
String extension, int thumbnailWidth, int thumbnailHeight,
boolean hasError) {
this.messageId = messageId;
this.header = header;
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.extension = extension;
this.thumbnailWidth = thumbnailWidth;
this.thumbnailHeight = thumbnailHeight;
@@ -54,19 +56,24 @@ public class AttachmentItem implements Parcelable {
protected AttachmentItem(Parcel in) {
byte[] messageIdByte = new byte[MessageId.LENGTH];
in.readByteArray(messageIdByte);
messageId = new MessageId(messageIdByte);
MessageId messageId = new MessageId(messageIdByte);
width = in.readInt();
height = in.readInt();
mimeType = in.readString();
extension = in.readString();
String mimeType = requireNonNull(in.readString());
extension = requireNonNull(in.readString());
thumbnailWidth = in.readInt();
thumbnailHeight = in.readInt();
hasError = in.readByte() != 0;
instanceId = in.readLong();
header = new AttachmentHeader(messageId, mimeType);
}
public AttachmentHeader getHeader() {
return header;
}
public MessageId getMessageId() {
return messageId;
return header.getMessageId();
}
int getWidth() {
@@ -77,27 +84,27 @@ public class AttachmentItem implements Parcelable {
return height;
}
String getMimeType() {
return mimeType;
public String getMimeType() {
return header.getContentType();
}
String getExtension() {
public String getExtension() {
return extension;
}
int getThumbnailWidth() {
public int getThumbnailWidth() {
return thumbnailWidth;
}
int getThumbnailHeight() {
public int getThumbnailHeight() {
return thumbnailHeight;
}
boolean hasError() {
public boolean hasError() {
return hasError;
}
String getTransitionName() {
public String getTransitionName() {
return String.valueOf(instanceId);
}
@@ -108,10 +115,10 @@ public class AttachmentItem implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(messageId.getBytes());
dest.writeByteArray(header.getMessageId().getBytes());
dest.writeInt(width);
dest.writeInt(height);
dest.writeString(mimeType);
dest.writeString(header.getContentType());
dest.writeString(extension);
dest.writeInt(thumbnailWidth);
dest.writeInt(thumbnailHeight);

View File

@@ -0,0 +1,50 @@
package org.briarproject.briar.android.attachment;
import android.net.Uri;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentItemResult {
private final Uri uri;
@Nullable
private final AttachmentItem item;
@Nullable
private final String errorMsg;
AttachmentItemResult(Uri uri, AttachmentItem item) {
this.uri = uri;
this.item = item;
this.errorMsg = null;
}
AttachmentItemResult(Uri uri, @Nullable String errorMsg) {
this.uri = uri;
this.item = null;
this.errorMsg = errorMsg;
}
public Uri getUri() {
return uri;
}
@Nullable
public AttachmentItem getItem() {
return item;
}
public boolean hasError() {
return item == null;
}
@Nullable
public String getErrorMsg() {
return errorMsg;
}
}

View File

@@ -0,0 +1,24 @@
package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import android.net.Uri;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection;
import java.util.List;
@UiThread
@NotNullByDefault
public interface AttachmentManager {
LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri,
boolean restart);
List<AttachmentHeader> getAttachmentHeadersForSending();
void cancel();
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
@Module
public class AttachmentModule {
@Provides
ImageHelper provideImageHelper(ImageHelperImpl imageHelper) {
return imageHelper;
}
@Provides
ImageSizeCalculator provideImageSizeCalculator(ImageHelper imageHelper) {
return new ImageSizeCalculator(imageHelper);
}
@Provides
AttachmentDimensions provideAttachmentDimensions(Application app) {
return getAttachmentDimensions(app.getResources());
}
@Provides
@Singleton
AttachmentRetriever provideAttachmentRetriever(
AttachmentRetrieverImpl attachmentRetriever) {
return attachmentRetriever;
}
@Provides
@Singleton
AttachmentCreator provideAttachmentCreator(
AttachmentCreatorImpl attachmentCreator) {
return attachmentCreator;
}
}

View File

@@ -0,0 +1,30 @@
package org.briarproject.briar.android.attachment;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentResult {
private final Collection<AttachmentItemResult> itemResults;
private final boolean finished;
public AttachmentResult(Collection<AttachmentItemResult> itemResults,
boolean finished) {
this.itemResults = itemResults;
this.finished = finished;
}
public Collection<AttachmentItemResult> getItemResults() {
return itemResults;
}
public boolean isFinished() {
return finished;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.io.InputStream;
import java.util.List;
@NotNullByDefault
public interface AttachmentRetriever {
void cachePut(MessageId messageId, List<AttachmentItem> attachments);
@Nullable
List<AttachmentItem> cacheGet(MessageId messageId);
Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
/**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns.
*/
AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
}

View File

@@ -0,0 +1,129 @@
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
@NotNullByDefault
class AttachmentRetrieverImpl implements AttachmentRetriever {
private static final Logger LOG =
getLogger(AttachmentRetrieverImpl.class.getName());
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final ImageSizeCalculator imageSizeCalculator;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@Inject
AttachmentRetrieverImpl(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper,
ImageSizeCalculator imageSizeCalculator) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
this.imageSizeCalculator = imageSizeCalculator;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
@Override
public void cachePut(MessageId messageId,
List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Override
@Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
@Override
public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException {
return messagingManager.getAttachment(h);
}
@Override
public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
AttachmentHeader h = a.getHeader();
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;
@@ -7,7 +7,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
@NotNullByDefault
interface ImageHelper {
public interface ImageHelper {
DecodeResult decodeStream(InputStream is);

View File

@@ -0,0 +1,39 @@
package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class ImageHelperImpl implements ImageHelper {
@Inject
ImageHelperImpl() {
}
@Override
public DecodeResult decodeStream(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
}

View File

@@ -0,0 +1,94 @@
package org.briarproject.briar.android.attachment;
import android.support.media.ExifInterface;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ImageSizeCalculator {
private static final Logger LOG =
getLogger(ImageSizeCalculator.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final ImageHelper imageHelper;
ImageSizeCalculator(ImageHelper imageHelper) {
this.imageHelper = imageHelper;
}
Size getSize(InputStream is, String contentType) {
Size size = new Size();
is = new MarkEnforcingInputStream(is);
is.mark(READ_LIMIT);
if (contentType.equals("image/jpeg")) {
try {
// use exif to get size
size = getSizeFromExif(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
if (size.error) {
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
try {
// use BitmapFactory to get size
size = getSizeFromBitmap(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
return size;
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.android.attachment;
class Size {
final int width;
final int height;
final String mimeType;
final boolean error;
Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.blog;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -21,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List;
@@ -121,7 +121,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
@@ -27,6 +26,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.security.GeneralSecurityException;
import java.util.List;
@@ -120,7 +120,8 @@ public class WriteBlogPostActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar

View File

@@ -14,6 +14,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -67,7 +68,6 @@ import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_REMOTE_CONTACTS;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
@@ -85,6 +85,8 @@ public class ContactListFragment extends BaseFragment implements EventListener,
EventBus eventBus;
@Inject
AndroidNotificationManager notificationManager;
@Inject
FeatureFlags featureFlags;
private ContactListAdapter adapter;
private BriarRecyclerView list;
@@ -124,7 +126,7 @@ public class ContactListFragment extends BaseFragment implements EventListener,
container, false);
FabSpeedDial speedDial = contentView.findViewById(R.id.speedDial);
if (FEATURE_FLAG_REMOTE_CONTACTS) {
if (featureFlags.shouldEnableRemoteContacts()) {
speedDial.addOnMenuItemClickListener(this);
} else {
speedDial.setMenu(new FabSpeedDialMenu(contentView.getContext()));

View File

@@ -82,19 +82,20 @@ public class NicknameFragment extends BaseFragment {
@Nullable
private String getNicknameOrNull() {
Editable name = contactNameInput.getText();
if (name == null || name.toString().trim().length() == 0) {
Editable text = contactNameInput.getText();
if (text == null || text.toString().trim().length() == 0) {
contactNameLayout.setError(getString(R.string.nickname_missing));
contactNameInput.requestFocus();
return null;
}
if (utf8IsTooLong(name.toString(), MAX_AUTHOR_NAME_LENGTH)) {
String name = text.toString().trim();
if (utf8IsTooLong(name, MAX_AUTHOR_NAME_LENGTH)) {
contactNameLayout.setError(getString(R.string.name_too_long));
contactNameInput.requestFocus();
return null;
}
contactNameLayout.setError(null);
return name.toString().trim();
return name;
}
private void onAddButtonClicked() {

View File

@@ -4,6 +4,7 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
@@ -15,6 +16,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import java.util.Collection;
@@ -22,6 +24,7 @@ import java.util.Collection;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static org.briarproject.bramble.api.contact.PendingContactState.FAILED;
import static org.briarproject.briar.android.contact.add.remote.PendingContactItem.POLL_DURATION_MS;
@@ -36,6 +39,7 @@ public class PendingContactListActivity extends BriarActivity
private PendingContactListViewModel viewModel;
private PendingContactListAdapter adapter;
private BriarRecyclerView list;
private Snackbar offlineSnackbar;
@Override
public void injectActivity(ActivityComponent component) {
@@ -58,6 +62,8 @@ public class PendingContactListActivity extends BriarActivity
viewModel.onCreate();
viewModel.getPendingContacts()
.observe(this, this::onPendingContactsChanged);
viewModel.getHasInternetConnection()
.observe(this, this::onInternetConnectionChanged);
adapter = new PendingContactListAdapter(this, this,
PendingContactItem.class);
@@ -66,6 +72,10 @@ public class PendingContactListActivity extends BriarActivity
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
list.showProgressBar();
offlineSnackbar = new BriarSnackbarBuilder()
.setBackgroundColor(R.color.briar_red)
.make(list, R.string.offline_state, LENGTH_INDEFINITE);
}
@Override
@@ -130,4 +140,9 @@ public class PendingContactListActivity extends BriarActivity
}
}
private void onInternetConnectionChanged(boolean online) {
if (online) offlineSnackbar.dismiss();
else offlineSnackbar.show();
}
}

View File

@@ -31,6 +31,7 @@ import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
@@ -48,6 +49,8 @@ public class PendingContactListViewModel extends AndroidViewModel
private final MutableLiveData<Collection<PendingContactItem>>
pendingContacts = new MutableLiveData<>();
private final MutableLiveData<Boolean> hasInternetConnection =
new MutableLiveData<>();
@Inject
PendingContactListViewModel(Application application,
@@ -88,13 +91,16 @@ public class PendingContactListViewModel extends AndroidViewModel
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = false;
for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();
long lastPoll = rendezvousPoller.getLastPollTime(p.getId());
items.add(new PendingContactItem(p, pair.getSecond(),
lastPoll));
items.add(new PendingContactItem(p, state, lastPoll));
online = online || state != OFFLINE;
}
pendingContacts.postValue(items);
hasInternetConnection.postValue(online);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -115,4 +121,8 @@ public class PendingContactListViewModel extends AndroidViewModel
});
}
LiveData<Boolean> getHasInternetConnection() {
return hasInternetConnection;
}
}

View File

@@ -52,6 +52,11 @@ class PendingContactViewHolder extends ViewHolder {
.getColor(status.getContext(), R.color.briar_yellow);
status.setText(R.string.waiting_for_contact_to_come_online);
break;
case OFFLINE:
color = ContextCompat
.getColor(status.getContext(), R.color.briar_yellow);
status.setText("");
break;
case CONNECTING:
status.setText(R.string.connecting);
break;

View File

@@ -82,7 +82,7 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
}
private void onSetButtonClicked() {
String alias = aliasEditText.getText().toString();
String alias = aliasEditText.getText().toString().trim();
if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) {
aliasEditLayout.setError(getString(R.string.name_too_long));
} else {

View File

@@ -1,264 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable;
import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault
class AttachmentController {
private static final Logger LOG =
getLogger(AttachmentController.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
AttachmentController(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
AttachmentController(MessagingManager messagingManager,
AttachmentDimensions dimensions) {
this(messagingManager, dimensions, new ImageHelper() {
@Override
public DecodeResult decodeStream(InputStream is) {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
});
}
void put(MessageId messageId, List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Nullable
List<AttachmentItem> get(MessageId messageId) {
return attachmentCache.get(messageId);
}
@DatabaseExecutor
List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException {
long start = now();
List<Pair<AttachmentHeader, Attachment>> attachments =
new ArrayList<>(headers.size());
for (AttachmentHeader h : headers) {
Attachment a = messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a));
}
logDuration(LOG, "Loading attachment", start);
return attachments;
}
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) {
AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
items.add(item);
}
return items;
}
/**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns.
*/
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) {
MessageId messageId = h.getMessageId();
if (!needsSize) {
String mimeType = h.getContentType();
String extension = imageHelper.getExtensionFromMimeType(mimeType);
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0,
0, hasError);
}
Size size = new Size();
InputStream is = new MarkEnforcingInputStream(
new BufferedInputStream(a.getStream()));
is.mark(READ_LIMIT);
try {
// use exif to get size
if (h.getContentType().equals("image/jpeg")) {
size = getSizeFromExif(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
}
try {
// use BitmapFactory to get size
if (size.error) {
is.reset();
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
size = getSizeFromBitmap(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
} finally {
tryToClose(is, LOG, WARNING);
}
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (extension == null) extension = "";
return new AttachmentItem(messageId, size.width, size.height,
size.mimeType, extension, thumbnailSize.width,
thumbnailSize.height, hasError);
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
private static class Size {
private final int width;
private final int height;
private final String mimeType;
private final boolean error;
private Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
private Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}
}

View File

@@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@@ -30,14 +29,15 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
@@ -46,13 +46,14 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.attachment.AttachmentRetriever;
import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
@@ -63,10 +64,9 @@ import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.ImagePreview;
import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachImageListener;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException;
@@ -81,8 +81,8 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList;
@@ -92,7 +92,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -110,15 +109,16 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
@@ -128,6 +128,7 @@ import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED;
@@ -135,13 +136,13 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, SendListener,
TextCache, AttachmentCache, AttachImageListener {
implements EventListener, ConversationListener, TextCache,
AttachmentCache, AttachmentListener {
public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
getLogger(ConversationActivity.class.getName());
private static final int TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250;
@@ -151,10 +152,9 @@ public class ConversationActivity extends BriarActivity
@Inject
ConnectionRegistry connectionRegistry;
@Inject
@CryptoExecutor
Executor cryptoExecutor;
@Inject
ViewModelProvider.Factory viewModelFactory;
@Inject
FeatureFlags featureFlags;
// Fields that are accessed from background threads must be volatile
@Inject
@@ -166,10 +166,6 @@ public class ConversationActivity extends BriarActivity
@Inject
volatile EventBus eventBus;
@Inject
volatile SettingsManager settingsManager;
@Inject
volatile PrivateMessageFactory privateMessageFactory;
@Inject
volatile IntroductionManager introductionManager;
@Inject
volatile ForumSharingManager forumSharingManager;
@@ -179,12 +175,14 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> {
requireNonNull(name);
loadMessages();
};
private AttachmentController attachmentController;
private AttachmentRetriever attachmentRetriever;
private ConversationViewModel viewModel;
private ConversationVisitor visitor;
private ConversationAdapter adapter;
@@ -219,7 +217,7 @@ public class ConversationActivity extends BriarActivity
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ConversationViewModel.class);
attachmentController = viewModel.getAttachmentController();
attachmentRetriever = viewModel.getAttachmentRetriever();
setContentView(R.layout.activity_conversation);
@@ -242,7 +240,7 @@ public class ConversationActivity extends BriarActivity
requireNonNull(deleted);
if (deleted) finish();
});
viewModel.getAddedPrivateMessage().observe(this,
viewModel.getAddedPrivateMessage().observeEvent(this,
this::onAddedPrivateMessage);
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
@@ -261,13 +259,13 @@ public class ConversationActivity extends BriarActivity
list.getRecyclerView().addOnScrollListener(scrollListener);
textInputView = findViewById(R.id.text_input_container);
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
if (featureFlags.shouldEnableImageAttachments()) {
ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView,
imagePreview, this, this);
imagePreview, this, viewModel);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController)
.setImagesSupported();
}
@@ -302,7 +300,7 @@ public class ConversationActivity extends BriarActivity
Snackbar.LENGTH_SHORT)
.show();
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController).onImageReceived(data);
}
}
@@ -442,29 +440,40 @@ public class ConversationActivity extends BriarActivity
});
}
private void eagerlyLoadMessageSize(PrivateMessageHeader h)
throws DbException {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, text);
private void eagerlyLoadMessageSize(PrivateMessageHeader h) {
try {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text));
}
}
}
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
if (h.getAttachmentHeaders().size() == 1) {
List<AttachmentItem> items = attachmentController.get(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
items = attachmentController.getAttachmentItems(
attachmentController.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentController.put(id, items);
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
List<AttachmentHeader> headers = h.getAttachmentHeaders();
if (headers.size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
AttachmentHeader header = headers.get(0);
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item =
attachmentRetriever.getAttachmentItem(a, true);
attachmentRetriever.cachePut(id, singletonList(item));
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
}
}
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
@@ -475,8 +484,10 @@ public class ConversationActivity extends BriarActivity
adapter.incrementRevision();
textInputView.setReady(true);
// start observing onboarding after enabling
viewModel.showImageOnboarding().observeEvent(this,
this::showImageOnboarding);
if (featureFlags.shouldEnableImageAttachments()) {
viewModel.showImageOnboarding().observeEvent(this,
this::showImageOnboarding);
}
List<ConversationItem> items = createItems(headers);
adapter.addAll(items);
list.showData();
@@ -512,7 +523,7 @@ public class ConversationActivity extends BriarActivity
long start = now();
String text = messagingManager.getMessageText(m);
logDuration(LOG, "Loading text", start);
displayMessageText(m, text);
displayMessageText(m, requireNonNull(text));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -540,16 +551,30 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager);
}
private void loadMessageAttachments(MessageId messageId,
List<AttachmentHeader> headers) {
private void loadMessageAttachments(PrivateMessageHeader h) {
// TODO: Use placeholders for missing/invalid attachments
runOnDbThread(() -> {
try {
List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentController.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentItem> items =
attachmentController.getAttachmentItems(attachments);
displayMessageAttachments(messageId, items);
List<AttachmentHeader> headers = h.getAttachmentHeaders();
boolean needsSize = headers.size() == 1;
List<AttachmentItem> items = new ArrayList<>(headers.size());
for (AttachmentHeader header : headers) {
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item = attachmentRetriever
.getAttachmentItem(a, needsSize);
items.add(item);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
return;
}
}
// Don't cache items unless all are present and valid
attachmentRetriever.cachePut(h.getId(), items);
displayMessageAttachments(h.getId(), items);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -559,7 +584,6 @@ public class ConversationActivity extends BriarActivity
private void displayMessageAttachments(MessageId m,
List<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> {
attachmentController.put(m, items);
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
@@ -573,6 +597,13 @@ public class ConversationActivity extends BriarActivity
@Override
public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
onAttachmentReceived(a.getMessageId());
}
}
if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) {
@@ -623,6 +654,15 @@ public class ConversationActivity extends BriarActivity
scrollToBottom();
}
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread
private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest ||
@@ -658,12 +698,21 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text) && imageUris.isEmpty())
public void onTooManyAttachments() {
String format = getResources().getString(
R.string.messaging_too_many_attachments_toast);
String warning = String.format(format, MAX_ATTACHMENTS_PER_MESSAGE);
Toast.makeText(this, warning, LENGTH_SHORT).show();
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError();
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
viewModel.sendMessage(text, imageUris, timestamp);
viewModel.sendMessage(text, attachmentHeaders, timestamp);
textInputView.clearText();
}
@@ -676,7 +725,6 @@ public class ConversationActivity extends BriarActivity
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
if (h == null) return;
addConversationItem(h.accept(visitor));
viewModel.onAddedPrivateMessageSeen();
}
private void askToRemoveContact() {
@@ -726,7 +774,7 @@ public class ConversationActivity extends BriarActivity
}
private void showImageOnboarding() {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController)
.showImageOnboarding(this, () ->
viewModel.onImageOnboardingSeen());
@@ -901,11 +949,11 @@ public class ConversationActivity extends BriarActivity
}
@Override
public List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers) {
List<AttachmentItem> attachments = attachmentController.get(m);
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
List<AttachmentItem> attachments =
attachmentRetriever.cacheGet(h.getId());
if (attachments == null) {
loadMessageAttachments(m, headers);
loadMessageAttachments(h);
return emptyList();
}
return attachments;

View File

@@ -4,6 +4,7 @@ import android.support.annotation.UiThread;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.AttachmentItem;
@UiThread
@NotNullByDefault

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation;
import android.support.annotation.LayoutRes;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.util.List;

View File

@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItem;
import static android.support.constraint.ConstraintSet.WRAP_CONTENT;
import static android.support.v4.content.ContextCompat.getColor;

View File

@@ -5,17 +5,14 @@ import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations;
import android.content.ContentResolver;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
@@ -27,19 +24,19 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentCreator;
import org.briarproject.briar.android.attachment.AttachmentManager;
import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.attachment.AttachmentRetriever;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
@@ -50,19 +47,19 @@ import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault
public class ConversationViewModel extends AndroidViewModel {
public class ConversationViewModel extends AndroidViewModel
implements AttachmentManager {
private static Logger LOG =
getLogger(ConversationViewModel.class.getName());
private static final String SHOW_ONBOARDING_IMAGE =
"showOnboardingImage";
private static final String SHOW_ONBOARDING_INTRODUCTION =
@@ -70,14 +67,13 @@ public class ConversationViewModel extends AndroidViewModel {
@DatabaseExecutor
private final Executor dbExecutor;
@CryptoExecutor
private final Executor cryptoExecutor;
private final TransactionManager db;
private final MessagingManager messagingManager;
private final ContactManager contactManager;
private final SettingsManager settingsManager;
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentController attachmentController;
private final AttachmentRetriever attachmentRetriever;
private final AttachmentCreator attachmentCreator;
@Nullable
private ContactId contactId = null;
@@ -86,6 +82,7 @@ public class ConversationViewModel extends AndroidViewModel {
Transformations.map(contact, c -> c.getAuthor().getId());
private final LiveData<String> contactName =
Transformations.map(contact, UiUtils::getContactDisplayName);
private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding =
@@ -96,31 +93,39 @@ public class ConversationViewModel extends AndroidViewModel {
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>();
private final MutableLiveData<GroupId> messagingGroupId =
new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
private final MutableLiveEvent<PrivateMessageHeader> addedHeader =
new MutableLiveEvent<>();
@Inject
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager,
TransactionManager db,
MessagingManager messagingManager,
ContactManager contactManager,
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) {
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application);
this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.db = db;
this.messagingManager = messagingManager;
this.contactManager = contactManager;
this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory;
this.attachmentController = new AttachmentController(messagingManager,
getAttachmentDimensions(application.getResources()));
this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator;
messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false);
}
@Override
protected void onCleared() {
super.onCleared();
attachmentCreator.deleteUnsentAttachments();
}
/**
* Setting the {@link ContactId} automatically triggers loading of other
* data.
@@ -176,25 +181,42 @@ public class ConversationViewModel extends AndroidViewModel {
});
}
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) {
if (messagingGroupId.getValue() == null) loadGroupId();
@UiThread
void sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long timestamp) {
// messagingGroupId is loaded with the contact
observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return;
// calls through to creating and storing the message
storeAttachments(groupId, text, uris, timestamp);
requireNonNull(groupId);
observeForeverOnce(imageSupport, hasImageSupport -> {
requireNonNull(hasImageSupport);
createMessage(groupId, text, headers, timestamp,
hasImageSupport);
});
});
}
private void loadGroupId() {
if (contactId == null) throw new IllegalStateException();
dbExecutor.execute(() -> {
try {
messagingGroupId.postValue(
messagingManager.getConversationId(contactId));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
boolean restart) {
if (restart) {
return attachmentCreator.getLiveAttachments();
} else {
// messagingGroupId is loaded with the contact
return attachmentCreator.storeAttachments(messagingGroupId, uris);
}
}
@Override
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
return attachmentCreator.getAttachmentHeadersForSending();
}
@Override
@UiThread
public void cancel() {
attachmentCreator.cancel();
}
@DatabaseExecutor
@@ -252,75 +274,26 @@ public class ConversationViewModel extends AndroidViewModel {
});
}
private void storeAttachments(GroupId groupId, @Nullable String text,
List<Uri> uris, long timestamp) {
dbExecutor.execute(() -> {
long start = now();
List<AttachmentHeader> attachments = new ArrayList<>();
List<AttachmentItem> items = new ArrayList<>();
boolean needsSize = uris.size() == 1;
for (Uri uri : uris) {
Pair<AttachmentHeader, AttachmentItem> pair =
createAttachmentHeader(groupId, uri, timestamp,
needsSize);
if (pair == null) continue;
attachments.add(pair.getFirst());
items.add(pair.getSecond());
}
logDuration(LOG, "Storing attachments", start);
createMessage(groupId, text, attachments, items, timestamp);
});
}
@Nullable
@DatabaseExecutor
private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
GroupId groupId, Uri uri, long timestamp, boolean needsSize) {
InputStream is = null;
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> headers, long timestamp,
boolean hasImageSupport) {
try {
ContentResolver contentResolver =
getApplication().getContentResolver();
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
is.close();
// re-open stream to get AttachmentItem
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
AttachmentItem item = attachmentController
.getAttachmentItem(h, new Attachment(is), needsSize);
return new Pair<>(h, item);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
return null;
} finally {
if (is != null) tryToClose(is, LOG, WARNING);
PrivateMessage pm;
if (hasImageSupport) {
pm = privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers);
} else {
pm = privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
}
storeMessage(pm);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
long timestamp) {
cryptoExecutor.execute(() -> {
try {
// TODO remove when text can be null in the backend
String msgText = text == null ? "null" : text;
PrivateMessage pm = privateMessageFactory
.createPrivateMessage(groupId, timestamp, msgText,
attachments);
attachmentController.put(pm.getMessage().getId(), aItems);
storeMessage(pm, msgText, attachments);
} catch (FormatException e) {
throw new RuntimeException(e);
}
});
}
private void storeMessage(PrivateMessage m, @Nullable String text,
List<AttachmentHeader> attachments) {
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
dbExecutor.execute(() -> {
try {
long start = now();
@@ -330,22 +303,17 @@ public class ConversationViewModel extends AndroidViewModel {
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
text != null, attachments);
m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here
addedHeader.postValue(h);
addedHeader.postEvent(h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@UiThread
void onAddedPrivateMessageSeen() {
addedHeader.setValue(null);
}
AttachmentController getAttachmentController() {
return attachmentController;
AttachmentRetriever getAttachmentRetriever() {
return attachmentRetriever;
}
LiveData<Contact> getContact() {
@@ -380,7 +348,7 @@ public class ConversationViewModel extends AndroidViewModel {
return contactDeleted;
}
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
LiveEvent<PrivateMessageHeader> getAddedPrivateMessage() {
return addedHeader;
}

View File

@@ -7,6 +7,7 @@ import android.support.annotation.UiThread;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.api.blog.BlogInvitationRequest;
import org.briarproject.briar.api.blog.BlogInvitationResponse;
import org.briarproject.briar.api.conversation.ConversationMessageVisitor;
@@ -14,7 +15,6 @@ import org.briarproject.briar.api.forum.ForumInvitationRequest;
import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.introduction.IntroductionRequest;
import org.briarproject.briar.api.introduction.IntroductionResponse;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
@@ -55,8 +55,7 @@ class ConversationVisitor implements
if (h.getAttachmentHeaders().isEmpty()) {
attachments = emptyList();
} else {
attachments = attachmentCache
.getAttachmentItems(h.getId(), h.getAttachmentHeaders());
attachments = attachmentCache.getAttachmentItems(h);
}
if (h.isLocal()) {
item = new ConversationMessageItem(
@@ -294,7 +293,6 @@ class ConversationVisitor implements
}
interface AttachmentCache {
List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers);
List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h);
}
}

View File

@@ -31,6 +31,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.PullDownLayout;

View File

@@ -12,6 +12,7 @@ import android.view.WindowManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.conversation.glide.Radii;
import java.util.ArrayList;

View File

@@ -21,6 +21,7 @@ import com.github.chrisbanes.photoview.PhotoView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -11,6 +11,7 @@ import com.bumptech.glide.load.Transformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.conversation.glide.Radii;

View File

@@ -12,7 +12,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment;
@@ -134,10 +134,10 @@ public class ImageViewModel extends AndroidViewModel {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> {
try {
Attachment a = messagingManager.getAttachment(messageId);
Attachment a =
messagingManager.getAttachment(attachment.getHeader());
copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) {
logException(LOG, WARNING, e);

View File

@@ -9,8 +9,8 @@ import com.bumptech.glide.load.data.DataFetcher;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.conversation.AttachmentItem;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.InputStream;
@@ -50,11 +50,12 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
@Override
public void loadData(Priority priority,
DataCallback<? super InputStream> callback) {
MessageId id = attachment.getMessageId();
dbExecutor.execute(() -> {
if (cancel) return;
try {
inputStream = messagingManager.getAttachment(id).getStream();
Attachment a =
messagingManager.getAttachment(attachment.getHeader());
inputStream = a.getStream();
callback.onDataReady(inputStream);
} catch (DbException e) {
callback.onLoadFailed(e);

View File

@@ -2,7 +2,7 @@ package org.briarproject.briar.android.conversation.glide;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.conversation.AttachmentItem;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.util.concurrent.Executor;

View File

@@ -10,7 +10,7 @@ import com.bumptech.glide.module.AppGlideModule;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.conversation.AttachmentItem;
import org.briarproject.briar.android.attachment.AttachmentItem;
import java.io.InputStream;

View File

@@ -8,7 +8,7 @@ import com.bumptech.glide.signature.ObjectKey;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.conversation.AttachmentItem;
import org.briarproject.briar.android.attachment.AttachmentItem;
import java.io.InputStream;

View File

@@ -6,7 +6,7 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.conversation.AttachmentItem;
import org.briarproject.briar.android.attachment.AttachmentItem;
import java.io.InputStream;

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
@@ -26,6 +25,7 @@ import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List;
import java.util.logging.Logger;
@@ -193,7 +193,8 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
// disable button to prevent accidental double invitations
ui.message.setReady(false);

View File

@@ -0,0 +1,40 @@
package org.briarproject.briar.android.reporting;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class CrashFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_crash, container, false);
v.findViewById(R.id.acceptButton).setOnClickListener(view ->
getDevReportActivity().displayFragment(true));
v.findViewById(R.id.declineButton).setOnClickListener(view ->
getDevReportActivity().closeReport());
return v;
}
private DevReportActivity getDevReportActivity() {
return (DevReportActivity) requireNonNull(getActivity());
}
}

View File

@@ -1,87 +1,38 @@
package org.briarproject.briar.android.reporting;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.acra.ReportField;
import org.acra.collector.CrashReportData;
import org.acra.dialog.BaseCrashReportDialog;
import org.acra.file.CrashReportPersister;
import org.acra.model.Element;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.logout.HideUiActivity;
import org.briarproject.briar.android.util.UserFeedback;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.inputmethod.InputMethodManager.SHOW_FORCED;
import static java.util.logging.Level.WARNING;
import static java.util.Objects.requireNonNull;
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PACKAGE_NAME;
import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
public class DevReportActivity extends BaseCrashReportDialog
implements CompoundButton.OnCheckedChangeListener {
private static final Logger LOG =
Logger.getLogger(DevReportActivity.class.getName());
private static final String STATE_REVIEWING = "reviewing";
private static final Set<ReportField> requiredFields = new HashSet<>();
static {
requiredFields.add(REPORT_ID);
requiredFields.add(APP_VERSION_CODE);
requiredFields.add(APP_VERSION_NAME);
requiredFields.add(PACKAGE_NAME);
requiredFields.add(ANDROID_VERSION);
requiredFields.add(STACK_TRACE);
}
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class DevReportActivity extends BaseCrashReportDialog {
private AppCompatDelegate delegate;
private Set<ReportField> excludedFields = new HashSet<>();
private EditText userCommentView = null;
private EditText userEmailView = null;
private CheckBox includeDebugReport = null;
private Button chevron = null;
private LinearLayout report = null;
private View progress = null;
private MenuItem sendReport = null;
private boolean reviewing = false;
private AppCompatDelegate getDelegate() {
if (delegate == null) {
@@ -110,68 +61,21 @@ public class DevReportActivity extends BaseCrashReportDialog
}
@Override
public void init(Bundle state) {
public void init(@Nullable Bundle state) {
super.init(state);
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
getDelegate().setContentView(R.layout.activity_dev_report);
Toolbar tb = findViewById(R.id.toolbar);
getDelegate().setSupportActionBar(tb);
Toolbar toolbar = findViewById(R.id.toolbar);
getDelegate().setSupportActionBar(toolbar);
View requestReport = findViewById(R.id.request_report);
View reportForm = findViewById(R.id.report_form);
userCommentView = findViewById(R.id.user_comment);
userEmailView = findViewById(R.id.user_email);
includeDebugReport = findViewById(R.id.include_debug_report);
chevron = findViewById(R.id.chevron);
report = findViewById(R.id.report_content);
progress = findViewById(R.id.progress_wheel);
String title = getString(isFeedback() ? R.string.feedback_title :
R.string.crash_report_title);
requireNonNull(getDelegate().getSupportActionBar()).setTitle(title);
//noinspection ConstantConditions
getDelegate().getSupportActionBar().setTitle(
isFeedback() ? R.string.feedback_title :
R.string.crash_report_title);
userCommentView.setHint(isFeedback() ? R.string.enter_feedback :
R.string.describe_crash);
if (isFeedback()) {
includeDebugReport
.setText(getString(R.string.include_debug_report_feedback));
reportForm.setVisibility(VISIBLE);
requestReport.setVisibility(INVISIBLE);
} else {
includeDebugReport.setChecked(true);
reportForm.setVisibility(INVISIBLE);
requestReport.setVisibility(VISIBLE);
}
findViewById(R.id.acceptButton).setOnClickListener(v -> {
reviewing = true;
reportForm.setVisibility(VISIBLE);
requestReport.setVisibility(INVISIBLE);
((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
.showSoftInput(userCommentView, SHOW_FORCED);
});
findViewById(R.id.declineButton).setOnClickListener(v -> closeReport());
chevron.setOnClickListener(v -> {
boolean show =
chevron.getText().equals(getString(R.string.show));
if (show) {
chevron.setText(R.string.hide);
refresh();
} else {
chevron.setText(R.string.show);
report.setVisibility(GONE);
}
});
if (state != null)
reviewing = state.getBoolean(STATE_REVIEWING, isFeedback());
if (!isFeedback() && !reviewing)
requestReport.setVisibility(VISIBLE);
if (state == null) displayFragment(isFeedback());
}
@Override
@@ -181,47 +85,17 @@ public class DevReportActivity extends BaseCrashReportDialog
}
@Override
public void onPostCreate(Bundle state) {
public void onPostCreate(@Nullable Bundle state) {
super.onPostCreate(state);
getDelegate().onPostCreate(state);
}
@Override
public void onStart() {
super.onStart();
if (chevron.isSelected()) refresh();
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getDelegate().getMenuInflater();
inflater.inflate(R.menu.dev_report_actions, menu);
sendReport = menu.findItem(R.id.action_send_report);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.action_send_report:
processReport();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
@@ -234,12 +108,6 @@ public class DevReportActivity extends BaseCrashReportDialog
getDelegate().onConfigurationChanged(newConfig);
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putBoolean(STATE_REVIEWING, reviewing);
}
@Override
public void onStop() {
super.onStop();
@@ -257,132 +125,51 @@ public class DevReportActivity extends BaseCrashReportDialog
closeReport();
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ReportField field = (ReportField) buttonView.getTag();
if (field != null) {
if (isChecked) excludedFields.remove(field);
else excludedFields.add(field);
}
void sendCrashReport(String comment, String email) {
sendCrash(comment, email);
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
private boolean isFeedback() {
return getException() instanceof UserFeedback;
}
private void refresh() {
report.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE);
report.removeAllViews();
new AsyncTask<Void, Void, CrashReportData>() {
void displayFragment(boolean showReportForm) {
Fragment f;
if (showReportForm) {
File file =
(File) getIntent().getSerializableExtra(EXTRA_REPORT_FILE);
f = ReportFormFragment.newInstance(isFeedback(), file);
requireNonNull(getDelegate().getSupportActionBar()).show();
} else {
f = new CrashFragment();
requireNonNull(getDelegate().getSupportActionBar()).hide();
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, f.getTag())
.commit();
@Override
protected CrashReportData doInBackground(Void... args) {
File reportFile = (File) getIntent().getSerializableExtra(
EXTRA_REPORT_FILE);
CrashReportPersister persister = new CrashReportPersister();
try {
return persister.load(reportFile);
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Could not load report file", e);
return null;
}
}
@Override
protected void onPostExecute(CrashReportData crashData) {
LayoutInflater inflater = getLayoutInflater();
if (crashData != null) {
for (Entry<ReportField, Element> e : crashData.entrySet()) {
ReportField field = e.getKey();
String value = e.getValue().toString()
.replaceAll("\\\\n", "\n");
boolean required = requiredFields.contains(field);
boolean excluded = excludedFields.contains(field);
View v = inflater.inflate(R.layout.list_item_crash,
report, false);
CheckBox cb = v.findViewById(R.id.include_in_report);
cb.setTag(field);
cb.setChecked(required || !excluded);
cb.setEnabled(!required);
cb.setOnCheckedChangeListener(DevReportActivity.this);
cb.setText(field.toString());
TextView content = v.findViewById(R.id.content);
content.setText(value);
report.addView(v);
}
} else {
View v = inflater.inflate(
android.R.layout.simple_list_item_1, report, false);
TextView error = v.findViewById(android.R.id.text1);
error.setText(R.string.could_not_load_report_data);
report.addView(v);
}
report.setVisibility(VISIBLE);
progress.setVisibility(GONE);
}
}.execute();
}
private void processReport() {
userCommentView.setEnabled(false);
userEmailView.setEnabled(false);
sendReport.setEnabled(false);
progress.setVisibility(VISIBLE);
boolean includeReport = !isFeedback() || includeDebugReport.isChecked();
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... args) {
File reportFile = (File) getIntent().getSerializableExtra(
EXTRA_REPORT_FILE);
CrashReportPersister persister = new CrashReportPersister();
try {
CrashReportData data = persister.load(reportFile);
if (includeReport) {
for (ReportField field : excludedFields) {
LOG.info("Removing field " + field.name());
data.remove(field);
}
} else {
Iterator<Entry<ReportField, Element>> iter =
data.entrySet().iterator();
while (iter.hasNext()) {
Entry<ReportField, Element> e = iter.next();
if (!requiredFields.contains(e.getKey())) {
iter.remove();
}
}
}
persister.store(data, reportFile);
return true;
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Error processing report file", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
// Retrieve user's comment and email address, if any
String comment = "";
if (userCommentView != null)
comment = userCommentView.getText().toString();
String email = "";
if (userEmailView != null) {
email = userEmailView.getText().toString();
}
sendCrash(comment, email);
}
finish();
}
}.execute();
@Override
public void invalidateOptionsMenu() {
super.invalidateOptionsMenu();
getDelegate().invalidateOptionsMenu();
}
private void closeReport() {
void closeReport() {
cancelReports();
exit();
}
void exit() {
if (!isFeedback()) {
Intent i = new Intent(this, HideUiActivity.class);
i.addFlags(FLAG_ACTIVITY_NEW_TASK
| FLAG_ACTIVITY_NO_ANIMATION
| FLAG_ACTIVITY_CLEAR_TASK);
startActivity(i);
}
finish();
}
}

View File

@@ -0,0 +1,290 @@
package org.briarproject.briar.android.reporting;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.acra.ReportField;
import org.acra.collector.CrashReportData;
import org.acra.file.CrashReportPersister;
import org.acra.model.Element;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PACKAGE_NAME;
import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ReportFormFragment extends Fragment
implements OnCheckedChangeListener {
private static final Logger LOG =
getLogger(ReportFormFragment.class.getName());
private static final String IS_FEEDBACK = "isFeedback";
private static final Set<ReportField> requiredFields = new HashSet<>();
private static final Set<ReportField> excludedFields = new HashSet<>();
static {
requiredFields.add(REPORT_ID);
requiredFields.add(APP_VERSION_CODE);
requiredFields.add(APP_VERSION_NAME);
requiredFields.add(PACKAGE_NAME);
requiredFields.add(ANDROID_VERSION);
requiredFields.add(STACK_TRACE);
}
private boolean isFeedback;
private File reportFile;
private EditText userCommentView;
private EditText userEmailView;
private CheckBox includeDebugReport;
private Button chevron;
private LinearLayout report;
private View progress;
@Nullable
private MenuItem sendReport;
static ReportFormFragment newInstance(boolean isFeedback,
File reportFile) {
ReportFormFragment f = new ReportFormFragment();
Bundle args = new Bundle();
args.putBoolean(IS_FEEDBACK, isFeedback);
args.putSerializable(EXTRA_REPORT_FILE, reportFile);
f.setArguments(args);
return f;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_report_form, container,
false);
userCommentView = v.findViewById(R.id.user_comment);
userEmailView = v.findViewById(R.id.user_email);
includeDebugReport = v.findViewById(R.id.include_debug_report);
chevron = v.findViewById(R.id.chevron);
report = v.findViewById(R.id.report_content);
progress = v.findViewById(R.id.progress_wheel);
Bundle args = requireNonNull(getArguments());
isFeedback = args.getBoolean(IS_FEEDBACK);
reportFile =
(File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE));
if (isFeedback) {
includeDebugReport
.setText(getString(R.string.include_debug_report_feedback));
userCommentView.setHint(R.string.enter_feedback);
} else {
includeDebugReport.setChecked(true);
userCommentView.setHint(R.string.describe_crash);
}
chevron.setOnClickListener(view -> {
boolean show = chevron.getText().equals(getString(R.string.show));
if (show) {
chevron.setText(R.string.hide);
refresh();
} else {
chevron.setText(R.string.show);
report.setVisibility(GONE);
}
});
return v;
}
@Override
public void onStart() {
super.onStart();
if (chevron.isSelected()) refresh();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.dev_report_actions, menu);
sendReport = menu.findItem(R.id.action_send_report);
// calling setShowAsAction() shouldn't be needed, but for some reason is
sendReport.setShowAsAction(SHOW_AS_ACTION_ALWAYS);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_send_report) {
processReport();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ReportField field = (ReportField) buttonView.getTag();
if (field != null) {
if (isChecked) excludedFields.remove(field);
else excludedFields.add(field);
}
}
private void refresh() {
report.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE);
report.removeAllViews();
new AsyncTask<Void, Void, CrashReportData>() {
@Override
protected CrashReportData doInBackground(Void... args) {
CrashReportPersister persister = new CrashReportPersister();
try {
return persister.load(reportFile);
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Could not load report file", e);
return null;
}
}
@Override
protected void onPostExecute(CrashReportData crashData) {
LayoutInflater inflater = getLayoutInflater();
if (crashData != null) {
for (Map.Entry<ReportField, Element> e : crashData
.entrySet()) {
ReportField field = e.getKey();
StringBuilder valueBuilder = new StringBuilder();
for (String pair : e.getValue().flatten()) {
valueBuilder.append(pair).append("\n");
}
String value = valueBuilder.toString();
boolean required = requiredFields.contains(field);
boolean excluded = excludedFields.contains(field);
View v = inflater.inflate(R.layout.list_item_crash,
report, false);
CheckBox cb = v.findViewById(R.id.include_in_report);
cb.setTag(field);
cb.setChecked(required || !excluded);
cb.setEnabled(!required);
cb.setOnCheckedChangeListener(ReportFormFragment.this);
cb.setText(field.toString());
TextView content = v.findViewById(R.id.content);
content.setText(value);
report.addView(v);
}
} else {
View v = inflater.inflate(
android.R.layout.simple_list_item_1, report, false);
TextView error = v.findViewById(android.R.id.text1);
error.setText(R.string.could_not_load_report_data);
report.addView(v);
}
report.setVisibility(VISIBLE);
progress.setVisibility(GONE);
}
}.execute();
}
private void processReport() {
userCommentView.setEnabled(false);
userEmailView.setEnabled(false);
requireNonNull(sendReport).setEnabled(false);
progress.setVisibility(VISIBLE);
boolean includeReport = !isFeedback || includeDebugReport.isChecked();
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... args) {
CrashReportPersister persister = new CrashReportPersister();
try {
CrashReportData data = persister.load(reportFile);
if (includeReport) {
for (ReportField field : excludedFields) {
LOG.info("Removing field " + field.name());
data.remove(field);
}
} else {
Iterator<Map.Entry<ReportField, Element>> iter =
data.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<ReportField, Element> e = iter.next();
if (!requiredFields.contains(e.getKey())) {
iter.remove();
}
}
}
persister.store(data, reportFile);
return true;
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Error processing report file", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
// Retrieve user's comment and email address, if any
String comment = "";
if (userCommentView != null)
comment = userCommentView.getText().toString();
String email = "";
if (userEmailView != null) {
email = userEmailView.getText().toString();
}
getDevReportActivity().sendCrashReport(comment, email);
}
if (getActivity() != null) getDevReportActivity().exit();
}
}.execute();
}
private DevReportActivity getDevReportActivity() {
return (DevReportActivity) requireNonNull(getActivity());
}
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.sharing;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
@@ -19,6 +18,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.LargeTextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List;
@@ -83,7 +83,8 @@ public abstract class BaseMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
// disable button to prevent accidental double actions
sendController.setReady(false);
message.hideSoftKeyboard();

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.threaded;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.CallSuper;
@@ -34,6 +33,7 @@ import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection;
import java.util.List;
@@ -341,7 +341,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError();
I replyItem = adapter.getHighlightedItem();

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.support.v7.widget.RecyclerView;
@@ -10,11 +9,13 @@ import android.view.LayoutInflater;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItemResult;
import java.util.Collection;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static java.util.Objects.requireNonNull;
@@ -60,34 +61,28 @@ public class ImagePreview extends ConstraintLayout {
this.listener = listener;
}
void showPreview(Collection<Uri> imageUris) {
void showPreview(Collection<ImagePreviewItem> items) {
if (listener == null) throw new IllegalStateException();
if (imageUris.size() == 1) {
if (items.size() == 1) {
LayoutParams params = (LayoutParams) imageList.getLayoutParams();
params.width = MATCH_PARENT;
imageList.setLayoutParams(params);
}
setVisibility(VISIBLE);
imageList.setAdapter(new ImagePreviewAdapter(imageUris, listener));
ImagePreviewAdapter adapter = new ImagePreviewAdapter(items);
imageList.setAdapter(adapter);
}
void removeUri(Uri uri) {
void loadPreviewImage(AttachmentItemResult result) {
ImagePreviewAdapter adapter =
(ImagePreviewAdapter) imageList.getAdapter();
requireNonNull(adapter).removeUri(uri);
((ImagePreviewAdapter) imageList.getAdapter());
int pos = requireNonNull(adapter).loadItemPreview(result);
if (pos != NO_POSITION) {
imageList.smoothScrollToPosition(pos);
}
}
interface ImagePreviewListener {
void onPreviewLoaded();
/**
* Called when Glide can't load a preview image.
*
* Warning: Glide may call this multiple times.
*/
void onUriError(Uri uri);
void onCancel();
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.view;
import android.net.Uri;
import android.support.annotation.LayoutRes;
import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
@@ -9,25 +8,24 @@ import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
import org.briarproject.briar.android.attachment.AttachmentItemResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static java.util.Objects.requireNonNull;
@NotNullByDefault
class ImagePreviewAdapter extends Adapter<ImagePreviewViewHolder> {
private final List<Uri> items;
private final ImagePreviewListener listener;
private final List<ImagePreviewItem> items;
@LayoutRes
private final int layout;
ImagePreviewAdapter(Collection<Uri> items, ImagePreviewListener listener) {
ImagePreviewAdapter(Collection<ImagePreviewItem> items) {
this.items = new ArrayList<>(items);
this.listener = listener;
this.layout = items.size() == 1 ?
R.layout.list_item_image_preview_single :
R.layout.list_item_image_preview;
@@ -38,7 +36,7 @@ class ImagePreviewAdapter extends Adapter<ImagePreviewViewHolder> {
int type) {
View v = LayoutInflater.from(viewGroup.getContext())
.inflate(layout, viewGroup, false);
return new ImagePreviewViewHolder(v, requireNonNull(listener));
return new ImagePreviewViewHolder(v);
}
@Override
@@ -52,11 +50,17 @@ class ImagePreviewAdapter extends Adapter<ImagePreviewViewHolder> {
return items.size();
}
void removeUri(Uri uri) {
int pos = items.indexOf(uri);
if (pos == -1) return;
items.remove(uri);
notifyItemRemoved(pos);
int loadItemPreview(AttachmentItemResult result) {
ImagePreviewItem newItem = new ImagePreviewItem(result.getUri());
int pos = items.indexOf(newItem);
if (pos == NO_POSITION) throw new AssertionError();
ImagePreviewItem item = items.get(pos);
if (item.getItem() == null) {
item.setItem(requireNonNull(result.getItem()));
notifyItemChanged(pos, item);
return pos;
}
return NO_POSITION;
}
}

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