Compare commits

...

72 Commits

Author SHA1 Message Date
akwizgran
b6b15fe657 Only cache attachment items that include size. 2019-06-19 13:30:06 +01:00
akwizgran
f3bbc7179e Move deletion of unsent attachments into task. 2019-06-19 13:30:06 +01:00
akwizgran
9abe32ab4b Refactor attachment code to reduce mutable state. 2019-06-19 13:30:06 +01:00
akwizgran
d07b98eae1 Code cleanups, javadoc. 2019-06-19 13:30:05 +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
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
143 changed files with 3896 additions and 1394 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; throws DbException, FormatException;
void addLocalMessage(Transaction txn, Message m, BdfDictionary metadata, 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) Message createMessage(GroupId g, long timestamp, BdfList body)
throws FormatException; throws FormatException;
@@ -108,7 +111,7 @@ public interface ClientHelper {
Author parseAndValidateAuthor(BdfList author) throws FormatException; Author parseAndValidateAuthor(BdfList author) throws FormatException;
PublicKey parseAndValidateAgreementPublicKey(byte[] publicKeyBytes) PublicKey parseAndValidateAgreementPublicKey(byte[] publicKeyBytes)
throws FormatException; throws FormatException;
TransportProperties parseAndValidateTransportProperties( TransportProperties parseAndValidateTransportProperties(
BdfDictionary properties) throws FormatException; BdfDictionary properties) throws FormatException;

View File

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

View File

@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.transport.TransportKeySet;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -77,7 +78,7 @@ public interface DatabaseComponent extends TransactionManager {
* Stores a local message. * Stores a local message.
*/ */
void addLocalMessage(Transaction txn, Message m, Metadata meta, void addLocalMessage(Transaction txn, Message m, Metadata meta,
boolean shared) throws DbException; boolean shared, boolean temporary) throws DbException;
/** /**
* Stores a pending contact. * Stores a pending contact.
@@ -427,6 +428,13 @@ public interface DatabaseComponent extends TransactionManager {
*/ */
Settings getSettings(Transaction txn, String namespace) throws DbException; 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. * Returns all transport keys for the given transport.
* <p/> * <p/>
@@ -510,6 +518,12 @@ public interface DatabaseComponent extends TransactionManager {
void removePendingContact(Transaction txn, PendingContactId p) void removePendingContact(Transaction txn, PendingContactId p)
throws DbException; 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. * 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, void setGroupVisibility(Transaction txn, ContactId c, GroupId g,
Visibility v) throws DbException; 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. * Marks the given message as shared.
*/ */
@@ -568,6 +587,12 @@ public interface DatabaseComponent extends TransactionManager {
void setReorderingWindow(Transaction txn, KeySetId k, TransportId t, void setReorderingWindow(Transaction txn, KeySetId k, TransportId t,
long timePeriod, long base, byte[] bitmap) throws DbException; 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. * Marks the given transport keys as usable for outgoing streams.
*/ */

View File

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

View File

@@ -1,8 +1,14 @@
package org.briarproject.bramble.api.sync; 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.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@Immutable
@NotNullByDefault
public class Message { public class Message {
/** /**

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,9 @@ package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.UniqueId; 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; import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
public interface SyncConstants { public interface SyncConstants {
@@ -11,6 +14,11 @@ public interface SyncConstants {
*/ */
byte PROTOCOL_VERSION = 0; 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. * 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. * The maximum number of message IDs in an ack, offer or request record.
*/ */
int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH; 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; 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 writeRequest(Request r) throws IOException;
void writeVersions(Versions v) throws IOException;
void flush() 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 @Override
public void addLocalMessage(Message m, BdfDictionary metadata, public void addLocalMessage(Message m, BdfDictionary metadata,
boolean shared) throws DbException, FormatException { 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 @Override
public void addLocalMessage(Transaction txn, Message m, public void addLocalMessage(Transaction txn, Message m,
BdfDictionary metadata, boolean shared) BdfDictionary metadata, boolean shared, boolean temporary)
throws DbException, FormatException { 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 @Override

View File

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

View File

@@ -33,6 +33,7 @@ import org.briarproject.bramble.api.transport.TransportKeySet;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -115,7 +116,7 @@ interface Database<T> {
* if the message was created locally. * if the message was created locally.
*/ */
void addMessage(T txn, Message m, MessageState state, boolean shared, 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 * 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; 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. * Returns all transport keys for the given transport.
* <p/> * <p/>
@@ -630,6 +638,12 @@ interface Database<T> {
*/ */
void removePendingContact(T txn, PendingContactId p) throws DbException; 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. * 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, void setHandshakeKeyPair(T txn, AuthorId local, PublicKey publicKey,
PrivateKey privateKey) throws DbException; 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. * Marks the given message as shared.
*/ */
@@ -689,6 +708,12 @@ interface Database<T> {
void setReorderingWindow(T txn, KeySetId k, TransportId t, void setReorderingWindow(T txn, KeySetId k, TransportId t,
long timePeriod, long base, byte[] bitmap) throws DbException; 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. * 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.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; 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.sync.validation.MessageState;
import org.briarproject.bramble.api.transport.KeySetId; import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeySet; import org.briarproject.bramble.api.transport.TransportKeySet;
@@ -273,13 +274,14 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Override @Override
public void addLocalMessage(Transaction transaction, Message m, 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(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsGroup(txn, m.getGroupId())) if (!db.containsGroup(txn, m.getGroupId()))
throw new NoSuchGroupException(); throw new NoSuchGroupException();
if (!db.containsMessage(txn, m.getId())) { 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 MessageAddedEvent(m, null));
transaction.attach(new MessageStateChangedEvent(m.getId(), true, transaction.attach(new MessageStateChangedEvent(m.getId(), true,
DELIVERED)); DELIVERED));
@@ -715,6 +717,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getSettings(txn, namespace); 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 @Override
public Collection<TransportKeySet> getTransportKeys(Transaction transaction, public Collection<TransportKeySet> getTransportKeys(Transaction transaction,
TransportId t) throws DbException { TransportId t) throws DbException {
@@ -800,7 +811,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.raiseSeenFlag(txn, c, m.getId()); db.raiseSeenFlag(txn, c, m.getId());
db.raiseAckFlag(txn, c, m.getId()); db.raiseAckFlag(txn, c, m.getId());
} else { } 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 MessageAddedEvent(m, c));
} }
transaction.attach(new MessageToAckEvent(c)); transaction.attach(new MessageToAckEvent(c));
@@ -908,6 +919,14 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
transaction.attach(new PendingContactRemovedEvent(p)); 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 @Override
public void removeTransport(Transaction transaction, TransportId t) public void removeTransport(Transaction transaction, TransportId t)
throws DbException { throws DbException {
@@ -967,6 +986,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
transaction.attach(new GroupVisibilityUpdatedEvent(affected)); 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 @Override
public void setMessageShared(Transaction transaction, MessageId m) public void setMessageShared(Transaction transaction, MessageId m)
throws DbException { throws DbException {
@@ -975,8 +1004,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsMessage(txn, m)) if (!db.containsMessage(txn, m))
throw new NoSuchMessageException(); throw new NoSuchMessageException();
if (db.getMessageState(txn, m) != DELIVERED) if (db.getMessageState(txn, m) != DELIVERED)
throw new IllegalArgumentException( throw new IllegalArgumentException("Shared undelivered message");
"Shared undelivered message");
db.setMessageShared(txn, m); db.setMessageShared(txn, m);
transaction.attach(new MessageSharedEvent(m)); transaction.attach(new MessageSharedEvent(m));
} }
@@ -1028,6 +1056,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.setReorderingWindow(txn, k, t, timePeriod, base, bitmap); 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 @Override
public void setTransportKeysActive(Transaction transaction, TransportId t, public void setTransportKeysActive(Transaction transaction, TransportId t,
KeySetId k) throws DbException { KeySetId k) throws DbException {

View File

@@ -62,6 +62,7 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import static java.sql.Types.BINARY; import static java.sql.Types.BINARY;
import static java.sql.Types.BOOLEAN; import static java.sql.Types.BOOLEAN;
@@ -97,7 +98,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database<Connection> { abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing // 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 // Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1; private static final int OFFSET_PREV = -1;
@@ -134,6 +135,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " handshakePublicKey _BINARY," // Null if key is unknown + " handshakePublicKey _BINARY," // Null if key is unknown
+ " localAuthorId _HASH NOT NULL," + " localAuthorId _HASH NOT NULL,"
+ " verified BOOLEAN NOT NULL," + " verified BOOLEAN NOT NULL,"
+ " syncVersions _BINARY DEFAULT '00' NOT NULL,"
+ " PRIMARY KEY (contactId)," + " PRIMARY KEY (contactId),"
+ " FOREIGN KEY (localAuthorId)" + " FOREIGN KEY (localAuthorId)"
+ " REFERENCES localAuthors (authorId)" + " REFERENCES localAuthors (authorId)"
@@ -177,6 +179,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " timestamp BIGINT NOT NULL," + " timestamp BIGINT NOT NULL,"
+ " state INT NOT NULL," + " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL," + " shared BOOLEAN NOT NULL,"
+ " temporary BOOLEAN NOT NULL,"
+ " length INT NOT NULL," + " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted + " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId)," + " PRIMARY KEY (messageId),"
@@ -336,25 +339,26 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final Logger LOG = private static final Logger LOG =
getLogger(JdbcDatabase.class.getName()); getLogger(JdbcDatabase.class.getName());
// Different database libraries use different names for certain types
private final MessageFactory messageFactory; private final MessageFactory messageFactory;
private final Clock clock; private final Clock clock;
private final DatabaseTypes dbTypes; 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 final LinkedList<Connection> connections = new LinkedList<>();
private int openConnections = 0; // Locking: connectionsLock @GuardedBy("connectionsLock")
private boolean closed = false; // Locking: connectionsLock private int openConnections = 0;
@GuardedBy("connectionsLock")
private boolean closed = false;
protected abstract Connection createConnection() protected abstract Connection createConnection()
throws DbException, SQLException; throws DbException, SQLException;
protected abstract void compactAndClose() throws DbException; protected abstract void compactAndClose() throws DbException;
private final Lock connectionsLock = new ReentrantLock();
private final Condition connectionsChanged = connectionsLock.newCondition();
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory, JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
Clock clock) { Clock clock) {
this.dbTypes = databaseTypes; this.dbTypes = databaseTypes;
@@ -457,7 +461,9 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration41_42(dbTypes), new Migration41_42(dbTypes),
new Migration42_43(dbTypes), new Migration42_43(dbTypes),
new Migration43_44(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 @Override
public void addMessage(Connection txn, Message m, MessageState state, public void addMessage(Connection txn, Message m, MessageState state,
boolean messageShared, @Nullable ContactId sender) boolean shared, boolean temporary, @Nullable ContactId sender)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "INSERT INTO messages (messageId, groupId, timestamp," String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
+ " state, shared, length, raw)" + " state, shared, temporary, length, raw)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes()); ps.setBytes(1, m.getId().getBytes());
ps.setBytes(2, m.getGroupId().getBytes()); ps.setBytes(2, m.getGroupId().getBytes());
ps.setLong(3, m.getTimestamp()); ps.setLong(3, m.getTimestamp());
ps.setInt(4, state.getValue()); ps.setInt(4, state.getValue());
ps.setBoolean(5, messageShared); ps.setBoolean(5, shared);
ps.setBoolean(6, temporary);
byte[] raw = messageFactory.getRawMessage(m); byte[] raw = messageFactory.getRawMessage(m);
ps.setInt(6, raw.length); ps.setInt(7, raw.length);
ps.setBytes(7, raw); ps.setBytes(8, raw);
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); ps.close();
@@ -804,8 +811,7 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean offered = removeOfferedMessage(txn, c, m.getId()); boolean offered = removeOfferedMessage(txn, c, m.getId());
boolean seen = offered || c.equals(sender); boolean seen = offered || c.equals(sender);
addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(), addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(),
raw.length, state, e.getValue(), messageShared, raw.length, state, e.getValue(), shared, false, seen);
false, seen);
} }
// Update denormalised column in messageDependencies if dependency // Update denormalised column in messageDependencies if dependency
// is in same group as dependent // 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 @Override
public Collection<TransportKeySet> getTransportKeys(Connection txn, public Collection<TransportKeySet> getTransportKeys(Connection txn,
TransportId t) throws DbException { 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 @Override
public void removeTransport(Connection txn, TransportId t) public void removeTransport(Connection txn, TransportId t)
throws DbException { 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 @Override
public void setMessageShared(Connection txn, MessageId m) public void setMessageShared(Connection txn, MessageId m)
throws DbException { 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 @Override
public void setTransportKeysActive(Connection txn, TransportId t, public void setTransportKeysActive(Connection txn, TransportId t,
KeySetId k) throws DbException { 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); else logDuration(LOG, "Creating database", start);
db.transaction(false, txn -> { db.transaction(false, txn -> {
long start1 = now();
db.removeTemporaryMessages(txn);
logDuration(LOG, "Removing temporary messages", start1);
for (OpenDatabaseHook hook : openDatabaseHooks) { for (OpenDatabaseHook hook : openDatabaseHooks) {
long start1 = now(); start1 = now();
hook.onDatabaseOpened(txn); hook.onDatabaseOpened(txn);
if (LOG.isLoggable(FINE)) { if (LOG.isLoggable(FINE)) {
logDuration(LOG, "Calling open database hook " logDuration(LOG, "Calling open database hook "

View File

@@ -284,7 +284,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
meta.put("transportId", t.getString()); meta.put("transportId", t.getString());
meta.put("version", version); meta.put("version", version);
meta.put("local", local); meta.put("local", local);
clientHelper.addLocalMessage(txn, m, meta, shared); clientHelper.addLocalMessage(txn, m, meta, shared, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(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 java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT; 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.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.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNull;
import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS; 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) { private void addPendingContact(PendingContact p) {
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
long expiry = p.getTimestamp() + RENDEZVOUS_TIMEOUT_MS; long expiry = p.getTimestamp() + RENDEZVOUS_TIMEOUT_MS;
if (expiry > now) { if (expiry <= now) {
broadcastState(p.getId(), WAITING_FOR_CONNECTION);
} else {
broadcastState(p.getId(), FAILED); broadcastState(p.getId(), FAILED);
return; return;
} }
@@ -180,9 +179,13 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
for (PluginState ps : pluginStates.values()) { for (PluginState ps : pluginStates.values()) {
RendezvousEndpoint endpoint = RendezvousEndpoint endpoint =
createEndpoint(ps.plugin, p.getId(), cs); createEndpoint(ps.plugin, p.getId(), cs);
if (endpoint != null) if (endpoint != null) {
requireNull(ps.endpoints.put(p.getId(), endpoint)); 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) { } catch (DbException | GeneralSecurityException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
@@ -328,9 +331,14 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
TransportId t = plugin.getId(); TransportId t = plugin.getId();
Map<PendingContactId, RendezvousEndpoint> endpoints = new HashMap<>(); Map<PendingContactId, RendezvousEndpoint> endpoints = new HashMap<>();
for (Entry<PendingContactId, CryptoState> e : cryptoStates.entrySet()) { for (Entry<PendingContactId, CryptoState> e : cryptoStates.entrySet()) {
RendezvousEndpoint endpoint = PendingContactId p = e.getKey();
createEndpoint(plugin, e.getKey(), e.getValue()); CryptoState cs = e.getValue();
if (endpoint != null) endpoints.put(e.getKey(), endpoint); 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))); requireNull(pluginStates.put(t, new PluginState(plugin, endpoints)));
} }
@@ -344,8 +352,11 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
private void removeTransport(TransportId t) { private void removeTransport(TransportId t) {
PluginState ps = pluginStates.remove(t); PluginState ps = pluginStates.remove(t);
if (ps != null) { if (ps != null) {
for (RendezvousEndpoint endpoint : ps.endpoints.values()) { for (Entry<PendingContactId, RendezvousEndpoint> e :
tryToClose(endpoint, LOG, INFO); 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 boolean alice;
private final long expiry; private final long expiry;
private int numEndpoints = 0;
private CryptoState(SecretKey rendezvousKey, boolean alice, private CryptoState(SecretKey rendezvousKey, boolean alice,
long expiry) { long expiry) {
this.rendezvousKey = rendezvousKey; 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.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession; 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.GroupVisibilityUpdatedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent; import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent; 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.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; 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.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.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.SUPPORTED_VERSIONS;
import static org.briarproject.bramble.util.LogUtils.logException; 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 { class DuplexOutgoingSession implements SyncSession, EventListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(DuplexOutgoingSession.class.getName()); getLogger(DuplexOutgoingSession.class.getName());
private static final ThrowingRunnable<IOException> CLOSE = () -> { private static final ThrowingRunnable<IOException> CLOSE = () -> {
}; };
@@ -103,6 +106,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
public void run() throws IOException { public void run() throws IOException {
eventBus.addListener(this); eventBus.addListener(this);
try { try {
// Send our supported protocol versions
recordWriter.writeVersions(new Versions(SUPPORTED_VERSIONS));
// Start a query for each type of record // Start a query for each type of record
generateAck(); generateAck();
generateBatch(); 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.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader; import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.SyncSession; import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.util.LogUtils.logException; 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 { class IncomingSession implements SyncSession, EventListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(IncomingSession.class.getName()); getLogger(IncomingSession.class.getName());
private final DatabaseComponent db; private final DatabaseComponent db;
private final Executor dbExecutor; private final Executor dbExecutor;
@@ -80,6 +83,9 @@ class IncomingSession implements SyncSession, EventListener {
} else if (recordReader.hasRequest()) { } else if (recordReader.hasRequest()) {
Request r = recordReader.readRequest(); Request r = recordReader.readRequest();
dbExecutor.execute(new ReceiveRequest(r)); dbExecutor.execute(new ReceiveRequest(r));
} else if (recordReader.hasVersions()) {
Versions v = recordReader.readVersions();
dbExecutor.execute(new ReceiveVersions(v));
} else { } else {
// unknown records are ignored in RecordReader#eof() // unknown records are ignored in RecordReader#eof()
throw new FormatException(); 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.Message;
import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.SyncSession; import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.api.transport.StreamWriter; import org.briarproject.bramble.api.transport.StreamWriter;
import java.io.IOException; 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.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; 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.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.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.SUPPORTED_VERSIONS;
import static org.briarproject.bramble.util.LogUtils.logException; 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 { class SimplexOutgoingSession implements SyncSession, EventListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(SimplexOutgoingSession.class.getName()); getLogger(SimplexOutgoingSession.class.getName());
private static final ThrowingRunnable<IOException> CLOSE = () -> { private static final ThrowingRunnable<IOException> CLOSE = () -> {
}; };
@@ -80,6 +83,8 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
public void run() throws IOException { public void run() throws IOException {
eventBus.addListener(this); eventBus.addListener(this);
try { try {
// Send our supported protocol versions
recordWriter.writeVersions(new Versions(SUPPORTED_VERSIONS));
// Start a query for each type of record // Start a query for each type of record
dbExecutor.execute(new GenerateAck()); dbExecutor.execute(new GenerateAck());
dbExecutor.execute(new GenerateBatch()); 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.Offer;
import org.briarproject.bramble.api.sync.Request; import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader; import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.util.ByteUtils; import org.briarproject.bramble.util.ByteUtils;
import java.io.IOException; 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.MESSAGE;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER; 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.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.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION; import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@@ -45,7 +48,7 @@ class SyncRecordReaderImpl implements SyncRecordReader {
private static boolean isKnownRecordType(byte type) { private static boolean isKnownRecordType(byte type) {
return type == ACK || type == MESSAGE || type == OFFER || return type == ACK || type == MESSAGE || type == OFFER ||
type == REQUEST; type == REQUEST || type == VERSIONS;
} }
private final MessageFactory messageFactory; private final MessageFactory messageFactory;
@@ -148,4 +151,27 @@ class SyncRecordReaderImpl implements SyncRecordReader {
if (!hasRequest()) throw new FormatException(); if (!hasRequest()) throw new FormatException();
return new Request(readMessageIds()); 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.Offer;
import org.briarproject.bramble.api.sync.Request; import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordWriter; import org.briarproject.bramble.api.sync.SyncRecordWriter;
import org.briarproject.bramble.api.sync.Versions;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; 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.MESSAGE;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER; 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.REQUEST;
import static org.briarproject.bramble.api.sync.RecordTypes.VERSIONS;
import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION; import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
@NotThreadSafe @NotThreadSafe
@@ -65,6 +67,12 @@ class SyncRecordWriterImpl implements SyncRecordWriter {
writeRecord(REQUEST); writeRecord(REQUEST);
} }
@Override
public void writeVersions(Versions v) throws IOException {
for (byte b : v.getSupportedVersions()) payload.write(b);
writeRecord(VERSIONS);
}
@Override @Override
public void flush() throws IOException { public void flush() throws IOException {
writer.flush(); writer.flush();

View File

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

View File

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

View File

@@ -96,7 +96,7 @@ public class ClientHelperImplTest extends BrambleTestCase {
oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(metadataEncoder).encode(dictionary); oneOf(metadataEncoder).encode(dictionary);
will(returnValue(metadata)); will(returnValue(metadata));
oneOf(db).addLocalMessage(txn, message, metadata, shared); oneOf(db).addLocalMessage(txn, message, metadata, shared, false);
}}); }});
clientHelper.addLocalMessage(message, dictionary, shared); 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.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactState; import org.briarproject.bramble.api.contact.PendingContactState;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent; 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.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey; 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.Identity;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestDatabaseConfigModule; import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestDuplexTransportConnection; 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.assertNotNull;
import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.fail; 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.TestDuplexTransportConnection.createPair;
import static org.briarproject.bramble.test.TestPluginConfigModule.DUPLEX_TRANSPORT_ID; import static org.briarproject.bramble.test.TestPluginConfigModule.DUPLEX_TRANSPORT_ID;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
@@ -188,9 +192,14 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
private PendingContact addPendingContact( private PendingContact addPendingContact(
ContactExchangeIntegrationTestComponent local, ContactExchangeIntegrationTestComponent local,
ContactExchangeIntegrationTestComponent remote) throws Exception { ContactExchangeIntegrationTestComponent remote) throws Exception {
EventWaiter waiter = new EventWaiter();
local.getEventBus().addListener(waiter);
String link = remote.getContactManager().getHandshakeLink(); String link = remote.getContactManager().getHandshakeLink();
String alias = remote.getIdentityManager().getLocalAuthor().getName(); 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, private void assertContacts(boolean verified,
@@ -237,7 +246,7 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
assertEquals(1, pairs.size()); assertEquals(1, pairs.size());
Pair<PendingContact, PendingContactState> pair = Pair<PendingContact, PendingContactState> pair =
pairs.iterator().next(); pairs.iterator().next();
assertEquals(WAITING_FOR_CONNECTION, pair.getSecond()); assertEquals(OFFLINE, pair.getSecond());
PendingContact pendingContact = pair.getFirst(); PendingContact pendingContact = pair.getFirst();
assertEquals(expectedIdentity.getLocalAuthor().getName(), assertEquals(expectedIdentity.getLocalAuthor().getName(),
pendingContact.getAlias()); pendingContact.getAlias());
@@ -261,4 +270,19 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
tearDown(bob); tearDown(bob);
deleteTestDirectory(testDir); 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.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Random;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; 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 Contact contact;
private final KeySetId keySetId; private final KeySetId keySetId;
private final PendingContactId pendingContactId; private final PendingContactId pendingContactId;
private final Random random = new Random();
private final boolean shared = random.nextBoolean();
private final boolean temporary = random.nextBoolean();
public DatabaseComponentImplTest() { public DatabaseComponentImplTest() {
clientId = getClientId(); clientId = getClientId();
@@ -253,7 +258,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.addLocalMessage(transaction, message, metadata, true)); db.addLocalMessage(transaction, message, metadata, shared,
temporary));
} }
@Test @Test
@@ -265,20 +271,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true)); will(returnValue(true));
oneOf(database).containsMessage(txn, messageId); oneOf(database).containsMessage(txn, messageId);
will(returnValue(false)); 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).mergeMessageMetadata(txn, messageId, metadata);
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// The message was added, so the listeners should be called // The message was added, so the listeners should be called
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
oneOf(eventBus) oneOf(eventBus).broadcast(with(any(
.broadcast(with(any(MessageStateChangedEvent.class))); MessageStateChangedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); if (shared)
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.addLocalMessage(transaction, message, metadata, true)); db.addLocalMessage(transaction, message, metadata, shared,
temporary));
} }
@Test @Test
@@ -286,11 +295,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not) // 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)); will(returnValue(txn));
exactly(16).of(database).containsContact(txn, contactId); exactly(18).of(database).containsContact(txn, contactId);
will(returnValue(false)); will(returnValue(false));
exactly(16).of(database).abortTransaction(txn); exactly(18).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
@@ -368,6 +377,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected // Expected
} }
try {
db.transaction(false, transaction ->
db.getSyncVersions(transaction, contactId));
fail();
} catch (NoSuchContactException expected) {
// Expected
}
try { try {
Ack a = new Ack(singletonList(messageId)); Ack a = new Ack(singletonList(messageId));
db.transaction(false, transaction -> db.transaction(false, transaction ->
@@ -427,6 +444,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} catch (NoSuchContactException expected) { } catch (NoSuchContactException expected) {
// Expected // Expected
} }
try {
db.transaction(false, transaction ->
db.setSyncVersions(transaction, contactId, emptyList()));
fail();
} catch (NoSuchContactException expected) {
// Expected
}
} }
@Test @Test
@@ -569,11 +594,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the message is in the DB (which it's not) // 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)); will(returnValue(txn));
exactly(11).of(database).containsMessage(txn, messageId); exactly(12).of(database).containsMessage(txn, messageId);
will(returnValue(false)); will(returnValue(false));
exactly(11).of(database).abortTransaction(txn); exactly(12).of(database).abortTransaction(txn);
// Allow other checks to pass // Allow other checks to pass
allowing(database).containsContact(txn, contactId); allowing(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
@@ -637,6 +662,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected // Expected
} }
try {
db.transaction(false, transaction ->
db.setMessagePermanent(transaction, message.getId()));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try { try {
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.setMessageShared(transaction, message.getId())); db.setMessageShared(transaction, message.getId()));
@@ -972,7 +1005,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(VISIBLE)); will(returnValue(VISIBLE));
oneOf(database).containsMessage(txn, messageId); oneOf(database).containsMessage(txn, messageId);
will(returnValue(false)); will(returnValue(false));
oneOf(database).addMessage(txn, message, UNKNOWN, false, contactId); oneOf(database).addMessage(txn, message, UNKNOWN, false, false,
contactId);
// Second time // Second time
oneOf(database).containsContact(txn, contactId); oneOf(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
@@ -1507,6 +1541,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
public void testMessageDependencies() throws Exception { public void testMessageDependencies() throws Exception {
int shutdownHandle = 12345; int shutdownHandle = 12345;
MessageId messageId2 = new MessageId(getRandomId()); MessageId messageId2 = new MessageId(getRandomId());
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// open() // open()
oneOf(database).open(key, null); oneOf(database).open(key, null);
@@ -1521,7 +1556,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(true)); will(returnValue(true));
oneOf(database).containsMessage(txn, messageId); oneOf(database).containsMessage(txn, messageId);
will(returnValue(false)); 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).mergeMessageMetadata(txn, messageId, metadata);
// addMessageDependencies() // addMessageDependencies()
oneOf(database).containsMessage(txn, messageId); 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(MessageAddedEvent.class)));
oneOf(eventBus).broadcast(with(any( oneOf(eventBus).broadcast(with(any(
MessageStateChangedEvent.class))); MessageStateChangedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); if (shared)
oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
// endTransaction() // endTransaction()
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// close() // close()
@@ -1555,7 +1592,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
assertFalse(db.open(key, null)); assertFalse(db.open(key, null));
db.transaction(false, transaction -> { db.transaction(false, transaction -> {
db.addLocalMessage(transaction, message, metadata, true); db.addLocalMessage(transaction, message, metadata, shared,
temporary);
Collection<MessageId> dependencies = new ArrayList<>(2); Collection<MessageId> dependencies = new ArrayList<>(2);
dependencies.add(messageId1); dependencies.add(messageId1);
dependencies.add(messageId2); dependencies.add(messageId2);

View File

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

View File

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

View File

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

View File

@@ -637,7 +637,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(timestamp)); will(returnValue(timestamp));
oneOf(clientHelper).createMessage(g, timestamp, body); oneOf(clientHelper).createMessage(g, timestamp, body);
will(returnValue(message)); 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 java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT; 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.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.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS; import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS;
import static org.briarproject.bramble.rendezvous.RendezvousConstants.RENDEZVOUS_TIMEOUT_MS; import static org.briarproject.bramble.rendezvous.RendezvousConstants.RENDEZVOUS_TIMEOUT_MS;
@@ -120,7 +121,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
will(returnValue(beforeExpiry)); will(returnValue(beforeExpiry));
oneOf(eventBus).broadcast(with(new PredicateMatcher<>( oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
PendingContactStateChangedEvent.class, e -> PendingContactStateChangedEvent.class, e ->
e.getPendingContactState() == WAITING_FOR_CONNECTION))); e.getPendingContactState() == OFFLINE)));
// Capture the poll task // Capture the poll task
oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)), oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)),
with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS),
@@ -184,7 +185,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied(); context.assertIsSatisfied();
// Add the pending contact - endpoint should be created and polled // Add the pending contact - endpoint should be created and polled
expectAddUnexpiredPendingContact(beforeExpiry); expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION);
expectDeriveRendezvousKey(); expectDeriveRendezvousKey();
expectCreateEndpoint(); expectCreateEndpoint();
@@ -205,9 +206,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied(); context.assertIsSatisfied();
// Remove the pending contact - endpoint should be closed // Remove the pending contact - endpoint should be closed
context.checking(new Expectations() {{ expectCloseEndpoint();
oneOf(rendezvousEndpoint).close();
}});
rendezvousPoller.eventOccurred( rendezvousPoller.eventOccurred(
new PendingContactRemovedEvent(pendingContact.getId())); new PendingContactRemovedEvent(pendingContact.getId()));
@@ -238,7 +237,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied(); context.assertIsSatisfied();
// Add the pending contact - endpoint should be created and polled // Add the pending contact - endpoint should be created and polled
expectAddUnexpiredPendingContact(beforeExpiry); expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION);
expectDeriveRendezvousKey(); expectDeriveRendezvousKey();
expectCreateEndpoint(); expectCreateEndpoint();
@@ -260,10 +259,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
// Run the poll task - pending contact expires, endpoint is closed // Run the poll task - pending contact expires, endpoint is closed
expectPendingContactExpires(afterExpiry); expectPendingContactExpires(afterExpiry);
expectCloseEndpoint();
context.checking(new Expectations() {{
oneOf(rendezvousEndpoint).close();
}});
capturePollTask.get().run(); capturePollTask.get().run();
context.assertIsSatisfied(); context.assertIsSatisfied();
@@ -289,7 +285,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
context.assertIsSatisfied(); context.assertIsSatisfied();
// Add the pending contact - no endpoints should be created yet // Add the pending contact - no endpoints should be created yet
expectAddUnexpiredPendingContact(beforeExpiry); expectAddPendingContact(beforeExpiry, OFFLINE);
expectDeriveRendezvousKey(); expectDeriveRendezvousKey();
rendezvousPoller.eventOccurred( rendezvousPoller.eventOccurred(
@@ -299,14 +295,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
// Enable the transport - endpoint should be created // Enable the transport - endpoint should be created
expectGetPlugin(); expectGetPlugin();
expectCreateEndpoint(); expectCreateEndpoint();
expectStateChangedEvent(WAITING_FOR_CONNECTION);
rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId)); rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId));
context.assertIsSatisfied(); context.assertIsSatisfied();
// Disable the transport - endpoint should be closed // Disable the transport - endpoint should be closed
context.checking(new Expectations() {{ expectCloseEndpoint();
oneOf(rendezvousEndpoint).close(); expectStateChangedEvent(OFFLINE);
}});
rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId)); rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId));
context.assertIsSatisfied(); context.assertIsSatisfied();
@@ -482,13 +478,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
return capturePollTask; return capturePollTask;
} }
private void expectAddUnexpiredPendingContact(long now) { private void expectAddPendingContact(long now,
PendingContactState initialState) {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis(); oneOf(clock).currentTimeMillis();
will(returnValue(now)); will(returnValue(now));
oneOf(eventBus).broadcast(with(new PredicateMatcher<>( oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
PendingContactStateChangedEvent.class, e -> PendingContactStateChangedEvent.class, e ->
e.getPendingContactState() == WAITING_FOR_CONNECTION))); e.getPendingContactState() == initialState)));
}}); }});
} }
@@ -546,7 +543,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
will(returnValue(now)); will(returnValue(now));
oneOf(eventBus).broadcast(with(new PredicateMatcher<>( oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
PendingContactStateChangedEvent.class, e -> PendingContactStateChangedEvent.class, e ->
e.getPendingContactState() == WAITING_FOR_CONNECTION))); e.getPendingContactState() == OFFLINE)));
// Capture the poll task // Capture the poll task
oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)), oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)),
with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS),
@@ -576,4 +573,10 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
e.getPendingContactState() == state))); 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.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.SyncRecordWriter; 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.api.transport.StreamWriter;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations; import org.briarproject.bramble.test.DbExpectations;
@@ -49,6 +50,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
// Add listener // Add listener
oneOf(eventBus).addListener(session); oneOf(eventBus).addListener(session);
// Send the protocol versions
oneOf(recordWriter).writeVersions(with(any(Versions.class)));
// No acks to send // No acks to send
oneOf(db).transactionWithNullableResult(with(false), oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(noAckTxn)); withNullableDbCallable(noAckTxn));
@@ -83,6 +86,8 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
// Add listener // Add listener
oneOf(eventBus).addListener(session); oneOf(eventBus).addListener(session);
// Send the protocol versions
oneOf(recordWriter).writeVersions(with(any(Versions.class)));
// One ack to send // One ack to send
oneOf(db).transactionWithNullableResult(with(false), oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(ackTxn)); 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.Offer;
import org.briarproject.bramble.api.sync.Request; import org.briarproject.bramble.api.sync.Request;
import org.briarproject.bramble.api.sync.SyncRecordReader; import org.briarproject.bramble.api.sync.SyncRecordReader;
import org.briarproject.bramble.api.sync.Versions;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.List;
import javax.annotation.Nullable; 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.ACK;
import static org.briarproject.bramble.api.sync.RecordTypes.OFFER; 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.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_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.api.sync.SyncConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -35,12 +40,17 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
context.mock(MessageFactory.class); context.mock(MessageFactory.class);
private final RecordReader recordReader = context.mock(RecordReader.class); private final RecordReader recordReader = context.mock(RecordReader.class);
private SyncRecordReader reader;
@Before
public void setUp() {
reader = new SyncRecordReaderImpl(messageFactory, recordReader);
}
@Test @Test
public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception { public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception {
expectReadRecord(createAck()); expectReadRecord(createAck());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
Ack ack = reader.readAck(); Ack ack = reader.readAck();
assertEquals(MAX_MESSAGE_IDS, ack.getMessageIds().size()); assertEquals(MAX_MESSAGE_IDS, ack.getMessageIds().size());
} }
@@ -49,8 +59,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testFormatExceptionIfAckIsEmpty() throws Exception { public void testFormatExceptionIfAckIsEmpty() throws Exception {
expectReadRecord(createEmptyAck()); expectReadRecord(createEmptyAck());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
reader.readAck(); reader.readAck();
} }
@@ -58,8 +66,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception { public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception {
expectReadRecord(createOffer()); expectReadRecord(createOffer());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
Offer offer = reader.readOffer(); Offer offer = reader.readOffer();
assertEquals(MAX_MESSAGE_IDS, offer.getMessageIds().size()); assertEquals(MAX_MESSAGE_IDS, offer.getMessageIds().size());
} }
@@ -68,8 +74,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testFormatExceptionIfOfferIsEmpty() throws Exception { public void testFormatExceptionIfOfferIsEmpty() throws Exception {
expectReadRecord(createEmptyOffer()); expectReadRecord(createEmptyOffer());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
reader.readOffer(); reader.readOffer();
} }
@@ -77,8 +81,6 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception { public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception {
expectReadRecord(createRequest()); expectReadRecord(createRequest());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
Request request = reader.readRequest(); Request request = reader.readRequest();
assertEquals(MAX_MESSAGE_IDS, request.getMessageIds().size()); assertEquals(MAX_MESSAGE_IDS, request.getMessageIds().size());
} }
@@ -87,11 +89,36 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
public void testFormatExceptionIfRequestIsEmpty() throws Exception { public void testFormatExceptionIfRequestIsEmpty() throws Exception {
expectReadRecord(createEmptyRequest()); expectReadRecord(createEmptyRequest());
SyncRecordReader reader =
new SyncRecordReaderImpl(messageFactory, recordReader);
reader.readRequest(); 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 @Test
public void testEofReturnsTrueWhenAtEndOfStream() throws Exception { public void testEofReturnsTrueWhenAtEndOfStream() throws Exception {
expectReadRecord(createAck()); expectReadRecord(createAck());
@@ -140,6 +167,12 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
return new Record(PROTOCOL_VERSION, REQUEST, new byte[0]); 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 { private byte[] createPayload() throws Exception {
ByteArrayOutputStream payload = new ByteArrayOutputStream(); ByteArrayOutputStream payload = new ByteArrayOutputStream();
while (payload.size() + UniqueId.LENGTH <= MAX_RECORD_PAYLOAD_BYTES) { 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 java.util.concurrent.Executor;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
@@ -80,24 +79,9 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testStartAndStop() throws Exception { public void testStartAndStop() throws Exception {
Transaction txn = new Transaction(null, true); expectGetMessagesToValidate();
Transaction txn1 = new Transaction(null, true); expectGetPendingMessages();
Transaction txn2 = new Transaction(null, true); expectGetMessagesToShare();
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()));
}});
vm.startService(); vm.startService();
vm.stopService(); vm.stopService();
@@ -106,167 +90,134 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
@Test @Test
public void testMessagesAreValidatedAtStartup() throws Exception { public void testMessagesAreValidatedAtStartup() throws Exception {
Transaction txn = new Transaction(null, true); Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true); Transaction txn1 = new Transaction(null, false);
Transaction txn2 = new Transaction(null, false); Transaction txn2 = new Transaction(null, true);
Transaction txn3 = new Transaction(null, true); Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, false);
Transaction txn5 = new Transaction(null, true); expectGetMessagesToValidate(messageId, messageId1);
Transaction txn6 = new Transaction(null, true);
context.checking(new DbExpectations() {{ 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 // Load the first raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessage(txn1, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
oneOf(db).getGroup(txn1, groupId); oneOf(db).getGroup(txn, groupId);
will(returnValue(group)); will(returnValue(group));
// Validate the first message: valid // Validate the first message: valid
oneOf(validator).validateMessage(message, group); oneOf(validator).validateMessage(message, group);
will(returnValue(validResult)); will(returnValue(validResult));
// Store the validation result for the first message // Store the validation result for the first message
oneOf(db).transaction(with(false), withDbRunnable(txn2)); oneOf(db).transaction(with(false), withDbRunnable(txn1));
oneOf(db).mergeMessageMetadata(txn2, messageId, metadata); oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
// Deliver the first message // Deliver the first message
oneOf(hook).incomingMessage(txn2, message, metadata); oneOf(hook).incomingMessage(txn1, message, metadata);
will(returnValue(false)); will(returnValue(false));
oneOf(db).setMessageState(txn2, messageId, DELIVERED); oneOf(db).setMessageState(txn1, messageId, DELIVERED);
// Get any pending dependents // Get any pending dependents
oneOf(db).getMessageDependents(txn2, messageId); oneOf(db).getMessageDependents(txn1, messageId);
will(returnValue(emptyMap())); will(returnValue(emptyMap()));
// Load the second raw message and group // Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn3)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getMessage(txn3, messageId1); oneOf(db).getMessage(txn2, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn3, groupId); oneOf(db).getGroup(txn2, groupId);
will(returnValue(group)); will(returnValue(group));
// Validate the second message: invalid // Validate the second message: invalid
oneOf(validator).validateMessage(message1, group); oneOf(validator).validateMessage(message1, group);
will(throwException(new InvalidMessageException())); will(throwException(new InvalidMessageException()));
// Store the validation result for the second message // Store the validation result for the second message
oneOf(db).transaction(with(false), withDbRunnable(txn4)); oneOf(db).transaction(with(false), withDbRunnable(txn3));
oneOf(db).getMessageState(txn4, messageId1); oneOf(db).getMessageState(txn3, messageId1);
will(returnValue(UNKNOWN)); will(returnValue(UNKNOWN));
oneOf(db).setMessageState(txn4, messageId1, INVALID); oneOf(db).setMessageState(txn3, messageId1, INVALID);
oneOf(db).deleteMessage(txn4, messageId1); oneOf(db).deleteMessage(txn3, messageId1);
oneOf(db).deleteMessageMetadata(txn4, messageId1); oneOf(db).deleteMessageMetadata(txn3, messageId1);
// Recursively invalidate any dependents // Recursively invalidate any dependents
oneOf(db).getMessageDependents(txn4, messageId1); oneOf(db).getMessageDependents(txn3, messageId1);
will(returnValue(emptyMap())); 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(); vm.startService();
} }
@Test @Test
public void testPendingMessagesAreDeliveredAtStartup() throws Exception { public void testPendingMessagesAreDeliveredAtStartup() throws Exception {
Transaction txn = new Transaction(null, true); Transaction txn = new Transaction(null, false);
Transaction txn1 = new Transaction(null, true); Transaction txn1 = new Transaction(null, false);
Transaction txn2 = new Transaction(null, false);
Transaction txn3 = new Transaction(null, false); expectGetMessagesToValidate();
Transaction txn4 = new Transaction(null, true); expectGetPendingMessages(messageId);
context.checking(new DbExpectations() {{ 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 // Check whether the message is ready to deliver
oneOf(db).transaction(with(false), withDbRunnable(txn2)); oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(db).getMessageState(txn2, messageId); oneOf(db).getMessageState(txn, messageId);
will(returnValue(PENDING)); will(returnValue(PENDING));
oneOf(db).getMessageDependencies(txn2, messageId); oneOf(db).getMessageDependencies(txn, messageId);
will(returnValue(singletonMap(messageId1, DELIVERED))); will(returnValue(singletonMap(messageId1, DELIVERED)));
// Get the message and its metadata to deliver // Get the message and its metadata to deliver
oneOf(db).getMessage(txn2, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
oneOf(db).getGroup(txn2, groupId); oneOf(db).getGroup(txn, groupId);
will(returnValue(group)); will(returnValue(group));
oneOf(db).getMessageMetadataForValidator(txn2, messageId); oneOf(db).getMessageMetadataForValidator(txn, messageId);
will(returnValue(new Metadata())); will(returnValue(new Metadata()));
// Deliver the message // Deliver the message
oneOf(hook).incomingMessage(txn2, message, metadata); oneOf(hook).incomingMessage(txn, message, metadata);
will(returnValue(false)); will(returnValue(false));
oneOf(db).setMessageState(txn2, messageId, DELIVERED); oneOf(db).setMessageState(txn, messageId, DELIVERED);
// Get any pending dependents // Get any pending dependents
oneOf(db).getMessageDependents(txn2, messageId); oneOf(db).getMessageDependents(txn, messageId);
will(returnValue(singletonMap(messageId2, PENDING))); will(returnValue(singletonMap(messageId2, PENDING)));
// Check whether the dependent is ready to deliver // Check whether the dependent is ready to deliver
oneOf(db).transaction(with(false), withDbRunnable(txn3)); oneOf(db).transaction(with(false), withDbRunnable(txn1));
oneOf(db).getMessageState(txn3, messageId2); oneOf(db).getMessageState(txn1, messageId2);
will(returnValue(PENDING)); will(returnValue(PENDING));
oneOf(db).getMessageDependencies(txn3, messageId2); oneOf(db).getMessageDependencies(txn1, messageId2);
will(returnValue(singletonMap(messageId1, DELIVERED))); will(returnValue(singletonMap(messageId1, DELIVERED)));
// Get the dependent and its metadata to deliver // Get the dependent and its metadata to deliver
oneOf(db).getMessage(txn3, messageId2); oneOf(db).getMessage(txn1, messageId2);
will(returnValue(message2)); will(returnValue(message2));
oneOf(db).getGroup(txn3, groupId); oneOf(db).getGroup(txn1, groupId);
will(returnValue(group)); will(returnValue(group));
oneOf(db).getMessageMetadataForValidator(txn3, messageId2); oneOf(db).getMessageMetadataForValidator(txn1, messageId2);
will(returnValue(metadata)); will(returnValue(metadata));
// Deliver the dependent // Deliver the dependent
oneOf(hook).incomingMessage(txn3, message2, metadata); oneOf(hook).incomingMessage(txn1, message2, metadata);
will(returnValue(false)); will(returnValue(false));
oneOf(db).setMessageState(txn3, messageId2, DELIVERED); oneOf(db).setMessageState(txn1, messageId2, DELIVERED);
// Get any pending dependents // Get any pending dependents
oneOf(db).getMessageDependents(txn3, messageId2); oneOf(db).getMessageDependents(txn1, messageId2);
will(returnValue(emptyMap())); will(returnValue(emptyMap()));
// Get messages to share
oneOf(db).transactionWithResult(with(true), withDbCallable(txn4));
oneOf(db).getMessagesToShare(txn4);
will(returnValue(emptyList()));
}}); }});
expectGetMessagesToShare();
vm.startService(); vm.startService();
} }
@Test @Test
public void testMessagesAreSharedAtStartup() throws Exception { public void testMessagesAreSharedAtStartup() throws Exception {
Transaction txn = new Transaction(null, true); Transaction txn = new Transaction(null, false);
Transaction txn1 = new Transaction(null, true); Transaction txn1 = new Transaction(null, false);
Transaction txn2 = new Transaction(null, true);
Transaction txn3 = new Transaction(null, false); expectGetMessagesToValidate();
Transaction txn4 = new Transaction(null, false); expectGetPendingMessages();
expectGetMessagesToShare(messageId);
context.checking(new DbExpectations() {{ 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 // Share message and get dependencies
oneOf(db).transaction(with(false), withDbRunnable(txn3)); oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(db).setMessageShared(txn3, messageId); oneOf(db).setMessageShared(txn, messageId);
oneOf(db).getMessageDependencies(txn3, messageId); oneOf(db).getMessageDependencies(txn, messageId);
will(returnValue(singletonMap(messageId2, DELIVERED))); will(returnValue(singletonMap(messageId2, DELIVERED)));
// Share dependency // Share dependency
oneOf(db).transaction(with(false), withDbRunnable(txn4)); oneOf(db).transaction(with(false), withDbRunnable(txn1));
oneOf(db).setMessageShared(txn4, messageId2); oneOf(db).setMessageShared(txn1, messageId2);
oneOf(db).getMessageDependencies(txn4, messageId2); oneOf(db).getMessageDependencies(txn1, messageId2);
will(returnValue(emptyMap())); will(returnValue(emptyMap()));
}}); }});
@@ -318,49 +269,39 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
Transaction txn = new Transaction(null, true); Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true); Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, true); Transaction txn2 = new Transaction(null, false);
Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, true); expectGetMessagesToValidate(messageId, messageId1);
Transaction txn5 = new Transaction(null, true);
context.checking(new DbExpectations() {{ 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! // Load the first raw message - *gasp* it's gone!
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessage(txn1, messageId); oneOf(db).getMessage(txn, messageId);
will(throwException(new NoSuchMessageException())); will(throwException(new NoSuchMessageException()));
// Load the second raw message and group // Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn2, messageId1); oneOf(db).getMessage(txn1, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn2, groupId); oneOf(db).getGroup(txn1, groupId);
will(returnValue(group)); will(returnValue(group));
// Validate the second message: invalid // Validate the second message: invalid
oneOf(validator).validateMessage(message1, group); oneOf(validator).validateMessage(message1, group);
will(throwException(new InvalidMessageException())); will(throwException(new InvalidMessageException()));
// Invalidate the second message // Invalidate the second message
oneOf(db).transaction(with(false), withDbRunnable(txn3)); oneOf(db).transaction(with(false), withDbRunnable(txn2));
oneOf(db).getMessageState(txn3, messageId1); oneOf(db).getMessageState(txn2, messageId1);
will(returnValue(UNKNOWN)); will(returnValue(UNKNOWN));
oneOf(db).setMessageState(txn3, messageId1, INVALID); oneOf(db).setMessageState(txn2, messageId1, INVALID);
oneOf(db).deleteMessage(txn3, messageId1); oneOf(db).deleteMessage(txn2, messageId1);
oneOf(db).deleteMessageMetadata(txn3, messageId1); oneOf(db).deleteMessageMetadata(txn2, messageId1);
// Recursively invalidate dependents // Recursively invalidate dependents
oneOf(db).getMessageDependents(txn3, messageId1); oneOf(db).getMessageDependents(txn2, messageId1);
will(returnValue(emptyMap())); 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(); vm.startService();
} }
@@ -369,52 +310,42 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
Transaction txn = new Transaction(null, true); Transaction txn = new Transaction(null, true);
Transaction txn1 = new Transaction(null, true); Transaction txn1 = new Transaction(null, true);
Transaction txn2 = new Transaction(null, true); Transaction txn2 = new Transaction(null, false);
Transaction txn3 = new Transaction(null, false);
Transaction txn4 = new Transaction(null, true); expectGetMessagesToValidate(messageId, messageId1);
Transaction txn5 = new Transaction(null, true);
context.checking(new DbExpectations() {{ 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 // Load the first raw message
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getMessage(txn1, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
// Load the group - *gasp* it's gone! // Load the group - *gasp* it's gone!
oneOf(db).getGroup(txn1, groupId); oneOf(db).getGroup(txn, groupId);
will(throwException(new NoSuchGroupException())); will(throwException(new NoSuchGroupException()));
// Load the second raw message and group // Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getMessage(txn2, messageId1); oneOf(db).getMessage(txn1, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn2, groupId); oneOf(db).getGroup(txn1, groupId);
will(returnValue(group)); will(returnValue(group));
// Validate the second message: invalid // Validate the second message: invalid
oneOf(validator).validateMessage(message1, group); oneOf(validator).validateMessage(message1, group);
will(throwException(new InvalidMessageException())); will(throwException(new InvalidMessageException()));
// Store the validation result for the second message // Store the validation result for the second message
oneOf(db).transaction(with(false), withDbRunnable(txn3)); oneOf(db).transaction(with(false), withDbRunnable(txn2));
oneOf(db).getMessageState(txn3, messageId1); oneOf(db).getMessageState(txn2, messageId1);
will(returnValue(UNKNOWN)); will(returnValue(UNKNOWN));
oneOf(db).setMessageState(txn3, messageId1, INVALID); oneOf(db).setMessageState(txn2, messageId1, INVALID);
oneOf(db).deleteMessage(txn3, messageId1); oneOf(db).deleteMessage(txn2, messageId1);
oneOf(db).deleteMessageMetadata(txn3, messageId1); oneOf(db).deleteMessageMetadata(txn2, messageId1);
// Recursively invalidate dependents // Recursively invalidate dependents
oneOf(db).getMessageDependents(txn3, messageId1); oneOf(db).getMessageDependents(txn2, messageId1);
will(returnValue(emptyMap())); 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(); vm.startService();
} }
@@ -801,4 +732,35 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
vm.eventOccurred(new MessageAddedEvent(message, contactId)); 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; package org.briarproject.bramble.test;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule; import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule; import org.briarproject.bramble.event.DefaultEventExecutorModule;
import dagger.Module; import dagger.Module;
import dagger.Provides;
@Module(includes = { @Module(includes = {
DefaultBatteryManagerModule.class, DefaultBatteryManagerModule.class,
@@ -13,4 +15,20 @@ import dagger.Module;
TestSecureRandomModule.class TestSecureRandomModule.class
}) })
public class BrambleCoreIntegrationTestModule { 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); localUpdateBody);
will(returnValue(localUpdate)); will(returnValue(localUpdate));
oneOf(clientHelper).addLocalMessage(txn, localUpdate, oneOf(clientHelper).addLocalMessage(txn, localUpdate,
localUpdateMeta, true); localUpdateMeta, true, false);
}}); }});
} }
@@ -172,7 +172,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
localVersionsBody); localVersionsBody);
will(returnValue(localVersions)); will(returnValue(localVersions));
oneOf(db).addLocalMessage(txn, localVersions, new Metadata(), oneOf(db).addLocalMessage(txn, localVersions, new Metadata(),
false); false, false);
// Inform contacts that client versions have changed // Inform contacts that client versions have changed
oneOf(db).getContacts(txn); oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact))); will(returnValue(singletonList(contact)));
@@ -259,7 +259,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalVersionsBody); newLocalVersionsBody);
will(returnValue(newLocalVersions)); will(returnValue(newLocalVersions));
oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(), oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(),
false); false, false);
// Inform contacts that client versions have changed // Inform contacts that client versions have changed
oneOf(db).getContacts(txn); oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact))); will(returnValue(singletonList(contact)));
@@ -284,7 +284,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// No visibilities have changed // No visibilities have changed
}}); }});
@@ -355,7 +355,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalVersionsBody); newLocalVersionsBody);
will(returnValue(newLocalVersions)); will(returnValue(newLocalVersions));
oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(), oneOf(db).addLocalMessage(txn, newLocalVersions, new Metadata(),
false); false, false);
// Inform contacts that client versions have changed // Inform contacts that client versions have changed
oneOf(db).getContacts(txn); oneOf(db).getContacts(txn);
will(returnValue(singletonList(contact))); will(returnValue(singletonList(contact)));
@@ -382,7 +382,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility); oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
}}); }});
@@ -567,7 +567,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
@@ -640,7 +640,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());

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.content.res.AssetManager;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
@@ -21,7 +21,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class AttachmentControllerIntegrationTest { public class AttachmentRetrieverIntegrationTest {
private static final String smallKitten = 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"; "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,15 @@ public class AttachmentControllerIntegrationTest {
); );
private final MessageId msgId = new MessageId(getRandomId()); private final MessageId msgId = new MessageId(getRandomId());
private final AttachmentController controller = private final AttachmentRetriever retriever =
new AttachmentController(null, dimensions); new AttachmentRetriever(null, dimensions);
@Test @Test
public void testSmallJpegImage() throws Exception { public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten); InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth()); assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight()); assertEquals(240, item.getHeight());
@@ -71,7 +71,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten); InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth()); assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight()); assertEquals(2592, item.getHeight());
@@ -87,7 +87,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten); InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth()); assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight()); assertEquals(510, item.getHeight());
@@ -103,7 +103,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif); InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -118,7 +118,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel); InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(64250, item.getWidth()); assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight()); assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -133,7 +133,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash); InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1184, item.getWidth()); assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight()); assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -148,7 +148,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash); InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -163,7 +163,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl); InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(32, item.getWidth()); assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight()); assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -178,7 +178,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError); InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertTrue(item.hasError()); assertTrue(item.hasError());
} }
@@ -187,7 +187,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif"); InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(65535, item.getWidth()); assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight()); assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -202,7 +202,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif"); InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(10000, item.getWidth()); assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight()); assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -217,7 +217,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif"); InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(16384, item.getWidth()); assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight()); assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -232,7 +232,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg"); InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight()); assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth()); assertEquals(dimensions.minWidth, item.getThumbnailWidth());
@@ -247,7 +247,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg"); InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(is); Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true); AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1920, item.getWidth()); assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); 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.BrambleCoreEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.contact.ContactExchangeManager; import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
@@ -161,6 +162,8 @@ public interface AndroidComponent
ViewModelProvider.Factory viewModelFactory(); ViewModelProvider.Factory viewModelFactory();
FeatureFlags featureFlags();
void inject(SignInReminderReceiver briarService); void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService); void inject(BriarService briarService);

View File

@@ -7,6 +7,7 @@ import android.os.StrictMode;
import com.vanniktech.emoji.RecentEmoji; import com.vanniktech.emoji.RecentEmoji;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PublicKey; 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 java.util.Collections.emptyList;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = {ContactExchangeModule.class, ViewModelModule.class}) @Module(includes = {ContactExchangeModule.class, ViewModelModule.class})
public class AppModule { public class AppModule {
@@ -230,4 +232,20 @@ public class AppModule {
lifecycleManager.registerOpenDatabaseHook(recentEmoji); lifecycleManager.registerOpenDatabaseHook(recentEmoji);
return 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.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.logging.CachingLogHandler; import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.reporting.BriarReportPrimer; import org.briarproject.briar.android.reporting.BriarReportPrimer;
@@ -64,6 +65,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
reportDialogClass = DevReportActivity.class, reportDialogClass = DevReportActivity.class,
resDialogOkToast = R.string.dev_report_saved, resDialogOkToast = R.string.dev_report_saved,
deleteOldUnsentReportsOnApplicationStart = false, deleteOldUnsentReportsOnApplicationStart = false,
buildConfigClass = BuildConfig.class,
customReportContent = { customReportContent = {
REPORT_ID, REPORT_ID,
APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME, 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 ? long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L : BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
Long.MAX_VALUE; Long.MAX_VALUE;
/**
* Feature flag for enabling image attachments.
*/
boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = IS_DEBUG_BUILD;
/**
* Feature flag for enabling adding contacts at a distance.
*/
boolean FEATURE_FLAG_REMOTE_CONTACTS = IS_DEBUG_BUILD;
} }

View File

@@ -0,0 +1,187 @@
package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.content.ContentResolver;
import android.net.Uri;
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.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
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;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
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.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;
@ThreadSafe
@NotNullByDefault
class AttachmentCreationTask {
private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName());
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final ContentResolver contentResolver;
private final AttachmentRetriever retriever;
private final GroupId groupId;
private final Collection<Uri> uris;
private final boolean needsSize;
private final MutableLiveData<AttachmentResult> result;
private volatile boolean canceled = false;
AttachmentCreationTask(Executor ioExecutor,
MessagingManager messagingManager, ContentResolver contentResolver,
AttachmentRetriever retriever, GroupId groupId,
Collection<Uri> uris, boolean needsSize) {
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.contentResolver = contentResolver;
this.retriever = retriever;
this.groupId = groupId;
this.uris = uris;
this.needsSize = needsSize;
result = new MutableLiveData<>();
}
LiveData<AttachmentResult> getResult() {
return result;
}
/**
* Cancels the task, asynchronously waits for it to finish, and deletes any
* created attachments.
*/
void cancel() {
canceled = true;
// Observe the task until it finishes (which may already have happened)
result.observeForever(new Observer<AttachmentResult>() {
@Override
public void onChanged(@Nullable AttachmentResult attachmentResult) {
requireNonNull(attachmentResult);
if (attachmentResult.isFinished()) {
deleteUnsentAttachments(attachmentResult.getItemResults());
result.removeObserver(this);
}
}
});
}
/**
* Asynchronously creates and stores the attachments.
*/
void storeAttachments() {
ioExecutor.execute(() -> {
if (LOG.isLoggable(INFO))
LOG.info("Storing " + uris.size() + " attachments");
List<AttachmentItemResult> results = new ArrayList<>();
for (Uri uri : uris) {
if (canceled) break;
results.add(processUri(uri));
result.postValue(new AttachmentResult(new ArrayList<>(results),
false, false));
}
result.postValue(new AttachmentResult(new ArrayList<>(results),
true, !canceled));
});
}
@IoExecutor
private AttachmentItemResult processUri(Uri uri) {
AttachmentHeader header = null;
try {
header = storeAttachment(uri);
Attachment a = retriever.getMessageAttachment(header);
AttachmentItem item =
retriever.getAttachmentItem(header, a, needsSize);
if (item.hasError()) throw new IOException();
if (needsSize) retriever.cachePut(item);
return new AttachmentItemResult(uri, item);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
// If the attachment was already stored, delete it
tryToRemove(header);
canceled = true;
return new AttachmentItemResult(uri, e);
}
}
@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();
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 void tryToRemove(@Nullable AttachmentHeader h) {
try {
if (h != null) messagingManager.removeAttachment(h);
} catch (DbException e1) {
logException(LOG, WARNING, e1);
}
}
private void deleteUnsentAttachments(
Collection<AttachmentItemResult> itemResults) {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
AttachmentItem item = itemResult.getItem();
if (item != null) headers.add(item.getHeader());
}
if (LOG.isLoggable(INFO))
LOG.info("Deleting " + headers.size() + " unsent attachments");
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
}

View File

@@ -0,0 +1,109 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
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.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import static java.util.Collections.emptyList;
@NotNullByDefault
public class AttachmentCreator {
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
@Nullable
private AttachmentCreationTask task;
public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
}
/**
* Starts a background task to create attachments from the given URIs and
* returns a LiveData to monitor the progress of the task.
*/
@UiThread
public LiveData<AttachmentResult> storeAttachments(GroupId groupId,
Collection<Uri> uris) {
if (task != null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(ioExecutor, messagingManager,
app.getContentResolver(), retriever, groupId, uris, needsSize);
task.storeAttachments();
return task.getResult();
}
/**
* 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
public LiveData<AttachmentResult> getLiveAttachments() {
if (task == null) throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return task.getResult();
}
/**
* Returns the headers of any attachments created by
* {@link #storeAttachments(GroupId, Collection)}, unless
* {@link #onAttachmentsSent()} or {@link #cancel()} has been called.
*/
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
if (task == null) return emptyList();
AttachmentResult result = task.getResult().getValue();
if (result == null) return emptyList();
List<AttachmentHeader> headers = new ArrayList<>();
for (AttachmentItemResult itemResult : result.getItemResults()) {
AttachmentItem item = itemResult.getItem();
if (item != null) headers.add(item.getHeader());
}
return headers;
}
/**
* Informs the AttachmentCreator that the attachments created by
* {@link #storeAttachments(GroupId, Collection)} will be sent.
*/
@UiThread
public void onAttachmentsSent() {
task = null; // Prevent cancel() from cancelling the task
}
/**
* Cancels the task started by
* {@link #storeAttachments(GroupId, Collection)}, if any, unless
* {@link #onAttachmentsSent()} has been called.
*/
@UiThread
public void cancel() {
if (task != null) {
task.cancel();
task = null;
}
}
}

View File

@@ -1,11 +1,16 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.attachment;
import android.content.res.Resources; import android.content.res.Resources;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
class AttachmentDimensions { import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentDimensions {
final int defaultSize; final int defaultSize;
final int minWidth, maxWidth; final int minWidth, maxWidth;
@@ -21,7 +26,7 @@ class AttachmentDimensions {
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
} }
static AttachmentDimensions getAttachmentDimensions(Resources res) { public static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize = int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default); res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize( int minWidth = res.getDimensionPixelSize(
@@ -33,7 +38,7 @@ class AttachmentDimensions {
int maxHeight = res.getDimensionPixelSize( int maxHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_height); R.dimen.message_bubble_image_max_height);
return new AttachmentDimensions(defaultSize, minWidth, maxWidth, 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.Parcel;
import android.os.Parcelable; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static java.util.Objects.requireNonNull;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class AttachmentItem implements Parcelable { public class AttachmentItem implements Parcelable {
private final MessageId messageId; private final AttachmentHeader header;
private final int width, height; private final int width, height;
private final String mimeType, extension; private final String extension;
private final int thumbnailWidth, thumbnailHeight; private final int thumbnailWidth, thumbnailHeight;
private final boolean hasError; private final boolean hasError;
private final long instanceId; private final long instanceId;
@@ -37,13 +40,12 @@ public class AttachmentItem implements Parcelable {
private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0); 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, String extension, int thumbnailWidth, int thumbnailHeight,
boolean hasError) { boolean hasError) {
this.messageId = messageId; this.header = header;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.mimeType = mimeType;
this.extension = extension; this.extension = extension;
this.thumbnailWidth = thumbnailWidth; this.thumbnailWidth = thumbnailWidth;
this.thumbnailHeight = thumbnailHeight; this.thumbnailHeight = thumbnailHeight;
@@ -54,19 +56,24 @@ public class AttachmentItem implements Parcelable {
protected AttachmentItem(Parcel in) { protected AttachmentItem(Parcel in) {
byte[] messageIdByte = new byte[MessageId.LENGTH]; byte[] messageIdByte = new byte[MessageId.LENGTH];
in.readByteArray(messageIdByte); in.readByteArray(messageIdByte);
messageId = new MessageId(messageIdByte); MessageId messageId = new MessageId(messageIdByte);
width = in.readInt(); width = in.readInt();
height = in.readInt(); height = in.readInt();
mimeType = in.readString(); String mimeType = requireNonNull(in.readString());
extension = in.readString(); extension = requireNonNull(in.readString());
thumbnailWidth = in.readInt(); thumbnailWidth = in.readInt();
thumbnailHeight = in.readInt(); thumbnailHeight = in.readInt();
hasError = in.readByte() != 0; hasError = in.readByte() != 0;
instanceId = in.readLong(); instanceId = in.readLong();
header = new AttachmentHeader(messageId, mimeType);
}
AttachmentHeader getHeader() {
return header;
} }
public MessageId getMessageId() { public MessageId getMessageId() {
return messageId; return header.getMessageId();
} }
int getWidth() { int getWidth() {
@@ -77,27 +84,27 @@ public class AttachmentItem implements Parcelable {
return height; return height;
} }
String getMimeType() { public String getMimeType() {
return mimeType; return header.getContentType();
} }
String getExtension() { public String getExtension() {
return extension; return extension;
} }
int getThumbnailWidth() { public int getThumbnailWidth() {
return thumbnailWidth; return thumbnailWidth;
} }
int getThumbnailHeight() { public int getThumbnailHeight() {
return thumbnailHeight; return thumbnailHeight;
} }
boolean hasError() { public boolean hasError() {
return hasError; return hasError;
} }
String getTransitionName() { public String getTransitionName() {
return String.valueOf(instanceId); return String.valueOf(instanceId);
} }
@@ -108,10 +115,10 @@ public class AttachmentItem implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(messageId.getBytes()); dest.writeByteArray(header.getMessageId().getBytes());
dest.writeInt(width); dest.writeInt(width);
dest.writeInt(height); dest.writeInt(height);
dest.writeString(mimeType); dest.writeString(header.getContentType());
dest.writeString(extension); dest.writeString(extension);
dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailWidth);
dest.writeInt(thumbnailHeight); 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 Exception exception;
AttachmentItemResult(Uri uri, AttachmentItem item) {
this.uri = uri;
this.item = item;
this.exception = null;
}
AttachmentItemResult(Uri uri, Exception exception) {
this.uri = uri;
this.item = null;
this.exception = exception;
}
public Uri getUri() {
return uri;
}
@Nullable
public AttachmentItem getItem() {
return item;
}
public boolean hasError() {
return item == null;
}
@Nullable
public Exception getException() {
return exception;
}
}

View File

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

View File

@@ -0,0 +1,35 @@
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;
private final boolean success;
AttachmentResult(Collection<AttachmentItemResult> itemResults,
boolean finished, boolean success) {
this.itemResults = itemResults;
this.finished = finished;
this.success = success;
}
public Collection<AttachmentItemResult> getItemResults() {
return itemResults;
}
public boolean isFinished() {
return finished;
}
public boolean isSuccess() {
return success;
}
}

View File

@@ -1,8 +1,9 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options; import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.media.ExifInterface; import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
@@ -13,7 +14,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult; import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
@@ -42,10 +43,11 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault @NotNullByDefault
class AttachmentController { public class AttachmentRetriever {
private static final Logger LOG = private static final Logger LOG =
getLogger(AttachmentController.class.getName()); getLogger(AttachmentRetriever.class.getName());
private static final int READ_LIMIT = 1024 * 8192; private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
@@ -54,10 +56,11 @@ class AttachmentController {
private final int minWidth, maxWidth; private final int minWidth, maxWidth;
private final int minHeight, maxHeight; private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache = private final Map<MessageId, AttachmentItem> attachmentCache =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
AttachmentController(MessagingManager messagingManager, @VisibleForTesting
AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) { AttachmentDimensions dimensions, ImageHelper imageHelper) {
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.imageHelper = imageHelper; this.imageHelper = imageHelper;
@@ -68,7 +71,7 @@ class AttachmentController {
maxHeight = dimensions.maxHeight; maxHeight = dimensions.maxHeight;
} }
AttachmentController(MessagingManager messagingManager, public AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions) { AttachmentDimensions dimensions) {
this(messagingManager, dimensions, new ImageHelper() { this(messagingManager, dimensions, new ImageHelper() {
@Override @Override
@@ -91,17 +94,17 @@ class AttachmentController {
}); });
} }
void put(MessageId messageId, List<AttachmentItem> attachments) { public void cachePut(AttachmentItem item) {
attachmentCache.put(messageId, attachments); attachmentCache.put(item.getMessageId(), item);
} }
@Nullable @Nullable
List<AttachmentItem> get(MessageId messageId) { public AttachmentItem cacheGet(MessageId attachmentId) {
return attachmentCache.get(messageId); return attachmentCache.get(attachmentId);
} }
@DatabaseExecutor @DatabaseExecutor
List<Pair<AttachmentHeader, Attachment>> getMessageAttachments( public List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException { List<AttachmentHeader> headers) throws DbException {
long start = now(); long start = now();
List<Pair<AttachmentHeader, Attachment>> attachments = List<Pair<AttachmentHeader, Attachment>> attachments =
@@ -110,16 +113,20 @@ class AttachmentController {
Attachment a = messagingManager.getAttachment(h.getMessageId()); Attachment a = messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a)); attachments.add(new Pair<>(h, a));
} }
logDuration(LOG, "Loading attachment", start); logDuration(LOG, "Loading attachments", start);
return attachments; return attachments;
} }
Attachment getMessageAttachment(AttachmentHeader h) throws DbException {
return messagingManager.getAttachment(h.getMessageId());
}
/** /**
* Creates {@link AttachmentItem}s from the passed headers and Attachments. * Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p> * <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}. * Note: This closes the {@link Attachment}'s {@link InputStream}.
*/ */
List<AttachmentItem> getAttachmentItems( public List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) { List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1; boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size()); List<AttachmentItem> items = new ArrayList<>(attachments.size());
@@ -137,17 +144,15 @@ class AttachmentController {
*/ */
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a, AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) { boolean needsSize) {
MessageId messageId = h.getMessageId();
if (!needsSize) { if (!needsSize) {
String mimeType = h.getContentType(); String extension =
String extension = imageHelper.getExtensionFromMimeType(mimeType); imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false; boolean hasError = false;
if (extension == null) { if (extension == null) {
extension = ""; extension = "";
hasError = true; hasError = true;
} }
return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0, return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
0, hasError);
} }
Size size = new Size(); Size size = new Size();
@@ -185,10 +190,17 @@ class AttachmentController {
// get file extension // get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType); String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error; 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 = ""; if (extension == null) extension = "";
return new AttachmentItem(messageId, size.width, size.height, return new AttachmentItem(h, size.width, size.height, extension,
size.mimeType, extension, thumbnailSize.width, thumbnailSize.width, thumbnailSize.height, hasError);
thumbnailSize.height, hasError);
} }
/** /**

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; 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.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import java.util.Collection; import java.util.Collection;
@@ -22,6 +24,7 @@ import java.util.Collection;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; 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.bramble.api.contact.PendingContactState.FAILED;
import static org.briarproject.briar.android.contact.add.remote.PendingContactItem.POLL_DURATION_MS; 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 PendingContactListViewModel viewModel;
private PendingContactListAdapter adapter; private PendingContactListAdapter adapter;
private BriarRecyclerView list; private BriarRecyclerView list;
private Snackbar offlineSnackbar;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -58,6 +62,8 @@ public class PendingContactListActivity extends BriarActivity
viewModel.onCreate(); viewModel.onCreate();
viewModel.getPendingContacts() viewModel.getPendingContacts()
.observe(this, this::onPendingContactsChanged); .observe(this, this::onPendingContactsChanged);
viewModel.getHasInternetConnection()
.observe(this, this::onInternetConnectionChanged);
adapter = new PendingContactListAdapter(this, this, adapter = new PendingContactListAdapter(this, this,
PendingContactItem.class); PendingContactItem.class);
@@ -66,6 +72,10 @@ public class PendingContactListActivity extends BriarActivity
list.setLayoutManager(new LinearLayoutManager(this)); list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter); list.setAdapter(adapter);
list.showProgressBar(); list.showProgressBar();
offlineSnackbar = new BriarSnackbarBuilder()
.setBackgroundColor(R.color.briar_red)
.make(list, R.string.offline_state, LENGTH_INDEFINITE);
} }
@Override @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.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault @NotNullByDefault
@@ -48,6 +49,8 @@ public class PendingContactListViewModel extends AndroidViewModel
private final MutableLiveData<Collection<PendingContactItem>> private final MutableLiveData<Collection<PendingContactItem>>
pendingContacts = new MutableLiveData<>(); pendingContacts = new MutableLiveData<>();
private final MutableLiveData<Boolean> hasInternetConnection =
new MutableLiveData<>();
@Inject @Inject
PendingContactListViewModel(Application application, PendingContactListViewModel(Application application,
@@ -88,13 +91,16 @@ public class PendingContactListViewModel extends AndroidViewModel
Collection<Pair<PendingContact, PendingContactState>> pairs = Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts(); contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size()); List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = false;
for (Pair<PendingContact, PendingContactState> pair : pairs) { for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst(); PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();
long lastPoll = rendezvousPoller.getLastPollTime(p.getId()); long lastPoll = rendezvousPoller.getLastPollTime(p.getId());
items.add(new PendingContactItem(p, pair.getSecond(), items.add(new PendingContactItem(p, state, lastPoll));
lastPoll)); online = online || state != OFFLINE;
} }
pendingContacts.postValue(items); pendingContacts.postValue(items);
hasInternetConnection.postValue(online);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, 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); .getColor(status.getContext(), R.color.briar_yellow);
status.setText(R.string.waiting_for_contact_to_come_online); status.setText(R.string.waiting_for_contact_to_come_online);
break; break;
case OFFLINE:
color = ContextCompat
.getColor(status.getContext(), R.color.briar_yellow);
status.setText("");
break;
case CONNECTING: case CONNECTING:
status.setText(R.string.connecting); status.setText(R.string.connecting);
break; break;

View File

@@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@@ -30,11 +29,11 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; 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.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
@@ -46,13 +45,14 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry; import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; 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.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; 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.blog.BlogActivity;
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache; import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
@@ -63,10 +63,9 @@ import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.ImagePreview; import org.briarproject.briar.android.view.ImagePreview;
import org.briarproject.briar.android.view.TextAttachmentController; 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.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.client.ProtocolStateException;
@@ -81,7 +80,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; 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.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
@@ -92,7 +90,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -118,7 +115,6 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; 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_ATTACH_IMAGE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
@@ -128,6 +124,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.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
import static org.briarproject.briar.android.util.UiUtils.observeOnce; 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 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_DISMISSED;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED;
@@ -135,8 +132,8 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, SendListener, implements EventListener, ConversationListener, TextCache,
TextCache, AttachmentCache, AttachImageListener { AttachmentCache, AttachmentListener {
public static final String CONTACT_ID = "briar.CONTACT_ID"; public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -151,10 +148,9 @@ public class ConversationActivity extends BriarActivity
@Inject @Inject
ConnectionRegistry connectionRegistry; ConnectionRegistry connectionRegistry;
@Inject @Inject
@CryptoExecutor
Executor cryptoExecutor;
@Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@Inject
FeatureFlags featureFlags;
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject @Inject
@@ -166,10 +162,6 @@ public class ConversationActivity extends BriarActivity
@Inject @Inject
volatile EventBus eventBus; volatile EventBus eventBus;
@Inject @Inject
volatile SettingsManager settingsManager;
@Inject
volatile PrivateMessageFactory privateMessageFactory;
@Inject
volatile IntroductionManager introductionManager; volatile IntroductionManager introductionManager;
@Inject @Inject
volatile ForumSharingManager forumSharingManager; volatile ForumSharingManager forumSharingManager;
@@ -184,7 +176,7 @@ public class ConversationActivity extends BriarActivity
loadMessages(); loadMessages();
}; };
private AttachmentController attachmentController; private AttachmentRetriever attachmentRetriever;
private ConversationViewModel viewModel; private ConversationViewModel viewModel;
private ConversationVisitor visitor; private ConversationVisitor visitor;
private ConversationAdapter adapter; private ConversationAdapter adapter;
@@ -219,7 +211,7 @@ public class ConversationActivity extends BriarActivity
viewModel = ViewModelProviders.of(this, viewModelFactory) viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ConversationViewModel.class); .get(ConversationViewModel.class);
attachmentController = viewModel.getAttachmentController(); attachmentRetriever = viewModel.getAttachmentRetriever();
setContentView(R.layout.activity_conversation); setContentView(R.layout.activity_conversation);
@@ -242,7 +234,7 @@ public class ConversationActivity extends BriarActivity
requireNonNull(deleted); requireNonNull(deleted);
if (deleted) finish(); if (deleted) finish();
}); });
viewModel.getAddedPrivateMessage().observe(this, viewModel.getAddedPrivateMessage().observeEvent(this,
this::onAddedPrivateMessage); this::onAddedPrivateMessage);
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
@@ -261,13 +253,13 @@ public class ConversationActivity extends BriarActivity
list.getRecyclerView().addOnScrollListener(scrollListener); list.getRecyclerView().addOnScrollListener(scrollListener);
textInputView = findViewById(R.id.text_input_container); textInputView = findViewById(R.id.text_input_container);
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) { if (featureFlags.shouldEnableImageAttachments()) {
ImagePreview imagePreview = findViewById(R.id.imagePreview); ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView, sendController = new TextAttachmentController(textInputView,
imagePreview, this, this); imagePreview, this, viewModel);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> { observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) { if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS // TODO: remove cast when removing feature flag
((TextAttachmentController) sendController) ((TextAttachmentController) sendController)
.setImagesSupported(); .setImagesSupported();
} }
@@ -302,7 +294,7 @@ public class ConversationActivity extends BriarActivity
Snackbar.LENGTH_SHORT) Snackbar.LENGTH_SHORT)
.show(); .show();
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) { } 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); ((TextAttachmentController) sendController).onImageReceived(data);
} }
} }
@@ -451,19 +443,21 @@ public class ConversationActivity extends BriarActivity
if (text == null) { if (text == null) {
LOG.info("Eagerly loading text for latest message"); LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id); text = messagingManager.getMessageText(id);
textCache.put(id, text); textCache.put(id, requireNonNull(text));
} }
} }
// If the message has a single image, load its size - for multiple // If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed // images we use a grid so the size is fixed
if (h.getAttachmentHeaders().size() == 1) { List<AttachmentHeader> headers = h.getAttachmentHeaders();
List<AttachmentItem> items = attachmentController.get(id); if (headers.size() == 1) {
if (items == null) { MessageId attachmentId = headers.get(0).getMessageId();
AttachmentItem item = attachmentRetriever.cacheGet(attachmentId);
if (item == null) {
LOG.info("Eagerly loading image size for latest message"); LOG.info("Eagerly loading image size for latest message");
items = attachmentController.getAttachmentItems( item = attachmentRetriever.getAttachmentItems(
attachmentController.getMessageAttachments( attachmentRetriever.getMessageAttachments(headers))
h.getAttachmentHeaders())); .get(0);
attachmentController.put(id, items); attachmentRetriever.cachePut(item);
} }
} }
} }
@@ -475,8 +469,10 @@ public class ConversationActivity extends BriarActivity
adapter.incrementRevision(); adapter.incrementRevision();
textInputView.setReady(true); textInputView.setReady(true);
// start observing onboarding after enabling // start observing onboarding after enabling
viewModel.showImageOnboarding().observeEvent(this, if (featureFlags.shouldEnableImageAttachments()) {
this::showImageOnboarding); viewModel.showImageOnboarding().observeEvent(this,
this::showImageOnboarding);
}
List<ConversationItem> items = createItems(headers); List<ConversationItem> items = createItems(headers);
adapter.addAll(items); adapter.addAll(items);
list.showData(); list.showData();
@@ -512,7 +508,7 @@ public class ConversationActivity extends BriarActivity
long start = now(); long start = now();
String text = messagingManager.getMessageText(m); String text = messagingManager.getMessageText(m);
logDuration(LOG, "Loading text", start); logDuration(LOG, "Loading text", start);
displayMessageText(m, text); displayMessageText(m, requireNonNull(text));
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
@@ -545,10 +541,12 @@ public class ConversationActivity extends BriarActivity
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
List<Pair<AttachmentHeader, Attachment>> attachments = List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentController.getMessageAttachments(headers); attachmentRetriever.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1 // TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentItem> items = List<AttachmentItem> items =
attachmentController.getAttachmentItems(attachments); attachmentRetriever.getAttachmentItems(attachments);
if (items.size() == 1)
attachmentRetriever.cachePut(items.get(0));
displayMessageAttachments(messageId, items); displayMessageAttachments(messageId, items);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
@@ -559,7 +557,6 @@ public class ConversationActivity extends BriarActivity
private void displayMessageAttachments(MessageId m, private void displayMessageAttachments(MessageId m,
List<AttachmentItem> items) { List<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> { runOnUiThreadUnlessDestroyed(() -> {
attachmentController.put(m, items);
Pair<Integer, ConversationMessageItem> pair = Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m); adapter.getMessageItem(m);
if (pair != null) { if (pair != null) {
@@ -658,12 +655,21 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) { public void onTooManyAttachments() {
if (isNullOrEmpty(text) && imageUris.isEmpty()) 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(); throw new AssertionError();
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
viewModel.sendMessage(text, imageUris, timestamp); viewModel.sendMessage(text, attachmentHeaders, timestamp);
textInputView.clearText(); textInputView.clearText();
} }
@@ -676,7 +682,6 @@ public class ConversationActivity extends BriarActivity
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) { private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
if (h == null) return; if (h == null) return;
addConversationItem(h.accept(visitor)); addConversationItem(h.accept(visitor));
viewModel.onAddedPrivateMessageSeen();
} }
private void askToRemoveContact() { private void askToRemoveContact() {
@@ -726,7 +731,7 @@ public class ConversationActivity extends BriarActivity
} }
private void showImageOnboarding() { private void showImageOnboarding() {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS // TODO: remove cast when removing feature flag
((TextAttachmentController) sendController) ((TextAttachmentController) sendController)
.showImageOnboarding(this, () -> .showImageOnboarding(this, () ->
viewModel.onImageOnboardingSeen()); viewModel.onImageOnboardingSeen());
@@ -903,12 +908,17 @@ public class ConversationActivity extends BriarActivity
@Override @Override
public List<AttachmentItem> getAttachmentItems(MessageId m, public List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers) {
List<AttachmentItem> attachments = attachmentController.get(m); List<AttachmentItem> items = new ArrayList<>(headers.size());
if (attachments == null) { for (AttachmentHeader header : headers) {
loadMessageAttachments(m, headers); AttachmentItem item =
return emptyList(); attachmentRetriever.cacheGet(header.getMessageId());
if (item == null) {
loadMessageAttachments(m, headers);
return emptyList();
}
items.add(item);
} }
return attachments; return items;
} }
} }

View File

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

View File

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

View File

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

View File

@@ -4,42 +4,41 @@ import android.app.Application;
import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.Transformations; import android.arch.lifecycle.Transformations;
import android.content.ContentResolver;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.FormatException; 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.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.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.util.UiUtils;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; 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.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -50,19 +49,20 @@ import javax.inject.Inject;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.AttachmentDimensions.getAttachmentDimensions; import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends AndroidViewModel { public class ConversationViewModel extends AndroidViewModel
implements AttachmentManager {
private static Logger LOG = private static Logger LOG =
getLogger(ConversationViewModel.class.getName()); getLogger(ConversationViewModel.class.getName());
private static final String SHOW_ONBOARDING_IMAGE = private static final String SHOW_ONBOARDING_IMAGE =
"showOnboardingImage"; "showOnboardingImage";
private static final String SHOW_ONBOARDING_INTRODUCTION = private static final String SHOW_ONBOARDING_INTRODUCTION =
@@ -70,14 +70,13 @@ public class ConversationViewModel extends AndroidViewModel {
@DatabaseExecutor @DatabaseExecutor
private final Executor dbExecutor; private final Executor dbExecutor;
@CryptoExecutor
private final Executor cryptoExecutor;
private final TransactionManager db; private final TransactionManager db;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final SettingsManager settingsManager; private final SettingsManager settingsManager;
private final PrivateMessageFactory privateMessageFactory; private final PrivateMessageFactory privateMessageFactory;
private final AttachmentController attachmentController; private final AttachmentRetriever attachmentRetriever;
private final AttachmentCreator attachmentCreator;
@Nullable @Nullable
private ContactId contactId = null; private ContactId contactId = null;
@@ -86,6 +85,7 @@ public class ConversationViewModel extends AndroidViewModel {
Transformations.map(contact, c -> c.getAuthor().getId()); Transformations.map(contact, c -> c.getAuthor().getId());
private final LiveData<String> contactName = private final LiveData<String> contactName =
Transformations.map(contact, UiUtils::getContactDisplayName); Transformations.map(contact, UiUtils::getContactDisplayName);
private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport = private final MutableLiveData<Boolean> imageSupport =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding = private final MutableLiveEvent<Boolean> showImageOnboarding =
@@ -96,31 +96,38 @@ public class ConversationViewModel extends AndroidViewModel {
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted = private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<GroupId> messagingGroupId = private final MutableLiveEvent<PrivateMessageHeader> addedHeader =
new MutableLiveData<>(); new MutableLiveEvent<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
@Inject @Inject
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor, TransactionManager db, @IoExecutor Executor ioExecutor, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager, MessagingManager messagingManager, ContactManager contactManager,
SettingsManager settingsManager, SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) { PrivateMessageFactory privateMessageFactory) {
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.db = db; this.db = db;
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.contactManager = contactManager; this.contactManager = contactManager;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory; this.privateMessageFactory = privateMessageFactory;
this.attachmentController = new AttachmentController(messagingManager, this.attachmentRetriever = new AttachmentRetriever(messagingManager,
getAttachmentDimensions(application.getResources())); getAttachmentDimensions(application.getResources()));
this.attachmentCreator = new AttachmentCreator(getApplication(),
ioExecutor, messagingManager, attachmentRetriever);
messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false); contactDeleted.setValue(false);
} }
@Override
protected void onCleared() {
super.onCleared();
attachmentCreator.cancel();
}
/** /**
* Setting the {@link ContactId} automatically triggers loading of other * Setting the {@link ContactId} automatically triggers loading of other
* data. * data.
@@ -176,25 +183,53 @@ public class ConversationViewModel extends AndroidViewModel {
}); });
} }
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) { @UiThread
if (messagingGroupId.getValue() == null) loadGroupId(); void sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long timestamp) {
// messagingGroupId is loaded with the contact
observeForeverOnce(messagingGroupId, groupId -> { observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return; requireNonNull(groupId);
// calls through to creating and storing the message observeForeverOnce(imageSupport, hasImageSupport -> {
storeAttachments(groupId, text, uris, timestamp); requireNonNull(hasImageSupport);
createMessage(groupId, text, headers, timestamp,
hasImageSupport);
});
}); });
} }
private void loadGroupId() { @Override
if (contactId == null) throw new IllegalStateException(); @UiThread
dbExecutor.execute(() -> { public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
try { boolean restart) {
messagingGroupId.postValue( MutableLiveData<AttachmentResult> delegate = new MutableLiveData<>();
messagingManager.getConversationId(contactId)); // messagingGroupId is loaded with the contact
} catch (DbException e) { observeForeverOnce(messagingGroupId, groupId -> {
logException(LOG, WARNING, e); requireNonNull(groupId);
} LiveData<AttachmentResult> result;
if (restart) result = attachmentCreator.getLiveAttachments();
else result = attachmentCreator.storeAttachments(groupId, uris);
result.observeForever(new Observer<AttachmentResult>() {
@Override
public void onChanged(@Nullable AttachmentResult value) {
requireNonNull(value);
if (value.isFinished()) result.removeObserver(this);
delegate.setValue(value);
}
});
}); });
return delegate;
}
@Override
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
return attachmentCreator.getAttachmentHeadersForSending();
}
@Override
@UiThread
public void cancel() {
attachmentCreator.cancel();
} }
@DatabaseExecutor @DatabaseExecutor
@@ -252,75 +287,26 @@ public class ConversationViewModel extends AndroidViewModel {
}); });
} }
private void storeAttachments(GroupId groupId, @Nullable String text, private void createMessage(GroupId groupId, @Nullable String text,
List<Uri> uris, long timestamp) { List<AttachmentHeader> headers, long timestamp,
dbExecutor.execute(() -> { boolean hasImageSupport) {
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;
try { try {
ContentResolver contentResolver = PrivateMessage pm;
getApplication().getContentResolver(); if (hasImageSupport) {
is = contentResolver.openInputStream(uri); pm = privateMessageFactory.createPrivateMessage(groupId,
if (is == null) throw new IOException(); timestamp, text, headers);
String contentType = contentResolver.getType(uri); } else {
if (contentType == null) throw new IOException("null content type"); pm = privateMessageFactory.createLegacyPrivateMessage(
AttachmentHeader h = messagingManager groupId, timestamp, requireNonNull(text));
.addLocalAttachment(groupId, timestamp, contentType, is); }
is.close(); storeMessage(pm);
// re-open stream to get AttachmentItem } catch (FormatException e) {
is = contentResolver.openInputStream(uri); throw new AssertionError(e);
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);
} }
} }
private void createMessage(GroupId groupId, @Nullable String text, private void storeMessage(PrivateMessage m) {
List<AttachmentHeader> attachments, List<AttachmentItem> aItems, attachmentCreator.onAttachmentsSent();
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) {
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
long start = now(); long start = now();
@@ -330,22 +316,17 @@ public class ConversationViewModel extends AndroidViewModel {
PrivateMessageHeader h = new PrivateMessageHeader( PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(), message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false, message.getTimestamp(), true, true, false, false,
text != null, attachments); m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here // TODO add text to cache when available here
addedHeader.postValue(h); addedHeader.postEvent(h);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
}); });
} }
@UiThread AttachmentRetriever getAttachmentRetriever() {
void onAddedPrivateMessageSeen() { return attachmentRetriever;
addedHeader.setValue(null);
}
AttachmentController getAttachmentController() {
return attachmentController;
} }
LiveData<Contact> getContact() { LiveData<Contact> getContact() {
@@ -380,7 +361,7 @@ public class ConversationViewModel extends AndroidViewModel {
return contactDeleted; return contactDeleted;
} }
LiveData<PrivateMessageHeader> getAddedPrivateMessage() { LiveEvent<PrivateMessageHeader> getAddedPrivateMessage() {
return addedHeader; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; 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.BlogInvitationRequest;
import org.briarproject.briar.api.blog.BlogInvitationResponse; import org.briarproject.briar.api.blog.BlogInvitationResponse;
import org.briarproject.briar.api.conversation.ConversationMessageVisitor; import org.briarproject.briar.api.conversation.ConversationMessageVisitor;

View File

@@ -31,6 +31,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; 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.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.PullDownLayout; 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.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.conversation.glide.Radii; import org.briarproject.briar.android.conversation.glide.Radii;
import java.util.ArrayList; 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.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.conversation.glide.GlideApp; import org.briarproject.briar.android.conversation.glide.GlideApp;
import javax.annotation.ParametersAreNonnullByDefault; 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.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; 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.BriarImageTransformation;
import org.briarproject.briar.android.conversation.glide.GlideApp; import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.conversation.glide.Radii; import org.briarproject.briar.android.conversation.glide.Radii;

View File

@@ -13,6 +13,7 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; 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.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;

View File

@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; 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.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.InputStream; import java.io.InputStream;

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.db.DatabaseExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; 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 org.briarproject.briar.api.messaging.MessagingManager;
import java.util.concurrent.Executor; 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.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.conversation.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import java.io.InputStream; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.conversation.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import java.io.InputStream; 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.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.conversation.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import java.io.InputStream; import java.io.InputStream;

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.introduction; package org.briarproject.briar.android.introduction;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar; 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;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -193,7 +193,8 @@ public class IntroductionMessageFragment extends BaseFragment
} }
@Override @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 // disable button to prevent accidental double invitations
ui.message.setReady(false); 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; package org.briarproject.briar.android.reporting;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar; 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.dialog.BaseCrashReportDialog;
import org.acra.file.CrashReportPersister; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.acra.model.Element; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.logout.HideUiActivity;
import org.briarproject.briar.android.util.UserFeedback; import org.briarproject.briar.android.util.UserFeedback;
import org.json.JSONException;
import java.io.File; 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.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.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.inputmethod.InputMethodManager.SHOW_FORCED; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE; 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; import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
public class DevReportActivity extends BaseCrashReportDialog @MethodsNotNullByDefault
implements CompoundButton.OnCheckedChangeListener { @ParametersNotNullByDefault
public class DevReportActivity extends BaseCrashReportDialog {
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);
}
private AppCompatDelegate delegate; 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() { private AppCompatDelegate getDelegate() {
if (delegate == null) { if (delegate == null) {
@@ -110,68 +61,21 @@ public class DevReportActivity extends BaseCrashReportDialog
} }
@Override @Override
public void init(Bundle state) { public void init(@Nullable Bundle state) {
super.init(state); super.init(state);
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
getDelegate().setContentView(R.layout.activity_dev_report); getDelegate().setContentView(R.layout.activity_dev_report);
Toolbar tb = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
getDelegate().setSupportActionBar(tb); getDelegate().setSupportActionBar(toolbar);
View requestReport = findViewById(R.id.request_report); String title = getString(isFeedback() ? R.string.feedback_title :
View reportForm = findViewById(R.id.report_form); R.string.crash_report_title);
userCommentView = findViewById(R.id.user_comment); requireNonNull(getDelegate().getSupportActionBar()).setTitle(title);
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);
//noinspection ConstantConditions if (state == null) displayFragment(isFeedback());
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);
} }
@Override @Override
@@ -181,47 +85,17 @@ public class DevReportActivity extends BaseCrashReportDialog
} }
@Override @Override
public void onPostCreate(Bundle state) { public void onPostCreate(@Nullable Bundle state) {
super.onPostCreate(state); super.onPostCreate(state);
getDelegate().onPostCreate(state); getDelegate().onPostCreate(state);
} }
@Override
public void onStart() {
super.onStart();
if (chevron.isSelected()) refresh();
}
@Override @Override
protected void onPostResume() { protected void onPostResume() {
super.onPostResume(); super.onPostResume();
getDelegate().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 @Override
public void onTitleChanged(CharSequence title, int color) { public void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color); super.onTitleChanged(title, color);
@@ -234,12 +108,6 @@ public class DevReportActivity extends BaseCrashReportDialog
getDelegate().onConfigurationChanged(newConfig); getDelegate().onConfigurationChanged(newConfig);
} }
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putBoolean(STATE_REVIEWING, reviewing);
}
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
@@ -257,132 +125,51 @@ public class DevReportActivity extends BaseCrashReportDialog
closeReport(); closeReport();
} }
@Override void sendCrashReport(String comment, String email) {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { sendCrash(comment, email);
ReportField field = (ReportField) buttonView.getTag();
if (field != null) {
if (isChecked) excludedFields.remove(field);
else excludedFields.add(field);
}
} }
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
private boolean isFeedback() { private boolean isFeedback() {
return getException() instanceof UserFeedback; return getException() instanceof UserFeedback;
} }
private void refresh() { void displayFragment(boolean showReportForm) {
report.setVisibility(INVISIBLE); Fragment f;
progress.setVisibility(VISIBLE); if (showReportForm) {
report.removeAllViews(); File file =
new AsyncTask<Void, Void, CrashReportData>() { (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() { @Override
userCommentView.setEnabled(false); public void invalidateOptionsMenu() {
userEmailView.setEnabled(false); super.invalidateOptionsMenu();
sendReport.setEnabled(false); getDelegate().invalidateOptionsMenu();
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();
} }
private void closeReport() { void closeReport() {
cancelReports(); 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(); 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; package org.briarproject.briar.android.sharing;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; 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.LargeTextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List; import java.util.List;
@@ -83,7 +83,8 @@ public abstract class BaseMessageFragment extends BaseFragment
} }
@Override @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 // disable button to prevent accidental double actions
sendController.setReady(false); sendController.setReady(false);
message.hideSoftKeyboard(); message.hideSoftKeyboard();

View File

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

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.view; package org.briarproject.briar.android.view;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout; import android.support.constraint.ConstraintLayout;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@@ -10,11 +9,13 @@ import android.view.LayoutInflater;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItemResult;
import java.util.Collection; import java.util.Collection;
import static android.content.Context.LAYOUT_INFLATER_SERVICE; import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.support.v4.content.ContextCompat.getColor; 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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@@ -60,34 +61,28 @@ public class ImagePreview extends ConstraintLayout {
this.listener = listener; this.listener = listener;
} }
void showPreview(Collection<Uri> imageUris) { void showPreview(Collection<ImagePreviewItem> items) {
if (listener == null) throw new IllegalStateException(); if (listener == null) throw new IllegalStateException();
if (imageUris.size() == 1) { if (items.size() == 1) {
LayoutParams params = (LayoutParams) imageList.getLayoutParams(); LayoutParams params = (LayoutParams) imageList.getLayoutParams();
params.width = MATCH_PARENT; params.width = MATCH_PARENT;
imageList.setLayoutParams(params); imageList.setLayoutParams(params);
} }
setVisibility(VISIBLE); 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 adapter =
(ImagePreviewAdapter) imageList.getAdapter(); ((ImagePreviewAdapter) imageList.getAdapter());
requireNonNull(adapter).removeUri(uri); int pos = requireNonNull(adapter).loadItemPreview(result);
if (pos != NO_POSITION) {
imageList.smoothScrollToPosition(pos);
}
} }
interface ImagePreviewListener { 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(); void onCancel();
} }

View File

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

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.android.view;
import android.net.Uri;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.AttachmentItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@NotNullByDefault
class ImagePreviewItem {
private final Uri uri;
@Nullable
private AttachmentItem item;
ImagePreviewItem(Uri uri) {
this.uri = uri;
this.item = null;
}
static List<ImagePreviewItem> fromUris(Collection<Uri> uris) {
List<ImagePreviewItem> items = new ArrayList<>(uris.size());
for (Uri uri : uris) {
items.add(new ImagePreviewItem(uri));
}
return items;
}
public void setItem(AttachmentItem item) {
this.item = item;
}
@Nullable
public AttachmentItem getItem() {
return item;
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof ImagePreviewItem &&
uri.equals(((ImagePreviewItem) o).uri);
}
@Override
public int hashCode() {
return uri.hashCode();
}
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.view; package org.briarproject.briar.android.view;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView.ViewHolder; import android.support.v7.widget.RecyclerView.ViewHolder;
@@ -17,9 +16,9 @@ import com.bumptech.glide.request.target.Target;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.GlideApp; import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER; import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@@ -30,45 +29,47 @@ class ImagePreviewViewHolder extends ViewHolder {
@DrawableRes @DrawableRes
private static final int ERROR_RES = R.drawable.ic_image_broken; private static final int ERROR_RES = R.drawable.ic_image_broken;
private final ImagePreviewListener listener;
private final ImageView imageView; private final ImageView imageView;
private final ProgressBar progressBar; private final ProgressBar progressBar;
ImagePreviewViewHolder(View v, ImagePreviewListener listener) { ImagePreviewViewHolder(View v) {
super(v); super(v);
this.listener = listener;
this.imageView = v.findViewById(R.id.imageView); this.imageView = v.findViewById(R.id.imageView);
this.progressBar = v.findViewById(R.id.progressBar); this.progressBar = v.findViewById(R.id.progressBar);
} }
void bind(Uri uri) { void bind(ImagePreviewItem item) {
GlideApp.with(imageView) if (item.getItem() == null) {
.load(uri) progressBar.setVisibility(VISIBLE);
.diskCacheStrategy(NONE) GlideApp.with(imageView)
.error(ERROR_RES) .clear(imageView);
.downsample(FIT_CENTER) } else {
.transition(withCrossFade()) GlideApp.with(imageView)
.addListener(new RequestListener<Drawable>() { .load(item.getItem())
@Override .diskCacheStrategy(NONE)
public boolean onLoadFailed(@Nullable GlideException e, .error(ERROR_RES)
Object model, Target<Drawable> target, .downsample(FIT_CENTER)
boolean isFirstResource) { .transition(withCrossFade())
progressBar.setVisibility(INVISIBLE); .addListener(new RequestListener<Drawable>() {
listener.onUriError(uri); @Override
return false; public boolean onLoadFailed(@Nullable GlideException e,
} Object model, Target<Drawable> target,
boolean isFirstResource) {
progressBar.setVisibility(INVISIBLE);
return false;
}
@Override @Override
public boolean onResourceReady(Drawable resource, public boolean onResourceReady(Drawable resource,
Object model, Target<Drawable> target, Object model, Target<Drawable> target,
DataSource dataSource, boolean isFirstResource) { DataSource dataSource,
progressBar.setVisibility(INVISIBLE); boolean isFirstResource) {
listener.onPreviewLoaded(); progressBar.setVisibility(INVISIBLE);
return false; return false;
} }
}) })
.into(imageView); .into(imageView);
}
} }
} }

View File

@@ -1,6 +1,9 @@
package org.briarproject.briar.android.view; package org.briarproject.briar.android.view;
import android.app.Activity; import android.app.Activity;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.content.ClipData; import android.content.ClipData;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -15,26 +18,36 @@ import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItemResult;
import org.briarproject.briar.android.attachment.AttachmentManager;
import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.jsoup.UnsupportedMimeTypeException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener;
import static android.arch.lifecycle.Lifecycle.State.DESTROYED;
import static android.content.Intent.ACTION_GET_CONTENT; import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.Intent.ACTION_OPEN_DOCUMENT; import static android.content.Intent.ACTION_OPEN_DOCUMENT;
import static android.content.Intent.CATEGORY_OPENABLE; import static android.content.Intent.CATEGORY_OPENABLE;
import static android.content.Intent.EXTRA_ALLOW_MULTIPLE; import static android.content.Intent.EXTRA_ALLOW_MULTIPLE;
import static android.content.Intent.EXTRA_MIME_TYPES;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.content.ContextCompat.getColor; import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v4.view.AbsSavedState.EMPTY_STATE; import static android.support.v4.view.AbsSavedState.EMPTY_STATE;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED;
@@ -44,19 +57,21 @@ public class TextAttachmentController extends TextSendController
implements ImagePreviewListener { implements ImagePreviewListener {
private final ImagePreview imagePreview; private final ImagePreview imagePreview;
private final AttachImageListener imageListener; private final AttachmentListener attachmentListener;
private final CompositeSendButton sendButton; private final CompositeSendButton sendButton;
private final AttachmentManager attachmentManager;
private CharSequence textHint; private final List<Uri> imageUris = new ArrayList<>();
private List<Uri> imageUris = emptyList(); private final CharSequence textHint;
private int previewsLoaded = 0; private boolean loadingUris = false;
private boolean loadingPreviews = false;
public TextAttachmentController(TextInputView v, ImagePreview imagePreview, public TextAttachmentController(TextInputView v, ImagePreview imagePreview,
SendListener listener, AttachImageListener imageListener) { AttachmentListener attachmentListener,
super(v, listener, false); AttachmentManager attachmentManager) {
this.imageListener = imageListener; super(v, attachmentListener, false);
this.attachmentListener = attachmentListener;
this.imagePreview = imagePreview; this.imagePreview = imagePreview;
this.attachmentManager = attachmentManager;
this.imagePreview.setImagePreviewListener(this); this.imagePreview.setImagePreviewListener(this);
sendButton = (CompositeSendButton) compositeSendButton; sendButton = (CompositeSendButton) compositeSendButton;
@@ -67,10 +82,10 @@ public class TextAttachmentController extends TextSendController
@Override @Override
protected void updateViewState() { protected void updateViewState() {
textInput.setEnabled(ready && !loadingPreviews); textInput.setEnabled(ready && !loadingUris);
boolean sendEnabled = ready && !loadingPreviews && boolean sendEnabled = ready && !loadingUris &&
(!textIsEmpty || canSendEmptyText()); (!textIsEmpty || canSendEmptyText());
if (loadingPreviews) { if (loadingUris) {
sendButton.showProgress(true); sendButton.showProgress(true);
} else if (imageUris.isEmpty()) { } else if (imageUris.isEmpty()) {
sendButton.showProgress(false); sendButton.showProgress(false);
@@ -84,7 +99,9 @@ public class TextAttachmentController extends TextSendController
@Override @Override
public void onSendEvent() { public void onSendEvent() {
if (canSend()) { if (canSend()) {
listener.onSendClick(textInput.getText(), imageUris); if (loadingUris) throw new AssertionError();
listener.onSendClick(textInput.getText(),
attachmentManager.getAttachmentHeadersForSending());
reset(); reset();
} }
} }
@@ -110,36 +127,101 @@ public class TextAttachmentController extends TextSendController
builder.show(); builder.show();
return; return;
} }
Intent intent = new Intent(SDK_INT >= 19 ? Intent intent = getAttachFileIntent();
ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT); if (attachmentListener.getLifecycle().getCurrentState() != DESTROYED) {
intent.addCategory(CATEGORY_OPENABLE); attachmentListener.onAttachImage(intent);
intent.setType("image/*");
if (SDK_INT >= 18) intent.putExtra(EXTRA_ALLOW_MULTIPLE, true);
requireNonNull(imageListener).onAttachImage(intent);
}
public void onImageReceived(@Nullable Intent resultData) {
if (resultData == null) return;
if (resultData.getData() != null) {
imageUris = new ArrayList<>(1);
imageUris.add(resultData.getData());
onNewUris();
} else if (SDK_INT >= 18 && resultData.getClipData() != null) {
ClipData clipData = resultData.getClipData();
imageUris = new ArrayList<>(clipData.getItemCount());
for (int i = 0; i < clipData.getItemCount(); i++) {
imageUris.add(clipData.getItemAt(i).getUri());
}
onNewUris();
} }
} }
private void onNewUris() { private Intent getAttachFileIntent() {
if (imageUris.isEmpty()) return; Intent intent = new Intent(SDK_INT >= 19 ?
loadingPreviews = true; ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT);
intent.setType("image/*");
intent.addCategory(CATEGORY_OPENABLE);
if (SDK_INT >= 19) intent.putExtra(EXTRA_MIME_TYPES, IMAGE_MIME_TYPES);
if (SDK_INT >= 18) intent.putExtra(EXTRA_ALLOW_MULTIPLE, true);
return intent;
}
/**
* This is called with the result Intent
* returned by the Activity started with {@link #getAttachFileIntent()}.
* <p>
* This method must be called at most once per call to
* {@link AttachmentListener#onAttachImage(Intent)}.
* Normally, this is true if called from
* {@link Activity#onActivityResult(int, int, Intent)} since this is called
* at most once per call to
* {@link Activity#startActivityForResult(Intent, int)}.
*/
@SuppressWarnings("JavadocReference")
public void onImageReceived(@Nullable Intent resultData) {
if (resultData == null) return;
if (loadingUris || !imageUris.isEmpty()) throw new AssertionError();
List<Uri> newUris = new ArrayList<>();
if (resultData.getData() != null) {
newUris.add(resultData.getData());
onNewUris(false, newUris);
} else if (SDK_INT >= 18 && resultData.getClipData() != null) {
ClipData clipData = resultData.getClipData();
for (int i = 0; i < clipData.getItemCount(); i++) {
newUris.add(clipData.getItemAt(i).getUri());
}
onNewUris(false, newUris);
}
}
private void onNewUris(boolean restart, List<Uri> newUris) {
if (newUris.isEmpty()) return;
if (loadingUris) throw new AssertionError();
loadingUris = true;
if (newUris.size() > MAX_ATTACHMENTS_PER_MESSAGE) {
newUris = newUris.subList(0, MAX_ATTACHMENTS_PER_MESSAGE);
attachmentListener.onTooManyAttachments();
}
imageUris.addAll(newUris);
updateViewState(); updateViewState();
textInput.setHint(R.string.image_caption_hint); textInput.setHint(R.string.image_caption_hint);
imagePreview.showPreview(imageUris); List<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris);
imagePreview.showPreview(items);
// store attachments and show preview when successful
LiveData<AttachmentResult> result =
attachmentManager.storeAttachments(imageUris, restart);
result.observe(attachmentListener, new Observer<AttachmentResult>() {
@Override
public void onChanged(@Nullable AttachmentResult attachmentResult) {
requireNonNull(attachmentResult);
boolean finished = attachmentResult.isFinished();
boolean success = attachmentResult.isSuccess();
if (finished) {
result.removeObserver(this);
if (!success) return;
}
boolean noError = onNewAttachmentItemResults(
attachmentResult.getItemResults());
if (noError && success) onAllAttachmentsCreated();
}
});
}
private boolean onNewAttachmentItemResults(
Collection<AttachmentItemResult> itemResults) {
if (!loadingUris) throw new AssertionError();
for (AttachmentItemResult result : itemResults) {
if (result.hasError()) {
onError(requireNonNull(result.getException()));
return false;
} else {
imagePreview.loadPreviewImage(result);
}
}
return true;
}
private void onAllAttachmentsCreated() {
if (!loadingUris) throw new AssertionError();
loadingUris = false;
updateViewState();
} }
private void reset() { private void reset() {
@@ -148,10 +230,9 @@ public class TextAttachmentController extends TextSendController
// hide image layout // hide image layout
imagePreview.setVisibility(GONE); imagePreview.setVisibility(GONE);
// reset image URIs // reset image URIs
imageUris = emptyList(); imageUris.clear();
// no preview has been loaded // definitely not loading anymore
previewsLoaded = 0; loadingUris = false;
loadingPreviews = false;
// show the image button again, so images can get attached // show the image button again, so images can get attached
updateViewState(); updateViewState();
} }
@@ -168,45 +249,36 @@ public class TextAttachmentController extends TextSendController
@Nullable @Nullable
public Parcelable onRestoreInstanceState(Parcelable inState) { public Parcelable onRestoreInstanceState(Parcelable inState) {
SavedState state = (SavedState) inState; SavedState state = (SavedState) inState;
imageUris = requireNonNull(state.imageUris); if (!imageUris.isEmpty()) throw new AssertionError();
onNewUris(); if (state.imageUris != null) onNewUris(true, state.imageUris);
return state.getSuperState(); return state.getSuperState();
} }
@Override @UiThread
public void onPreviewLoaded() { private void onError(Exception e) {
previewsLoaded++; String errorMsg;
checkAllPreviewsLoaded(); Context ctx = imagePreview.getContext();
} if (e instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) e).getMimeType();
@Override errorMsg = ctx.getString(
public void onUriError(Uri uri) { R.string.image_attach_error_invalid_mime_type, mimeType);
boolean removed = imageUris.remove(uri); } else if (e instanceof FileTooBigException) {
if (!removed) { int mb = MAX_IMAGE_SIZE / 1024 / 1024;
// we have removed this Uri already, do not remove it again errorMsg = ctx.getString(R.string.image_attach_error_too_big, mb);
return; } else {
errorMsg = ctx.getString(R.string.image_attach_error);
} }
imagePreview.removeUri(uri); Toast.makeText(ctx, errorMsg, LENGTH_LONG).show();
if (imageUris.isEmpty()) onCancel(); onCancel();
Toast.makeText(textInput.getContext(), R.string.image_attach_error,
LENGTH_LONG).show();
checkAllPreviewsLoaded();
} }
@Override @Override
public void onCancel() { public void onCancel() {
textInput.clearText(); textInput.clearText();
attachmentManager.cancel();
reset(); reset();
} }
private void checkAllPreviewsLoaded() {
if (previewsLoaded == imageUris.size()) {
loadingPreviews = false;
// all previews were loaded
updateViewState();
}
}
public void showImageOnboarding(Activity activity, public void showImageOnboarding(Activity activity,
Runnable onOnboardingSeen) { Runnable onOnboardingSeen) {
PromptStateChangeListener listener = (prompt, state) -> { PromptStateChangeListener listener = (prompt, state) -> {
@@ -261,8 +333,11 @@ public class TextAttachmentController extends TextSendController
}; };
} }
public interface AttachImageListener { @UiThread
void onAttachImage(Intent intent); public interface AttachmentListener extends SendListener, LifecycleOwner {
}
void onAttachImage(Intent intent);
void onTooManyAttachments();
}
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.view; package org.briarproject.briar.android.view;
import android.net.Uri;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
@@ -10,6 +9,7 @@ import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.view.EmojiTextInputView.TextInputListener; import org.briarproject.briar.android.view.EmojiTextInputView.TextInputListener;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.List; import java.util.List;
@@ -84,8 +84,9 @@ public class TextSendController implements TextInputListener {
return state; return state;
} }
@UiThread
public interface SendListener { public interface SendListener {
void onSendClick(@Nullable String text, List<Uri> imageUris); void onSendClick(@Nullable String text, List<AttachmentHeader> headers);
} }
} }

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11.99,2C6.47,2 2,6.47 2,12s4.47,10 9.99,10S22,17.53 22,12 17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM16.18,7.76l-1.06,1.06 -1.06,-1.06L13,8.82l1.06,1.06L13,10.94 14.06,12l1.06,-1.06L16.18,12l1.06,-1.06 -1.06,-1.06 1.06,-1.06zM7.82,12l1.06,-1.06L9.94,12 11,10.94 9.94,9.88 11,8.82 9.94,7.76 8.88,8.82 7.82,7.76 6.76,8.82l1.06,1.06 -1.06,1.06zM12,14c-2.33,0 -4.31,1.46 -5.11,3.5h10.22c-0.8,-2.04 -2.78,-3.5 -5.11,-3.5z"/>
</vector>

View File

@@ -1,224 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<android.support.constraint.ConstraintLayout <include
android:id="@+id/report_form" android:id="@+id/appBar"
layout="@layout/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"/>
android:visibility="visible"
tools:context=".android.reporting.DevReportActivity"
tools:visibility="invisible">
<include <FrameLayout
android:id="@+id/appBar" android:id="@+id/fragmentContainer"
layout="@layout/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<android.support.design.widget.TextInputLayout
android:id="@+id/user_comment_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
android:layout_marginTop="@dimen/margin_large"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBar">
<android.support.design.widget.TextInputEditText
android:id="@+id/user_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine|textCapSentences"
android:maxLines="5"
tools:hint="@string/describe_crash"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/user_email_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_comment_layout">
<android.support.design.widget.TextInputEditText
android:id="@+id/user_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional_contact_email"
android:inputType="textEmailAddress"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<CheckBox
android:id="@+id/include_debug_report"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
android:checked="false"
android:text="@string/include_debug_report_crash"
app:layout_constraintBottom_toBottomOf="@+id/chevron"
app:layout_constraintEnd_toStartOf="@+id/chevron"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/chevron"/>
<Button
android:id="@+id/chevron"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_email_layout"/>
<ScrollView
android:id="@+id/report_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/include_debug_report">
<LinearLayout
android:id="@+id/report_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/listitem_height_one_line_avatar"
android:paddingEnd="@dimen/margin_large"
android:paddingStart="@dimen/margin_large"
android:paddingTop="@dimen/margin_small"
android:visibility="gone"
tools:visibility="visible"/>
</ScrollView>
<ProgressBar
android:id="@+id/progress_wheel"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include_debug_report"
tools:visibility="visible"/>
</android.support.constraint.ConstraintLayout>
<android.support.constraint.ConstraintLayout
android:id="@+id/request_report"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"/>
android:padding="@dimen/margin_large"
android:visibility="invisible"
tools:visibility="visible">
<TextView </LinearLayout>
android:id="@+id/crashed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/briar_crashed"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/fault"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:layout_editor_absoluteY="8dp"/>
<TextView
android:id="@+id/fault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/not_your_fault"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/pleaseSend"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/crashed"/>
<TextView
android:id="@+id/pleaseSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/please_send_report"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/encrypted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fault"/>
<TextView
android:id="@+id/encrypted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/report_is_encrypted"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/acceptButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pleaseSend"/>
<Button
android:id="@+id/declineButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/close"
app:layout_constraintBottom_toBottomOf="@+id/acceptButton"
app:layout_constraintEnd_toStartOf="@+id/acceptButton"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/acceptButton"/>
<Button
android:id="@+id/acceptButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:text="@string/send_report"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@+id/declineButton"
app:layout_constraintTop_toBottomOf="@+id/encrypted"/>
</android.support.constraint.ConstraintLayout>
</FrameLayout>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@+id/acceptButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="8dp"
android:src="@drawable/ic_crash"
app:layout_constraintBottom_toTopOf="@+id/crashed"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="128dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/crashed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/briar_crashed"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/fault"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon"
tools:layout_editor_absoluteY="8dp"/>
<TextView
android:id="@+id/fault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/not_your_fault"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/pleaseSend"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/crashed"/>
<TextView
android:id="@+id/pleaseSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/please_send_report"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/encrypted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fault"/>
<TextView
android:id="@+id/encrypted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/report_is_encrypted"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pleaseSend"/>
</android.support.constraint.ConstraintLayout>
</ScrollView>
<Button
android:id="@+id/declineButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/close"
app:layout_constraintBottom_toBottomOf="@+id/acceptButton"
app:layout_constraintEnd_toStartOf="@+id/acceptButton"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/acceptButton"/>
<Button
android:id="@+id/acceptButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/send_report"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@+id/declineButton"
app:layout_constraintTop_toBottomOf="@+id/scrollView"/>
</android.support.constraint.ConstraintLayout>

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputLayout
android:id="@+id/user_comment_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
android:layout_marginTop="@dimen/margin_large"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.design.widget.TextInputEditText
android:id="@+id/user_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine|textCapSentences"
android:maxLines="5"
tools:hint="@string/describe_crash"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/user_email_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
app:hintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_comment_layout">
<android.support.design.widget.TextInputEditText
android:id="@+id/user_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional_contact_email"
android:inputType="textEmailAddress"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<CheckBox
android:id="@+id/include_debug_report"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_large"
android:layout_marginStart="@dimen/margin_large"
android:checked="false"
android:text="@string/include_debug_report_crash"
app:layout_constraintBottom_toBottomOf="@+id/chevron"
app:layout_constraintEnd_toStartOf="@+id/chevron"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/chevron"/>
<Button
android:id="@+id/chevron"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_email_layout"/>
<LinearLayout
android:id="@+id/report_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/listitem_height_one_line_avatar"
android:paddingEnd="@dimen/margin_large"
android:paddingStart="@dimen/margin_large"
android:paddingTop="@dimen/margin_small"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/include_debug_report"
tools:visibility="visible"/>
<ProgressBar
android:id="@+id/progress_wheel"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include_debug_report"
tools:visibility="visible"/>
</android.support.constraint.ConstraintLayout>
</ScrollView>

View File

@@ -129,7 +129,9 @@
<string name="message_hint">Type message</string> <string name="message_hint">Type message</string>
<string name="image_caption_hint">Add a caption (optional)</string> <string name="image_caption_hint">Add a caption (optional)</string>
<string name="image_attach">Attach image</string> <string name="image_attach">Attach image</string>
<string name="image_attach_error">Could not attach image</string> <string name="image_attach_error">Could not attach image(s)</string>
<string name="image_attach_error_too_big">Image too big. Limit is %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Image format unsupported: %s</string>
<string name="set_contact_alias">Change contact name</string> <string name="set_contact_alias">Change contact name</string>
<string name="set_contact_alias_hint">Contact name</string> <string name="set_contact_alias_hint">Contact name</string>
<string name="set_alias_button">Change</string> <string name="set_alias_button">Change</string>
@@ -148,6 +150,7 @@
<string name="dialog_message_no_image_support">Your contact\'s Briar does not yet support image attachments. Once they upgrade you\'ll see a different icon.</string> <string name="dialog_message_no_image_support">Your contact\'s Briar does not yet support image attachments. Once they upgrade you\'ll see a different icon.</string>
<string name="dialog_title_image_support">You can now send images to this contact</string> <string name="dialog_title_image_support">You can now send images to this contact</string>
<string name="dialog_message_image_support">Tap this icon to attach images.</string> <string name="dialog_message_image_support">Tap this icon to attach images.</string>
<string name="messaging_too_many_attachments_toast">Only the first %d images will be sent</string>
<!-- Adding Contacts --> <!-- Adding Contacts -->
@@ -213,6 +216,24 @@
<item quantity="one">New contact added.</item> <item quantity="one">New contact added.</item>
<item quantity="other">%d new contacts added.</item> <item quantity="other">%d new contacts added.</item>
</plurals> </plurals>
<string name="adding_contact_slow_warning">Adding this contact is taking longer than usual</string>
<string name="adding_contact_slow_title">Cannot Connect to Contact</string>
<string name="adding_contact_slow_text">Adding this contact is taking longer than usual.\n\nPlease check that your contact has received your link and added you:</string>
<string name="offline_state">No Internet connection</string>
<string name="duplicate_link_dialog_title">Duplicate Link</string>
<string name="duplicate_link_dialog_text_1">You already have a pending contact with this link: %s</string>
<!-- This is a question asking whether two nicknames refer to the same person -->
<string name="duplicate_link_dialog_text_2">Are %s and %s the same person?</string>
<!-- This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button -->
<string name="same_person_button">Same Person</string>
<!-- This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button -->
<string name="different_person_button">Different Person</string>
<string name="duplicate_link_dialog_text_3">%s and %s sent you the same link.\n\nOne of them may be trying to discover who your contacts are.\n\nDon\'t tell them you received the same link from someone else.</string>
<string name="pending_contact_updated_toast">Pending contact updated</string>
<!-- Introductions --> <!-- Introductions -->

View File

@@ -1,8 +1,8 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.attachment;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult; import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
@@ -19,7 +19,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class AttachmentControllerTest extends BrambleMockTestCase { public class AttachmentRetrieverTest extends BrambleMockTestCase {
private final AttachmentDimensions dimensions = new AttachmentDimensions( private final AttachmentDimensions dimensions = new AttachmentDimensions(
100, 50, 200, 75, 300 100, 50, 200, 75, 300
@@ -32,8 +32,8 @@ public class AttachmentControllerTest extends BrambleMockTestCase {
private final MessagingManager messagingManager = private final MessagingManager messagingManager =
context.mock(MessagingManager.class); context.mock(MessagingManager.class);
private final ImageHelper imageHelper = context.mock(ImageHelper.class); private final ImageHelper imageHelper = context.mock(ImageHelper.class);
private final AttachmentController controller = private final AttachmentRetriever controller =
new AttachmentController( new AttachmentRetriever(
messagingManager, messagingManager,
dimensions, dimensions,
imageHelper imageHelper
@@ -94,23 +94,6 @@ public class AttachmentControllerTest extends BrambleMockTestCase {
assertFalse(item.hasError()); assertFalse(item.hasError());
} }
@Test
public void testImageHealsWrongMimeType() {
AttachmentHeader h = getAttachmentHeader("image/png");
context.checking(new Expectations() {{
oneOf(imageHelper).decodeStream(with(any(InputStream.class)));
will(returnValue(new DecodeResult(160, 240, "image/jpeg")));
oneOf(imageHelper).getExtensionFromMimeType("image/jpeg");
will(returnValue("jpg"));
}});
AttachmentItem item = controller.getAttachmentItem(h, attachment, true);
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test @Test
public void testBigJpegImage() { public void testBigJpegImage() {
String mimeType = "image/jpeg"; String mimeType = "image/jpeg";

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation; package org.briarproject.briar.android.attachment;
import com.bumptech.glide.util.MarkEnforcingInputStream; import com.bumptech.glide.util.MarkEnforcingInputStream;

View File

@@ -4,26 +4,30 @@ import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.PrivateMessage;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public abstract class ThreadedMessage extends PrivateMessage { public abstract class ThreadedMessage {
private final Message message;
@Nullable @Nullable
private final MessageId parent; private final MessageId parent;
private final Author author; private final Author author;
public ThreadedMessage(Message message, @Nullable MessageId parent, public ThreadedMessage(Message message, @Nullable MessageId parent,
Author author) { Author author) {
super(message); this.message = message;
this.parent = parent; this.parent = parent;
this.author = author; this.author = author;
} }
public Message getMessage() {
return message;
}
@Nullable @Nullable
public MessageId getParent() { public MessageId getParent() {
return parent; return parent;

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