Compare commits

..

1 Commits

Author SHA1 Message Date
Torsten Grote
362b3f396f Remove response messages right after storing them 2018-11-27 11:43:54 -02:00
201 changed files with 1871 additions and 6946 deletions

View File

@@ -19,7 +19,9 @@ import javax.inject.Inject;
import static android.content.Intent.ACTION_BATTERY_CHANGED;
import static android.content.Intent.ACTION_POWER_CONNECTED;
import static android.content.Intent.ACTION_POWER_DISCONNECTED;
import static android.os.BatteryManager.EXTRA_PLUGGED;
import static android.os.BatteryManager.BATTERY_STATUS_CHARGING;
import static android.os.BatteryManager.BATTERY_STATUS_FULL;
import static android.os.BatteryManager.EXTRA_STATUS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
@@ -46,8 +48,9 @@ class AndroidBatteryManager implements BatteryManager, Service {
IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
Intent i = appContext.registerReceiver(null, filter);
if (i == null) return false;
int status = i.getIntExtra(EXTRA_PLUGGED, 0);
return status != 0;
int status = i.getIntExtra(EXTRA_STATUS, -1);
return status == BATTERY_STATUS_CHARGING ||
status == BATTERY_STATUS_FULL;
}
@Override

View File

@@ -13,8 +13,6 @@ import java.util.Collection;
import javax.annotation.Nullable;
import static org.briarproject.bramble.api.contact.PendingContact.PendingContactState.FAILED;
@NotNullByDefault
public interface ContactManager {
@@ -54,35 +52,6 @@ public interface ContactManager {
long timestamp, boolean alice, boolean verified, boolean active)
throws DbException;
/**
* Returns the static link that needs to be sent to the contact to be added.
*/
String getRemoteContactLink();
/**
* Returns true if the given link is syntactically valid.
*/
boolean isValidRemoteContactLink(String link);
/**
* Requests a new contact to be added via the given {@code link}.
*
* @param link The link received from the contact we want to add.
* @param alias The alias the user has given this contact.
* @return A PendingContact representing the contact to be added.
*/
PendingContact addRemoteContactRequest(String link, String alias);
/**
* Returns a list of {@link PendingContact}s.
*/
Collection<PendingContact> getPendingContacts();
/**
* Removes a {@link PendingContact} that is in state {@link FAILED}.
*/
void removePendingContact(PendingContact pendingContact);
/**
* Returns the contact with the given ID.
*/

View File

@@ -1,54 +0,0 @@
package org.briarproject.bramble.api.contact;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class PendingContact {
public enum PendingContactState {
WAITING_FOR_CONNECTION,
CONNECTED,
ADDING_CONTACT,
FAILED
}
private final PendingContactId id;
private final String alias;
private final PendingContactState state;
private final long timestamp;
public PendingContact(PendingContactId id, String alias,
PendingContactState state, long timestamp) {
this.id = id;
this.alias = alias;
this.state = state;
this.timestamp = timestamp;
}
public String getAlias() {
return alias;
}
public PendingContactState getState() {
return state;
}
public long getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof PendingContact &&
id.equals(((PendingContact) o).id);
}
}

View File

@@ -1,11 +0,0 @@
package org.briarproject.bramble.api.contact;
import org.briarproject.bramble.api.UniqueId;
public class PendingContactId extends UniqueId {
public PendingContactId(byte[] id) {
super(id);
}
}

View File

@@ -1,34 +0,0 @@
package org.briarproject.bramble.api.contact.event;
import org.briarproject.bramble.api.contact.PendingContact.PendingContactState;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a pending contact's state is changed.
*/
@Immutable
@NotNullByDefault
public class PendingContactStateChangedEvent extends Event {
private final PendingContactId id;
private final PendingContactState state;
public PendingContactStateChangedEvent(PendingContactId id,
PendingContactState state) {
this.id = id;
this.state = state;
}
public PendingContactId getId() {
return id;
}
public PendingContactState getPendingContactState() {
return state;
}
}

View File

@@ -25,7 +25,6 @@ import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -151,8 +150,6 @@ public interface DatabaseComponent {
boolean containsLocalAuthor(Transaction txn, AuthorId local)
throws DbException;
int countFakes(Transaction txn, List<byte[]> ids) throws DbException;
/**
* Deletes the message with the given ID. Unlike
* {@link #removeMessage(Transaction, MessageId)}, the message ID,

View File

@@ -16,7 +16,6 @@ public interface TorConstants {
String PREF_TOR_NETWORK = "network2";
String PREF_TOR_PORT = "port";
String PREF_TOR_MOBILE = "useMobileData";
String PREF_TOR_ONLY_WHEN_CHARGING = "onlyWhenCharging";
int PREF_TOR_NETWORK_AUTOMATIC = 0;
int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1;

View File

@@ -38,13 +38,6 @@ public interface ClientVersioningManager {
Visibility getClientVisibility(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException;
/**
* Returns the minor version of the given client that is supported by the
* given contact, or -1 if the contact does not support the client.
*/
int getClientMinorVersion(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException;
interface ClientVersioningHook {
void onClientVisibilityChanging(Transaction txn, Contact c,

View File

@@ -3,8 +3,6 @@ package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
@@ -21,16 +19,12 @@ import org.briarproject.bramble.api.transport.KeyManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.contact.PendingContact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN;
@@ -42,12 +36,6 @@ import static org.briarproject.bramble.util.StringUtils.toUtf8;
@NotNullByDefault
class ContactManagerImpl implements ContactManager {
private static final int LINK_LENGTH = 64;
private static final String REMOTE_CONTACT_LINK =
"briar://" + getRandomBase32String(LINK_LENGTH);
private static final Pattern LINK_REGEX =
Pattern.compile("(briar://)?([a-z2-7]{" + LINK_LENGTH + "})");
private final DatabaseComponent db;
private final KeyManager keyManager;
private final IdentityManager identityManager;
@@ -96,48 +84,6 @@ class ContactManagerImpl implements ContactManager {
verified, active));
}
@Override
public String getRemoteContactLink() {
// TODO replace with real implementation
return REMOTE_CONTACT_LINK;
}
@SuppressWarnings("SameParameterValue")
private static String getRandomBase32String(int length) {
Random random = new Random();
char[] c = new char[length];
for (int i = 0; i < length; i++) {
int character = random.nextInt(32);
if (character < 26) c[i] = (char) ('a' + character);
else c[i] = (char) ('2' + (character - 26));
}
return new String(c);
}
@Override
public boolean isValidRemoteContactLink(String link) {
return LINK_REGEX.matcher(link).matches();
}
@Override
public PendingContact addRemoteContactRequest(String link, String alias) {
// TODO replace with real implementation
PendingContactId id = new PendingContactId(link.getBytes());
return new PendingContact(id, alias, WAITING_FOR_CONNECTION,
System.currentTimeMillis());
}
@Override
public Collection<PendingContact> getPendingContacts() {
// TODO replace with real implementation
return emptyList();
}
@Override
public void removePendingContact(PendingContact pendingContact) {
// TODO replace with real implementation
}
@Override
public Contact getContact(ContactId c) throws DbException {
return db.transactionWithResult(true, txn -> db.getContact(txn, c));

View File

@@ -28,7 +28,6 @@ import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.TransportKeys;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -189,8 +188,6 @@ interface Database<T> {
boolean containsVisibleMessage(T txn, ContactId c, MessageId m)
throws DbException;
int countFakes(T txn, List<byte[]> ids) throws DbException;
/**
* Returns the number of messages offered by the given contact.
* <p/>

View File

@@ -313,13 +313,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.containsLocalAuthor(txn, local);
}
@Override
public int countFakes(Transaction transaction, List<byte[]> ids)
throws DbException {
T txn = unbox(transaction);
return db.countFakes(txn, ids);
}
@Override
public void deleteMessage(Transaction transaction, MessageId m)
throws DbException {

View File

@@ -1,6 +1,5 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.SecretKey;
@@ -48,7 +47,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@@ -94,8 +92,6 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final int OFFSET_CURR = 0;
private static final int OFFSET_NEXT = 1;
private static final int NUM_FAKES = 32 * 1024;
private static final String CREATE_SETTINGS =
"CREATE TABLE settings"
+ " (namespace _STRING NOT NULL,"
@@ -289,20 +285,6 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE)";
// In H2 the MEMORY keyword creates a persistent table with in-memory
// indexes, which must be dropped and recreated when reopening the database.
// In HyperSQL it creates a persistent table that's entirely cached in
// memory, which is the default. Dropping and recreating indexes is
// unnecessary.
private static final String CREATE_FAKES =
"CREATE MEMORY TABLE fakes (fakeId _HASH NOT NULL)";
private static final String DROP_INDEX_FAKES_BY_FAKE_ID =
"DROP INDEX IF EXISTS fakesByFakeId";
private static final String INDEX_FAKES_BY_FAKE_ID =
"CREATE INDEX fakesByFakeId ON fakes (fakeId)";
private static final String INDEX_CONTACTS_BY_AUTHOR_ID =
"CREATE INDEX IF NOT EXISTS contactsByAuthorId"
+ " ON contacts (authorId)";
@@ -394,16 +376,6 @@ abstract class JdbcDatabase implements Database<Connection> {
txn = startTransaction();
try {
storeLastCompacted(txn);
createInMemoryIndexes(txn);
commitTransaction(txn);
} catch (DbException e) {
abortTransaction(txn);
throw e;
}
} else {
txn = startTransaction();
try {
createInMemoryIndexes(txn);
commitTransaction(txn);
} catch (DbException e) {
abortTransaction(txn);
@@ -489,7 +461,6 @@ abstract class JdbcDatabase implements Database<Connection> {
private void createTables(Connection txn) throws DbException {
Statement s = null;
PreparedStatement ps = null;
try {
s = txn.createStatement();
s.executeUpdate(dbTypes.replaceTypes(CREATE_SETTINGS));
@@ -506,29 +477,9 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(dbTypes.replaceTypes(CREATE_TRANSPORTS));
s.executeUpdate(dbTypes.replaceTypes(CREATE_OUTGOING_KEYS));
s.executeUpdate(dbTypes.replaceTypes(CREATE_INCOMING_KEYS));
s.executeUpdate(dbTypes.replaceTypes(CREATE_FAKES));
s.close();
Random random = new Random();
long start = now();
ps = txn.prepareStatement("INSERT INTO fakes (fakeId) VALUES (?)");
int inserted = 0;
while (inserted < NUM_FAKES) {
int batchSize = Math.min(1024, NUM_FAKES - inserted);
for (int i = 0; i < batchSize; i++) {
byte[] id = new byte[UniqueId.LENGTH];
random.nextBytes(id);
ps.setBytes(1, id);
ps.addBatch();
}
ps.executeBatch();
inserted += batchSize;
}
ps.close();
LOG.info("Inserting " + NUM_FAKES + " fakes took "
+ (now() - start) + " ms");
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@@ -550,21 +501,6 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
private void createInMemoryIndexes(Connection txn) throws DbException {
Statement s = null;
try {
long start = now();
s = txn.createStatement();
s.executeUpdate(dbTypes.replaceTypes(DROP_INDEX_FAKES_BY_FAKE_ID));
s.executeUpdate(dbTypes.replaceTypes(INDEX_FAKES_BY_FAKE_ID));
s.close();
LOG.info("Indexing fakes took " + (now() - start) + " ms");
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public Connection startTransaction() throws DbException {
Connection txn;
@@ -1217,41 +1153,6 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public int countFakes(Connection txn, List<byte[]> ids)
throws DbException {
if (ids.isEmpty()) return 0;
PreparedStatement ps = null;
ResultSet rs = null;
try {
long start = now();
StringBuilder sb = new StringBuilder();
sb.append("SELECT COUNT (fakeId) FROM fakes WHERE fakeId IN (?");
for (int i = 1; i < ids.size(); i++) {
sb.append(",?");
}
sb.append(')');
String sql = sb.toString();
ps = txn.prepareStatement(sql);
for (int i = 0; i < ids.size(); i++) {
ps.setBytes(i + 1, ids.get(i));
}
rs = ps.executeQuery();
if (!rs.next()) throw new DbException();
int count = rs.getInt(1);
if (rs.next()) throw new DbException();
rs.close();
ps.close();
LOG.info("Counting " + ids.size() + " fakes took "
+ (now() - start) + " ms, found " + count);
return count;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public int countOfferedMessages(Connection txn, ContactId c)
throws DbException {

View File

@@ -69,7 +69,6 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_WITH_BRIDGES;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT;
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V2;
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
@@ -649,10 +648,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (s.getNamespace().equals(ID.getString())) {
LOG.info("Tor settings updated");
settings = s.getSettings();
// Works around a bug introduced in Tor 0.3.4.8.
// https://trac.torproject.org/projects/tor/ticket/28027
// Could be replaced with callback.transportDisabled()
// when fixed.
// Works around a bug introduced in Tor 0.3.4.8. Could be
// replaced with callback.transportDisabled() when fixed.
disableNetwork();
updateConnectionStatus(networkManager.getNetworkStatus(),
batteryManager.isCharging());
@@ -688,8 +685,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
int network = settings.getInt(PREF_TOR_NETWORK,
PREF_TOR_NETWORK_AUTOMATIC);
boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE, true);
boolean onlyWhenCharging =
settings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING, false);
boolean bridgesWork = circumventionProvider.doBridgesWork(country);
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
@@ -704,12 +699,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!online) {
LOG.info("Disabling network, device is offline");
enableNetwork(false);
} else if (!charging && onlyWhenCharging) {
LOG.info("Disabling network, device is on battery");
enableNetwork(false);
} else if (network == PREF_TOR_NETWORK_NEVER ||
(!useMobile && !wifi)) {
LOG.info("Disabling network, device is using mobile data");
LOG.info("Disabling network due to setting");
enableNetwork(false);
} else if (automatic && blocked && !bridgesWork) {
LOG.info("Disabling network, country is blocked");

View File

@@ -89,9 +89,14 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
public Visibility getClientVisibility(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException {
try {
LatestUpdates latest = findLatestUpdates(txn, contactId);
if (latest == null || latest.remote == null) return INVISIBLE;
Contact contact = db.getContact(txn, contactId);
Group g = getContactGroup(contact);
// Contact may be in the process of being added or removed, so
// contact group may not exist
if (!db.containsGroup(txn, g.getId())) return INVISIBLE;
LatestUpdates latest = findLatestUpdates(txn, g.getId());
if (latest.local == null) throw new DbException();
if (latest.remote == null) return INVISIBLE;
Update localUpdate = loadUpdate(txn, latest.local.messageId);
Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
Map<ClientMajorVersion, Visibility> visibilities =
@@ -105,24 +110,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
}
}
@Override
public int getClientMinorVersion(Transaction txn, ContactId contactId,
ClientId clientId, int majorVersion) throws DbException {
try {
LatestUpdates latest = findLatestUpdates(txn, contactId);
if (latest == null || latest.remote == null) return -1;
Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
ClientMajorVersion cv =
new ClientMajorVersion(clientId, majorVersion);
for (ClientState remote : remoteUpdate.states) {
if (remote.majorVersion.equals(cv)) return remote.minorVersion;
}
return -1;
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public void createLocalState(Transaction txn) throws DbException {
if (db.containsGroup(txn, localGroup.getId())) return;
@@ -349,17 +336,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
MAJOR_VERSION, c);
}
@Nullable
private LatestUpdates findLatestUpdates(Transaction txn, ContactId c)
throws DbException, FormatException {
Contact contact = db.getContact(txn, c);
Group g = getContactGroup(contact);
// Contact may be in the process of being added or removed, so
// contact group may not exist
if (!db.containsGroup(txn, g.getId())) return null;
return findLatestUpdates(txn, g.getId());
}
private LatestUpdates findLatestUpdates(Transaction txn, GroupId g)
throws DbException, FormatException {
Map<MessageId, BdfDictionary> metadata =

View File

@@ -131,19 +131,6 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(testDir.mkdirs());
}
@Test
public void testFakes() throws Exception {
List<byte[]> ids = new ArrayList<>(1024);
for (int i = 0; i < 1024; i++) {
ids.add(getRandomId());
}
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
assertEquals(0, db.countFakes(txn, ids));
db.commitTransaction(txn);
db.close();
}
@Test
public void testPersistence() throws Exception {
// Store some records

View File

@@ -8,7 +8,6 @@ import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.sync.ClientId;
@@ -44,7 +43,6 @@ import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
@@ -659,327 +657,4 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
c.registerClient(clientId, 123, 234, hook);
assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
}
@Test
public void testReturnsInvisibleIfContactGroupDoesNotExist()
throws Exception {
expectGetContactGroup(false);
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsInvisibleIfNoRemoteUpdateExists() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test(expected = DbException.class)
public void testThrowsExceptionIfNoLocalUpdateExists() throws Exception {
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(singletonMap(remoteUpdateId, remoteUpdateMeta)));
}});
ClientVersioningManagerImpl c = createInstance();
c.getClientVisibility(txn, contact.getId(), clientId, 123);
}
@Test
public void testReturnsInvisibleIfClientNotSupportedLocally()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported remotely but not locally
BdfList localUpdateBody = BdfList.of(new BdfList(), 1L);
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsInvisibleIfClientNotSupportedRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported locally but not remotely
BdfList localUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsVisibleIfClientNotActiveRemotely() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported locally and remotely but not active
BdfList localUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(VISIBLE, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsSharedIfClientActiveRemotely() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported locally and remotely and active
BdfList localUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(SHARED, c.getClientVisibility(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsNegativeIfContactGroupDoesNotExist()
throws Exception {
expectGetContactGroup(false);
ClientVersioningManagerImpl c = createInstance();
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsNegativeIfNoRemoteUpdateExists() throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsNegativeIfClientNotSupportedRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is not supported remotely
BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsMinorVersionIfClientNotActiveRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported remotely but not active
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(234, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
@Test
public void testReturnsMinorVersionIfClientActiveRemotely()
throws Exception {
MessageId localUpdateId = new MessageId(getRandomId());
BdfDictionary localUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, true));
MessageId remoteUpdateId = new MessageId(getRandomId());
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
new BdfEntry(MSG_KEY_LOCAL, false));
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
messageMetadata.put(localUpdateId, localUpdateMeta);
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
// The client is supported remotely and active
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
expectGetContactGroup(true);
context.checking(new Expectations() {{
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody));
}});
ClientVersioningManagerImpl c = createInstance();
assertEquals(234, c.getClientMinorVersion(txn, contact.getId(),
clientId, 123));
}
private void expectGetContactGroup(boolean exists) throws Exception {
context.checking(new Expectations() {{
oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact));
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(db).containsGroup(txn, contactGroup.getId());
will(returnValue(exists));
}});
}
}

View File

@@ -116,14 +116,13 @@ dependencies {
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.google.zxing:core:3.3.3'
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0'
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.12.4'
implementation 'com.vanniktech:emoji-google:0.5.1'
def glideVersion = '4.8.0'
implementation("com.github.bumptech.glide:glide:$glideVersion") {
exclude group: 'com.android.support'
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
}
implementation 'com.github.chrisbanes:PhotoView:2.1.4' // later versions already use androidx
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
@@ -134,7 +133,7 @@ dependencies {
testImplementation project(path: ':bramble-core', configuration: 'testOutput')
testImplementation 'org.robolectric:robolectric:4.0.1'
testImplementation 'org.robolectric:shadows-support-v4:3.3.2'
testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'org.mockito:mockito-core:2.13.0'
testImplementation 'junit:junit:4.12'
testImplementation "org.jmock:jmock:2.8.2"
testImplementation "org.jmock:jmock-junit4:2.8.2"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,279 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.res.AssetManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class AttachmentControllerIntegrationTest {
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";
private static final String originalKitten =
"https://upload.wikimedia.org/wikipedia/commons/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg";
private static final String pngKitten =
"https://upload.wikimedia.org/wikipedia/commons/c/c8/Young_cat.png";
private static final String uberGif =
"https://raw.githubusercontent.com/fuzzdb-project/fuzzdb/master/attack/file-upload/malicious-images/uber.gif";
private static final String lottaPixel =
"https://raw.githubusercontent.com/fuzzdb-project/fuzzdb/master/attack/file-upload/malicious-images/lottapixel.jpg";
private static final String imageIoCrash =
"https://www.landaire.net/img/crasher.png";
private static final String gimpCrash =
"https://gitlab.gnome.org/GNOME/gimp/uploads/75f5b7ed3b09b3f1c13f1f65bffe784f/31153c919d3aa634e8e6cff82219fe7352dd8a37.png";
private static final String optiPngAfl =
"https://sourceforge.net/p/optipng/bugs/64/attachment/test.gif";
private static final String librawError =
"https://www.libraw.org/sites/libraw.org/files/P1010671.JPG";
private final AttachmentDimensions dimensions = new AttachmentDimensions(
100, 50, 200, 75, 300
);
private final MessageId msgId = new MessageId(getRandomId());
private final AttachmentController controller =
new AttachmentController(null, dimensions);
@Test
public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
assertEquals(160, item.getThumbnailWidth());
assertEquals(240, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testBigJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testSmallPngImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(138, item.getThumbnailHeight());
assertEquals("image/png", item.getMimeType());
assertEquals("png", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testUberGif() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testLottaPixels() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testImageIoCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/png", item.getMimeType());
assertEquals("png", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testGimpCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testOptiPngAfl() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testLibrawError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertTrue(item.hasError());
}
@Test
public void testSmallAnimatedGifMaxDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testSmallAnimatedGifHugeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testSmallGifLargeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testHighError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
@Test
public void testWideError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
}
private InputStream getUrlInputStream(String url) throws IOException {
return new URL(url).openStream();
}
private InputStream getAssetInputStream(String name) throws IOException {
AssetManager assets = InstrumentationRegistry.getContext().getAssets();
return assets.open(name);
}
public static byte[] getRandomBytes(int length) {
byte[] b = new byte[length];
new Random().nextBytes(b);
return b;
}
public static byte[] getRandomId() {
return getRandomBytes(UniqueId.LENGTH);
}
}

View File

@@ -1,38 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.Intent;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.briarproject.briar.R;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@RunWith(AndroidJUnit4.class)
public class ConversationActivityNotSignedInTest {
@Rule
public ActivityTestRule<ConversationActivity> testRule =
new ActivityTestRule<>(ConversationActivity.class, false, false);
@Test
public void openWithoutSignedIn() {
Context targetContext = getInstrumentation().getTargetContext();
Intent intent = new Intent(targetContext, ConversationActivity.class);
intent.putExtra(CONTACT_ID, 1);
testRule.launchActivity(intent);
onView(withText(R.string.sign_in_button))
.perform(waitUntilMatches(isDisplayed()));
}
}

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="org.briarproject.briar"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
@@ -18,7 +17,6 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission-sdk-23 android:name="android.permission.USE_BIOMETRIC" />
@@ -30,8 +28,7 @@
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/BriarTheme"
tools:ignore="GoogleAppIndexingWarning">
android:theme="@style/BriarTheme">
<receiver
android:name="org.briarproject.briar.android.login.SignInReminderReceiver"
@@ -116,15 +113,6 @@
/>
</activity>
<activity
android:name=".android.conversation.ImageActivity"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
android:theme="@style/BriarTheme.ActionBarOverlay">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
</activity>
<activity
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
android:label="@string/groups_create_group_title"
@@ -157,7 +145,7 @@
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
</activity>
<activity
<activity
android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity"
android:label="@string/groups_member_list"
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
@@ -400,7 +388,7 @@
<activity
android:name="org.briarproject.briar.android.panic.PanicResponderActivity"
android:noHistory="true"
android:theme="@style/TranslucentTheme">
android:theme="@style/Theme.AppCompat.NoActionBar">
<!-- this can never have launchMode singleTask or singleInstance! -->
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
@@ -410,12 +398,12 @@
<activity
android:name="org.briarproject.briar.android.logout.ExitActivity"
android:theme="@android:style/Theme.NoDisplay">
android:theme="@style/Theme.AppCompat.NoActionBar">
</activity>
<activity
android:name=".android.logout.HideUiActivity"
android:theme="@android:style/Theme.NoDisplay">
android:theme="@style/Theme.AppCompat.NoActionBar">
</activity>
<activity

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
@@ -33,7 +32,7 @@ import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender;
import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager;
@@ -83,8 +82,6 @@ public interface AndroidComponent
@DatabaseExecutor
Executor databaseExecutor();
DatabaseComponent databaseComponent();
MessageTracker messageTracker();
LifecycleManager lifecycleManager();
@@ -172,7 +169,7 @@ public interface AndroidComponent
void inject(NotificationCleanupService notificationCleanupService);
void inject(EmojiTextInputView textInputView);
void inject(TextInputView textInputView);
void inject(BriarModelLoader briarModelLoader);

View File

@@ -1,54 +0,0 @@
package org.briarproject.briar.android;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
class BackgroundMonitor implements ActivityLifecycleCallbacks {
private final AtomicInteger foregroundActivities = new AtomicInteger(0);
boolean isRunningInBackground() {
return foregroundActivities.get() == 0;
}
@Override
public void onActivityCreated(Activity a, @Nullable Bundle state) {
}
@Override
public void onActivityStarted(Activity a) {
foregroundActivities.incrementAndGet();
}
@Override
public void onActivityResumed(Activity a) {
}
@Override
public void onActivityPaused(Activity a) {
}
@Override
public void onActivityStopped(Activity a) {
foregroundActivities.decrementAndGet();
}
@Override
public void onActivitySaveInstanceState(Activity a,
@Nullable Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity a) {
}
}

View File

@@ -16,6 +16,4 @@ public interface BriarApplication {
AndroidComponent getApplicationComponent();
SharedPreferences getDefaultSharedPreferences();
boolean isRunningInBackground();
}

View File

@@ -1,7 +1,5 @@
package org.briarproject.briar.android;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
@@ -32,8 +30,6 @@ import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static org.acra.ReportField.ANDROID_VERSION;
@@ -83,7 +79,6 @@ public class BriarApplicationImpl extends Application
Logger.getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler();
private final BackgroundMonitor backgroundMonitor = new BackgroundMonitor();
private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs;
@@ -120,9 +115,6 @@ public class BriarApplicationImpl extends Application
applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider());
if (SDK_INT < 16)
registerActivityLifecycleCallbacks(backgroundMonitor);
}
protected AndroidComponent createApplicationComponent() {
@@ -181,15 +173,4 @@ public class BriarApplicationImpl extends Application
public SharedPreferences getDefaultSharedPreferences() {
return prefs;
}
@Override
public boolean isRunningInBackground() {
if (SDK_INT >= 16) {
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
} else {
return backgroundMonitor.isRunningInBackground();
}
}
}

View File

@@ -1,5 +1,7 @@
package org.briarproject.briar.android;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -33,6 +35,7 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@@ -71,7 +74,6 @@ public class BriarService extends Service {
@Nullable
private BroadcastReceiver receiver = null;
private BriarApplication app;
@Inject
AndroidNotificationManager notificationManager;
@@ -91,8 +93,8 @@ public class BriarService extends Service {
public void onCreate() {
super.onCreate();
app = (BriarApplication) getApplication();
app.getApplicationComponent().inject(this);
BriarApplication application = (BriarApplication) getApplication();
application.getApplicationComponent().inject(this);
LOG.info("Created");
if (created.getAndSet(true)) {
@@ -218,8 +220,8 @@ public class BriarService extends Service {
public void onLowMemory() {
super.onLowMemory();
LOG.warning("Memory is low");
// If we're not in the foreground, clear the UI to save memory
if (app.isRunningInBackground()) hideUi();
// Clear the UI - this is done in onTrimMemory() if SDK_INT >= 16
if (SDK_INT < 16) hideUi();
}
@Override
@@ -233,16 +235,20 @@ public class BriarService extends Service {
LOG.info("Trim memory: near middle of LRU list");
} else if (level == TRIM_MEMORY_COMPLETE) {
LOG.info("Trim memory: near end of LRU list");
} else if (level == TRIM_MEMORY_RUNNING_MODERATE) {
LOG.info("Trim memory: running moderately low");
} else if (level == TRIM_MEMORY_RUNNING_LOW) {
LOG.info("Trim memory: running low");
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
// This level may be received if SDK_INT < 16, although the
// constant isn't declared until API level 16
LOG.warning("Trim memory: running critically low");
// If we're not in the foreground, clear the UI to save memory
if (app.isRunningInBackground()) hideUi();
} else if (SDK_INT >= 16) {
if (level == TRIM_MEMORY_RUNNING_MODERATE) {
LOG.info("Trim memory: running moderately low");
} else if (level == TRIM_MEMORY_RUNNING_LOW) {
LOG.info("Trim memory: running low");
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
LOG.info("Trim memory: running critically low");
// If we're not in the foreground, clear the UI to save memory
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
if (info.importance != IMPORTANCE_FOREGROUND) hideUi();
} else if (LOG.isLoggable(INFO)) {
LOG.info("Trim memory: unknown level " + level);
}
} else if (LOG.isLoggable(INFO)) {
LOG.info("Trim memory: unknown level " + level);
}

View File

@@ -3,29 +3,22 @@ package org.briarproject.briar.android;
import android.app.NotificationManager;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.fragment.ErrorFragment;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID;
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class StartupFailureActivity extends BaseActivity implements
BaseFragmentListener {
@Override
public void onCreate(@Nullable Bundle state) {
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_fragment_container);
@@ -45,7 +38,7 @@ public class StartupFailureActivity extends BaseActivity implements
// cancel notification
if (notificationId > -1) {
Object o = getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) requireNonNull(o);
NotificationManager nm = (NotificationManager) o;
nm.cancel(notificationId);
}
@@ -73,7 +66,7 @@ public class StartupFailureActivity extends BaseActivity implements
}
@Override
public void runOnDbThread(@NonNull Runnable runnable) {
public void runOnDbThread(Runnable runnable) {
throw new AssertionError("Deprecated and should not be used");
}

View File

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

View File

@@ -15,12 +15,10 @@ import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity;
import org.briarproject.briar.android.blog.RssFeedManageActivity;
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.contact.ContactListFragment;
import org.briarproject.briar.android.contact.ContactModule;
import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.conversation.ImageActivity;
import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity;
import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.forum.ForumListFragment;
@@ -31,6 +29,7 @@ import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
import org.briarproject.briar.android.keyagreement.ContactExchangeErrorFragment;
import org.briarproject.briar.android.keyagreement.IntroFragment;
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
import org.briarproject.briar.android.login.AuthorNameFragment;
@@ -48,6 +47,7 @@ import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupMessageFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupModule;
import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity;
import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment;
@@ -68,8 +68,10 @@ import org.briarproject.briar.android.sharing.ForumInvitationActivity;
import org.briarproject.briar.android.sharing.ForumSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareBlogActivity;
import org.briarproject.briar.android.sharing.ShareBlogFragment;
import org.briarproject.briar.android.sharing.ShareBlogMessageFragment;
import org.briarproject.briar.android.sharing.ShareForumActivity;
import org.briarproject.briar.android.sharing.ShareForumFragment;
import org.briarproject.briar.android.sharing.ShareForumMessageFragment;
import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.test.TestDataActivity;
@@ -108,8 +110,6 @@ public interface ActivityComponent {
void inject(ConversationActivity activity);
void inject(ImageActivity activity);
void inject(ForumInvitationActivity activity);
void inject(BlogInvitationActivity activity);
@@ -179,6 +179,8 @@ public interface ActivityComponent {
void inject(CreateGroupFragment fragment);
void inject(CreateGroupMessageFragment fragment);
void inject(GroupListFragment fragment);
void inject(GroupInviteFragment fragment);
@@ -189,14 +191,20 @@ public interface ActivityComponent {
void inject(FeedFragment fragment);
void inject(IntroFragment fragment);
void inject(KeyAgreementFragment fragment);
void inject(ContactChooserFragment fragment);
void inject(ShareForumFragment fragment);
void inject(ShareForumMessageFragment fragment);
void inject(ShareBlogFragment fragment);
void inject(ShareBlogMessageFragment fragment);
void inject(IntroductionMessageFragment fragment);
void inject(SettingsFragment fragment);
@@ -207,6 +215,4 @@ public interface ActivityComponent {
void inject(AliasDialogFragment aliasDialogFragment);
void inject(ImageFragment imageFragment);
}

View File

@@ -15,8 +15,6 @@ import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.BriarApplication;
@@ -53,8 +51,6 @@ import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOT
* Warning: Some activities don't extend {@link BaseActivity}.
* E.g. {@link DevReportActivity}
*/
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BaseActivity extends AppCompatActivity
implements DestroyableContext, OnTapFilteredListener {
@@ -81,17 +77,6 @@ public abstract class BaseActivity extends AppCompatActivity
@Override
public void onCreate(@Nullable Bundle state) {
// create the ActivityComponent *before* calling super.onCreate()
// because it already attaches fragments which need access
// to the component for their own injection
AndroidComponent applicationComponent =
((BriarApplication) getApplication()).getApplicationComponent();
activityComponent = DaggerActivityComponent.builder()
.androidComponent(applicationComponent)
.activityModule(getActivityModule())
.forumModule(getForumModule())
.build();
injectActivity(activityComponent);
super.onCreate(state);
// WARNING: When removing this or making it possible to turn it off,
@@ -101,6 +86,17 @@ public abstract class BaseActivity extends AppCompatActivity
// unlock screen is shown.
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
AndroidComponent applicationComponent =
((BriarApplication) getApplication()).getApplicationComponent();
activityComponent = DaggerActivityComponent.builder()
.androidComponent(applicationComponent)
.activityModule(getActivityModule())
.forumModule(getForumModule())
.build();
injectActivity(activityComponent);
for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityCreate(this);
}

View File

@@ -1,16 +1,16 @@
package org.briarproject.briar.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.support.annotation.RequiresApi;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.transition.Slide;
import android.transition.Transition;
import android.view.Gravity;
import android.view.Window;
import android.widget.CheckBox;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController;
@@ -33,12 +33,10 @@ import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_UNLOCK;
import static org.briarproject.briar.android.util.UiUtils.excludeSystemUi;
import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@SuppressLint("Registered")
public abstract class BriarActivity extends BaseActivity {
public static final String GROUP_ID = "briar.GROUP_ID";
@@ -62,8 +60,7 @@ public abstract class BriarActivity extends BaseActivity {
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_PASSWORD) {
// The result can be RESULT_CANCELED if there's no account
@@ -92,7 +89,7 @@ public abstract class BriarActivity extends BaseActivity {
} else if (lockManager.isLocked() && !isFinishing()) {
// Also check that the activity isn't finishing already.
// This is possible if we finished in onActivityResult().
// Launching another UnlockActivity would cause a loop.
// Lauching another UnlockActivity would cause a loop.
Intent i = new Intent(this, UnlockActivity.class);
startActivityForResult(i, REQUEST_UNLOCK);
} else if (SDK_INT >= 23) {
@@ -114,32 +111,21 @@ public abstract class BriarActivity extends BaseActivity {
lockManager.onActivityStop();
}
protected boolean signedIn() {
return briarController.accountSignedIn();
}
/**
* Sets the transition animations.
* @param enterTransition used to move views into initial positions
* @param exitTransition used to move views out when starting a <b>new</b> activity.
* @param returnTransition used when window is closing, because the activity is finishing.
*/
@RequiresApi(api = 21)
public void setSceneTransitionAnimation(
@Nullable Transition enterTransition,
@Nullable Transition exitTransition,
@Nullable Transition returnTransition) {
public void setSceneTransitionAnimation() {
if (SDK_INT < 21) return;
// workaround for #1007
if (isSamsung7()) {
return;
}
if (enterTransition != null) excludeSystemUi(enterTransition);
if (exitTransition != null) excludeSystemUi(exitTransition);
if (returnTransition != null) excludeSystemUi(returnTransition);
Transition slide = new Slide(Gravity.RIGHT);
slide.excludeTarget(android.R.id.statusBarBackground, true);
slide.excludeTarget(android.R.id.navigationBarBackground, true);
Window window = getWindow();
window.setEnterTransition(enterTransition);
window.setExitTransition(exitTransition);
window.setReturnTransition(returnTransition);
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
window.setExitTransition(slide);
window.setEnterTransition(slide);
window.setTransitionBackgroundFadeDuration(getResources()
.getInteger(android.R.integer.config_longAnimTime));
}
/**

View File

@@ -14,7 +14,5 @@ public interface RequestCodes {
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
int REQUEST_UNLOCK = 11;
int REQUEST_KEYGUARD_UNLOCK = 12;
int REQUEST_ATTACH_IMAGE = 13;
int REQUEST_SAVE_ATTACHMENT = 14;
}

View File

@@ -24,8 +24,6 @@ import javax.annotation.Nullable;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@@ -37,7 +35,7 @@ abstract class BasePostFragment extends BaseFragment {
static final String POST_ID = "briar.POST_ID";
private static final Logger LOG =
getLogger(BasePostFragment.class.getName());
Logger.getLogger(BasePostFragment.class.getName());
private final Handler handler = new Handler(Looper.getMainLooper());
@@ -54,7 +52,7 @@ abstract class BasePostFragment extends BaseFragment {
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// retrieve MessageId of blog post from arguments
byte[] p = requireNonNull(getArguments()).getByteArray(POST_ID);
byte[] p = getArguments().getByteArray(POST_ID);
if (p == null) throw new IllegalStateException("No post ID in args");
postId = new MessageId(p);
@@ -70,7 +68,6 @@ abstract class BasePostFragment extends BaseFragment {
@Override
public void onAuthorClick(BlogPostItem post) {
if (getContext() == null) return;
Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);

View File

@@ -43,7 +43,6 @@ import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@@ -70,6 +69,7 @@ public class BlogFragment extends BaseFragment
private boolean isMyBlog = false, canDeleteBlog = false;
static BlogFragment newInstance(GroupId groupId) {
BlogFragment f = new BlogFragment();
Bundle bundle = new Bundle();
@@ -79,27 +79,20 @@ public class BlogFragment extends BaseFragment
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogSharingListener(this);
sharingController.setSharingListener(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireNonNull(getArguments());
Bundle args = getArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args");
groupId = new GroupId(b);
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(requireNonNull(getActivity()), this,
getFragmentManager());
adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
list = v.findViewById(R.id.postList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
@@ -109,6 +102,13 @@ public class BlogFragment extends BaseFragment
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogSharingListener(this);
sharingController.setSharingListener(this);
}
@Override
public void onStart() {
super.onStart();
@@ -218,10 +218,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onAuthorClick(BlogPostItem post) {
if (post.getGroupId().equals(groupId) || getContext() == null) {
// We're already there
return;
}
if (post.getGroupId().equals(groupId)) return; // We're already there
Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);

View File

@@ -35,7 +35,6 @@ import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@@ -65,18 +64,13 @@ public class FeedFragment extends BaseFragment implements
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setFeedListener(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(R.string.blogs_button);
getActivity().setTitle(R.string.blogs_button);
View v = inflater.inflate(R.layout.fragment_blog, container, false);
@@ -94,6 +88,12 @@ public class FeedFragment extends BaseFragment implements
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setFeedListener(this);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);

View File

@@ -17,7 +17,6 @@ import org.briarproject.briar.android.controller.handler.UiResultExceptionHandle
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@UiThread
@@ -43,17 +42,13 @@ public class FeedPostFragment extends BasePostFragment {
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireNonNull(getArguments());
Bundle args = getArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args");
blogId = new GroupId(b);
@@ -66,6 +61,11 @@ public class FeedPostFragment extends BasePostFragment {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();

View File

@@ -18,6 +18,7 @@ public class ReblogActivity extends BriarActivity implements
@Override
public void onCreate(Bundle savedInstanceState) {
setSceneTransitionAnimation();
super.onCreate(savedInstanceState);
Intent intent = getIntent();

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.blog;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -19,10 +18,7 @@ import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import java.util.List;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -31,18 +27,18 @@ import static android.view.View.FOCUS_DOWN;
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 org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ReblogFragment extends BaseFragment implements SendListener {
public class ReblogFragment extends BaseFragment implements TextInputListener {
public static final String TAG = ReblogFragment.class.getName();
private ViewHolder ui;
private GroupId blogId;
private MessageId postId;
private BlogPostItem item;
@Inject
@@ -74,22 +70,24 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireNonNull(getArguments());
GroupId blogId =
new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
MessageId postId =
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
Bundle args = getArguments();
blogId = new GroupId(args.getByteArray(GROUP_ID));
postId = new MessageId(args.getByteArray(POST_ID));
View v = inflater.inflate(R.layout.fragment_reblog, container, false);
ui = new ViewHolder(v);
ui.post.setTransitionName(postId);
TextSendController sendController =
new TextSendController(ui.input, this, true);
ui.input.setSendController(sendController);
ui.input.setEnabled(false);
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
ui.input.setSendButtonEnabled(false);
showProgressBar();
return v;
}
@Override
public void onStart() {
super.onStart();
// TODO: Load blog post when fragment is created. #631
feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@@ -104,8 +102,6 @@ public class ReblogFragment extends BaseFragment implements SendListener {
handleDbException(exception);
}
});
return v;
}
private void bindViewHolder() {
@@ -116,14 +112,16 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.post.bindItem(item);
ui.post.hideReblogButton();
ui.input.setEnabled(true);
ui.input.setListener(this);
ui.input.setSendButtonEnabled(true);
ui.scrollView.post(() -> ui.scrollView.fullScroll(FOCUS_DOWN));
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(String text) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
String comment = getComment();
feedController.repeatPost(item, comment,
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
@@ -133,6 +131,12 @@ public class ReblogFragment extends BaseFragment implements SendListener {
finish();
}
@Nullable
private String getComment() {
if (ui.input.getText().length() == 0) return null;
return ui.input.getText().toString();
}
private void showProgressBar() {
ui.progressBar.setVisibility(VISIBLE);
ui.input.setVisibility(GONE);

View File

@@ -1,9 +1,9 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.widget.ProgressBar;
@@ -14,22 +14,19 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -38,13 +35,10 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class WriteBlogPostActivity extends BriarActivity
implements OnEditorActionListener, SendListener {
implements OnEditorActionListener, TextInputListener {
private static final Logger LOG =
Logger.getLogger(WriteBlogPostActivity.class.getName());
@@ -64,8 +58,9 @@ public class WriteBlogPostActivity extends BriarActivity
@Inject
volatile BlogManager blogManager;
@SuppressWarnings("ConstantConditions")
@Override
public void onCreate(@Nullable Bundle state) {
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
@@ -76,10 +71,24 @@ public class WriteBlogPostActivity extends BriarActivity
setContentView(R.layout.activity_write_blog_post);
input = findViewById(R.id.textInput);
TextSendController sendController =
new TextSendController(input, this, false);
input.setSendController(sendController);
input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
input.setSendButtonEnabled(false);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisablePublishButton();
}
});
input.setListener(this);
progressBar = findViewById(R.id.progressBar);
}
@@ -118,15 +127,18 @@ public class WriteBlogPostActivity extends BriarActivity
return true;
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text)) throw new AssertionError();
private void enableOrDisablePublishButton() {
input.setSendButtonEnabled(input.getText().length() > 0);
}
@Override
public void onSendClick(String text) {
// hide publish button, show progress bar
input.hideSoftKeyboard();
input.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
text = StringUtils.truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
storePost(text);
}

View File

@@ -51,13 +51,11 @@ import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static android.support.v4.view.ViewCompat.getTransitionName;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -104,7 +102,8 @@ public class ContactListFragment extends BaseFragment implements EventListener {
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(R.string.contact_list_button);
getActivity().setTitle(R.string.contact_list_button);
View contentView = inflater.inflate(R.layout.list, container, false);
@@ -115,7 +114,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
ContactId contactId = item.getContact().getId();
i.putExtra(CONTACT_ID, contactId.getInt());
if (SDK_INT >= 23 && !isSamsung7()) {
if (SDK_INT >= 23) {
ContactListItemViewHolder holder =
(ContactListItemViewHolder) list
.getRecyclerView()

View File

@@ -25,7 +25,6 @@ import java.util.Collection;
import javax.annotation.Nullable;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.CONTACTS;
import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.getContactsFromIds;
@@ -51,10 +50,10 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = requireNonNull(getArguments());
Bundle args = getArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No GroupId");
groupId = new GroupId(b);
@@ -73,7 +72,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
list.setEmptyImage(R.drawable.ic_empty_state_contact_list);
list.setEmptyText(getString(R.string.no_contacts_selector));
list.setEmptyAction(getString(R.string.no_contacts_selector_action));
adapter = getAdapter(requireNonNull(getContext()), this);
adapter = getAdapter(getContext(), this);
list.setAdapter(adapter);
// restore selected contacts if available

View File

@@ -39,8 +39,8 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme);
BriarActivity a = (BriarActivity) requireNonNull(getActivity());
a.getActivityComponent().inject(this);
if (getActivity() == null) return;
((BriarActivity) getActivity()).getActivityComponent().inject(this);
viewModel = ViewModelProviders.of(getActivity(), viewModelFactory)
.get(ConversationViewModel.class);
}
@@ -48,6 +48,7 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_alias_dialog, container,
false);
@@ -68,5 +69,4 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
return v;
}
}

View File

@@ -1,24 +1,20 @@
package org.briarproject.briar.android.conversation;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable;
import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -46,10 +42,8 @@ class AttachmentController {
private static final Logger LOG =
getLogger(AttachmentController.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
@@ -57,38 +51,18 @@ class AttachmentController {
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
AttachmentController(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) {
AttachmentController(MessagingManager messagingManager, Resources res) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
AttachmentController(MessagingManager messagingManager,
AttachmentDimensions dimensions) {
this(messagingManager, dimensions, new ImageHelper() {
@Override
public DecodeResult decodeStream(InputStream is) {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
});
defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
minWidth = res.getDimensionPixelSize(
R.dimen.message_bubble_image_min_width);
maxWidth = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_width);
minHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_min_height);
maxHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_height);
}
void put(MessageId messageId, List<AttachmentItem> attachments) {
@@ -107,53 +81,31 @@ class AttachmentController {
List<Pair<AttachmentHeader, Attachment>> attachments =
new ArrayList<>(headers.size());
for (AttachmentHeader h : headers) {
Attachment a = messagingManager.getAttachment(h.getMessageId());
Attachment a =
messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a));
}
logDuration(LOG, "Loading attachment", start);
return attachments;
}
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) {
AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
getAttachmentItem(a.getFirst(), a.getSecond());
items.add(item);
}
return items;
}
/**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns.
*/
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) {
private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) {
MessageId messageId = h.getMessageId();
if (!needsSize) {
String mimeType = h.getContentType();
String extension = imageHelper.getExtensionFromMimeType(mimeType);
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0,
0, hasError);
}
Size size = new Size();
InputStream is = new MarkEnforcingInputStream(
new BufferedInputStream(a.getStream()));
is.mark(READ_LIMIT);
InputStream is = a.getStream();
is.mark(Integer.MAX_VALUE);
try {
// use exif to get size
if (h.getContentType().equals("image/jpeg")) {
@@ -166,8 +118,6 @@ class AttachmentController {
// use BitmapFactory to get size
if (size.error) {
is.reset();
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
size = getSizeFromBitmap(is);
}
} catch (IOException e) {
@@ -177,24 +127,19 @@ class AttachmentController {
}
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
Size thumbnailSize = new Size(defaultSize, defaultSize);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
thumbnailSize = getThumbnailSize(size.width, size.height);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (extension == null) extension = "";
return new AttachmentItem(messageId, size.width, size.height,
size.mimeType, extension, thumbnailSize.width,
thumbnailSize.height, hasError);
thumbnailSize.width, thumbnailSize.height, size.error);
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
private static Size getSizeFromExif(InputStream is)
throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
@@ -206,21 +151,24 @@ class AttachmentController {
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
return new Size(height, width);
}
return new Size(width, height, "image/jpeg");
return new Size(width, height);
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
private static Size getSizeFromBitmap(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
if (options.outWidth < 1 || options.outHeight < 1)
return new Size();
return new Size(options.outWidth, options.outHeight);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
private Size getThumbnailSize(int width, int height) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
@@ -236,27 +184,24 @@ class AttachmentController {
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
return new Size(thumbnailWidth, thumbnailHeight);
}
private static class Size {
private final int width;
private final int height;
private final String mimeType;
private final boolean error;
private Size(int width, int height, String mimeType) {
private Size(int width, int height) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
private Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}

View File

@@ -1,39 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.res.Resources;
import android.support.annotation.VisibleForTesting;
import org.briarproject.briar.R;
class AttachmentDimensions {
final int defaultSize;
final int minWidth, maxWidth;
final int minHeight, maxHeight;
@VisibleForTesting
AttachmentDimensions(int defaultSize, int minWidth, int maxWidth,
int minHeight, int maxHeight) {
this.defaultSize = defaultSize;
this.minWidth = minWidth;
this.maxWidth = maxWidth;
this.minHeight = minHeight;
this.maxHeight = maxHeight;
}
static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize(
R.dimen.message_bubble_image_min_width);
int maxWidth = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_width);
int minHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_min_height);
int maxHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_height);
return new AttachmentDimensions(defaultSize, minWidth, maxWidth,
minHeight, minHeight);
}
}

View File

@@ -1,68 +1,27 @@
package org.briarproject.briar.android.conversation;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentItem implements Parcelable {
public class AttachmentItem {
private final MessageId messageId;
private final int width, height;
private final String mimeType, extension;
private final int thumbnailWidth, thumbnailHeight;
private final boolean hasError;
private final long instanceId;
public static final Creator<AttachmentItem> CREATOR =
new Creator<AttachmentItem>() {
@Override
public AttachmentItem createFromParcel(Parcel in) {
return new AttachmentItem(in);
}
@Override
public AttachmentItem[] newArray(int size) {
return new AttachmentItem[size];
}
};
private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0);
AttachmentItem(MessageId messageId, int width, int height, String mimeType,
String extension, int thumbnailWidth, int thumbnailHeight,
boolean hasError) {
AttachmentItem(MessageId messageId, int width, int height,
int thumbnailWidth, int thumbnailHeight, boolean hasError) {
this.messageId = messageId;
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.extension = extension;
this.thumbnailWidth = thumbnailWidth;
this.thumbnailHeight = thumbnailHeight;
this.hasError = hasError;
instanceId = NEXT_INSTANCE_ID.getAndIncrement();
}
protected AttachmentItem(Parcel in) {
byte[] messageIdByte = new byte[MessageId.LENGTH];
in.readByteArray(messageIdByte);
messageId = new MessageId(messageIdByte);
width = in.readInt();
height = in.readInt();
mimeType = in.readString();
extension = in.readString();
thumbnailWidth = in.readInt();
thumbnailHeight = in.readInt();
hasError = in.readByte() != 0;
instanceId = in.readLong();
}
public MessageId getMessageId() {
@@ -77,14 +36,6 @@ public class AttachmentItem implements Parcelable {
return height;
}
String getMimeType() {
return mimeType;
}
String getExtension() {
return extension;
}
int getThumbnailWidth() {
return thumbnailWidth;
}
@@ -97,32 +48,4 @@ public class AttachmentItem implements Parcelable {
return hasError;
}
String getTransitionName() {
return String.valueOf(instanceId);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(messageId.getBytes());
dest.writeInt(width);
dest.writeInt(height);
dest.writeString(mimeType);
dest.writeString(extension);
dest.writeInt(thumbnailWidth);
dest.writeInt(thumbnailHeight);
dest.writeByte((byte) (hasError ? 1 : 0));
dest.writeLong(instanceId);
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof AttachmentItem &&
instanceId == ((AttachmentItem) o).instanceId;
}
}

View File

@@ -1,26 +1,19 @@
package org.briarproject.briar.android.conversation;
import android.annotation.SuppressLint;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.ActionMenuView;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.Toolbar;
import android.transition.Slide;
import android.transition.Transition;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
@@ -30,6 +23,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -46,11 +40,14 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
@@ -61,12 +58,8 @@ import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.ImagePreview;
import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachImageListener;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException;
@@ -81,6 +74,7 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
@@ -102,12 +96,8 @@ import im.delight.android.identicons.IdenticonDrawable;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener;
import static android.arch.lifecycle.Lifecycle.State.STARTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static android.support.v4.view.ViewCompat.setTransitionName;
import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList;
import static java.util.Collections.sort;
@@ -117,14 +107,8 @@ import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
@@ -135,16 +119,15 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, SendListener,
TextCache, AttachmentCache, AttachImageListener {
implements EventListener, ConversationListener, TextInputListener,
TextCache, AttachmentCache {
public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
private static final int TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250;
private static final String SHOW_ONBOARDING_INTRODUCTION =
"showOnboardingIntroduction";
@Inject
AndroidNotificationManager notificationManager;
@@ -153,8 +136,20 @@ public class ConversationActivity extends BriarActivity
@Inject
@CryptoExecutor
Executor cryptoExecutor;
@Inject
ViewModelProvider.Factory viewModelFactory;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private AttachmentController attachmentController;
private ConversationViewModel viewModel;
private ConversationVisitor visitor;
private ConversationAdapter adapter;
private Toolbar toolbar;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private BriarRecyclerView list;
private LinearLayoutManager layoutManager;
private TextInputView textInputView;
// Fields that are accessed from background threads must be volatile
@Inject
@@ -177,39 +172,21 @@ public class ConversationActivity extends BriarActivity
volatile BlogSharingManager blogSharingManager;
@Inject
volatile GroupInvitationManager groupInvitationManager;
@Inject
ViewModelProvider.Factory viewModelFactory;
private volatile ContactId contactId;
@Nullable
private volatile GroupId messagingGroupId;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> {
requireNonNull(name);
loadMessages();
};
private AttachmentController attachmentController;
private ConversationViewModel viewModel;
private ConversationVisitor visitor;
private ConversationAdapter adapter;
private Toolbar toolbar;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private BriarRecyclerView list;
private LinearLayoutManager layoutManager;
private TextInputView textInputView;
private TextSendController sendController;
@Nullable
private Parcelable layoutManagerState;
private volatile ContactId contactId;
@Override
public void onCreate(@Nullable Bundle state) {
if (SDK_INT >= 21) {
// Spurious lint warning - using END causes a crash
@SuppressLint("RtlHardcoded")
Transition slide = new Slide(RIGHT);
slide.setDuration(TRANSITION_DURATION_MS);
setSceneTransitionAnimation(slide, null, slide);
}
setSceneTransitionAnimation();
super.onCreate(state);
Intent i = getIntent();
@@ -219,6 +196,7 @@ public class ConversationActivity extends BriarActivity
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ConversationViewModel.class);
viewModel.setContactId(contactId);
attachmentController = viewModel.getAttachmentController();
setContentView(R.layout.activity_conversation);
@@ -242,8 +220,6 @@ public class ConversationActivity extends BriarActivity
requireNonNull(deleted);
if (deleted) finish();
});
viewModel.getAddedPrivateMessage().observe(this,
this::onAddedPrivateMessage);
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
@@ -258,29 +234,7 @@ public class ConversationActivity extends BriarActivity
list.setEmptyText(getString(R.string.no_private_messages));
textInputView = findViewById(R.id.text_input_container);
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView,
imagePreview, this, this);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
((TextAttachmentController) sendController)
.setImagesSupported();
}
});
} else {
sendController = new TextSendController(textInputView, this, false);
}
textInputView.setSendController(sendController);
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setEnabled(false);
textInputView.addOnKeyboardShownListener(this::scrollToBottom);
}
private void scrollToBottom() {
int items = adapter.getItemCount();
if (items > 0) list.scrollToPosition(items - 1);
textInputView.setListener(this);
}
@Override
@@ -289,8 +243,7 @@ public class ConversationActivity extends BriarActivity
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_INTRODUCTION && result == RESULT_OK) {
@@ -298,9 +251,6 @@ public class ConversationActivity extends BriarActivity
Snackbar.LENGTH_SHORT);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show();
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
((TextAttachmentController) sendController).onImageReceived(data);
}
}
@@ -315,16 +265,6 @@ public class ConversationActivity extends BriarActivity
list.startPeriodicUpdate();
}
@Override
public void onResume() {
super.onResume();
// Trigger loading of contact data, noop if data was loaded already.
//
// We can only start loading data *after* we are sure
// the user has signed in. After sign-in, onCreate() isn't run again.
if (signedIn()) viewModel.setContactId(contactId);
}
@Override
public void onStop() {
super.onStop();
@@ -334,39 +274,16 @@ public class ConversationActivity extends BriarActivity
list.stopPeriodicUpdate();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (layoutManager != null) {
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
layoutManagerState = savedInstanceState.getParcelable("layoutManager");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.conversation_actions, menu);
// enable introduction action if available
observeOnce(viewModel.showIntroductionAction(), this, enable -> {
if (enable != null && enable) {
menu.findItem(R.id.action_introduction).setEnabled(true);
// show introduction onboarding, if needed
observeOnce(viewModel.showIntroductionOnboarding(), this,
this::showIntroductionOnboarding);
}
});
// enable alias action if available
observeOnce(viewModel.getContact(), this, contact ->
menu.findItem(R.id.action_set_alias).setEnabled(true));
enableIntroductionActionIfAvailable(
menu.findItem(R.id.action_introduction));
enableAliasActionIfAvailable(
menu.findItem(R.id.action_set_alias));
return super.onCreateOptionsMenu(menu);
}
@@ -429,10 +346,33 @@ public class ConversationActivity extends BriarActivity
Long.compare(b.getTimestamp(), a.getTimestamp()));
if (!sorted.isEmpty()) {
// If the latest header is a private message, eagerly load
// its size so we can set the scroll position correctly
// its text so we can set the scroll position correctly
ConversationMessageHeader latest = sorted.get(0);
if (latest instanceof PrivateMessageHeader) {
eagerlyLoadMessageSize((PrivateMessageHeader) latest);
MessageId id = latest.getId();
PrivateMessageHeader h = (PrivateMessageHeader) latest;
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info(
"Eagerly loading text of latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, text);
}
}
if (!h.getAttachmentHeaders().isEmpty()) {
List<AttachmentItem> items =
attachmentController.get(id);
if (items == null) {
LOG.info(
"Eagerly loading image size for latest message");
items = attachmentController.getAttachmentItems(
attachmentController
.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentController.put(id, items);
}
}
}
}
displayMessages(revision, sorted);
@@ -444,51 +384,17 @@ public class ConversationActivity extends BriarActivity
});
}
private void eagerlyLoadMessageSize(PrivateMessageHeader h)
throws DbException {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, text);
}
}
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
if (h.getAttachmentHeaders().size() == 1) {
List<AttachmentItem> items = attachmentController.get(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
items = attachmentController.getAttachmentItems(
attachmentController.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentController.put(id, items);
}
}
}
private void displayMessages(int revision,
Collection<ConversationMessageHeader> headers) {
runOnUiThreadUnlessDestroyed(() -> {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
textInputView.setEnabled(true);
// start observing onboarding after enabling (only once, because
// we only update this when an onboarding should be shown)
observeOnce(viewModel.showImageOnboarding(), this,
this::showImageOnboarding);
textInputView.setSendButtonEnabled(true);
List<ConversationItem> items = createItems(headers);
adapter.addAll(items);
list.showData();
if (layoutManagerState == null) {
scrollToBottom();
} else {
// Restore the previous scroll position
layoutManager.onRestoreInstanceState(layoutManagerState);
}
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
} else {
LOG.info("Concurrent update, reloading");
loadMessages();
@@ -529,28 +435,21 @@ public class ConversationActivity extends BriarActivity
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
boolean scroll = shouldScrollWhenUpdatingMessage();
pair.getSecond().setText(text);
boolean bottom = adapter.isScrolledToBottom(layoutManager);
adapter.notifyItemChanged(pair.getFirst());
if (scroll) scrollToBottom();
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1);
}
});
}
// When a message's text or attachments are loaded, scroll to the bottom
// if the conversation is visible and we were previously at the bottom
private boolean shouldScrollWhenUpdatingMessage() {
return getLifecycle().getCurrentState().isAtLeast(STARTED)
&& adapter.isScrolledToBottom(layoutManager);
}
private void loadMessageAttachments(MessageId messageId,
List<AttachmentHeader> headers) {
runOnDbThread(() -> {
try {
List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentController.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1
// TODO move getting the items off to the IoExecutor
List<AttachmentItem> items =
attachmentController.getAttachmentItems(attachments);
displayMessageAttachments(messageId, items);
@@ -567,10 +466,10 @@ public class ConversationActivity extends BriarActivity
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
boolean scroll = shouldScrollWhenUpdatingMessage();
pair.getSecond().setAttachments(items);
boolean bottom = adapter.isScrolledToBottom(layoutManager);
adapter.notifyItemChanged(pair.getFirst());
if (scroll) scrollToBottom();
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1);
}
});
}
@@ -619,13 +518,10 @@ public class ConversationActivity extends BriarActivity
private void addConversationItem(ConversationItem item) {
runOnUiThreadUnlessDestroyed(() -> {
boolean bottom = adapter.isScrolledToBottom(layoutManager);
adapter.incrementRevision();
adapter.add(item);
// When adding a new message, scroll to the bottom if the
// conversation is visible, even if we're not currently at
// the bottom
if (getLifecycle().getCurrentState().isAtLeast(STARTED))
scrollToBottom();
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1);
});
}
@@ -661,18 +557,14 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onAttachImage(Intent intent) {
startActivityForResult(intent, REQUEST_ATTACH_IMAGE);
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text) && imageUris.isEmpty())
throw new AssertionError();
public void onSendClick(String text) {
if (text.isEmpty()) return;
text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
viewModel.sendMessage(text, imageUris, timestamp);
textInputView.clearText();
if (messagingGroupId == null) loadGroupId(text, timestamp);
else createMessage(text, timestamp);
textInputView.setText("");
}
private long getMinTimestampForNewMessage() {
@@ -681,10 +573,48 @@ public class ConversationActivity extends BriarActivity
return item == null ? 0 : item.getTime() + 1;
}
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
if (h == null) return;
addConversationItem(h.accept(visitor));
viewModel.onAddedPrivateMessageSeen();
private void loadGroupId(String text, long timestamp) {
runOnDbThread(() -> {
try {
messagingGroupId =
messagingManager.getConversationId(contactId);
createMessage(text, timestamp);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void createMessage(String text, long timestamp) {
cryptoExecutor.execute(() -> {
try {
//noinspection ConstantConditions init in loadGroupId()
storeMessage(privateMessageFactory.createPrivateMessage(
messagingGroupId, timestamp, text, emptyList()), text);
} catch (FormatException e) {
throw new RuntimeException(e);
}
});
}
private void storeMessage(PrivateMessage m, String text) {
runOnDbThread(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, false, false, false,
true, emptyList());
textCache.put(message.getId(), text);
addConversationItem(h.accept(visitor));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void askToRemoveContact() {
@@ -721,70 +651,74 @@ public class ConversationActivity extends BriarActivity
});
}
private void showImageOnboarding(@Nullable Boolean show) {
if (show == null || !show) return;
if (SDK_INT >= 21) {
// show onboarding only after the enter transition has ended
// otherwise the tap target animation won't play
textInputView.postDelayed(this::showImageOnboarding,
TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS);
} else {
showImageOnboarding();
}
private void enableIntroductionActionIfAvailable(MenuItem item) {
runOnDbThread(() -> {
try {
if (contactManager.getActiveContacts().size() > 1) {
enableIntroductionAction(item);
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
if (settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION,
true)) {
showIntroductionOnboarding();
}
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void showImageOnboarding() {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
((TextAttachmentController) sendController)
.showImageOnboarding(this, () ->
viewModel.onImageOnboardingSeen());
private void enableAliasActionIfAvailable(MenuItem item) {
observeOnce(viewModel.getContact(), this, c -> item.setEnabled(true));
}
private void showIntroductionOnboarding(@Nullable Boolean show) {
if (show == null || !show) return;
if (SDK_INT >= 21) {
// show onboarding only after the enter transition has ended
// otherwise the tap target animation won't play
textInputView.postDelayed(this::showIntroductionOnboarding,
TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS);
} else {
showIntroductionOnboarding();
}
private void enableIntroductionAction(MenuItem item) {
runOnUiThreadUnlessDestroyed(() -> item.setEnabled(true));
}
private void showIntroductionOnboarding() {
// find view of overflow icon
View target = null;
for (int i = 0; i < toolbar.getChildCount(); i++) {
if (toolbar.getChildAt(i) instanceof ActionMenuView) {
ActionMenuView menu = (ActionMenuView) toolbar.getChildAt(i);
// The overflow icon should be the last child of the menu
target = menu.getChildAt(menu.getChildCount() - 1);
// If the menu hasn't been populated yet, use the menu itself
// as the target
if (target == null) target = menu;
break;
runOnUiThreadUnlessDestroyed(() -> {
// find view of overflow icon
View target = null;
for (int i = 0; i < toolbar.getChildCount(); i++) {
if (toolbar.getChildAt(i) instanceof ActionMenuView) {
ActionMenuView menu =
(ActionMenuView) toolbar.getChildAt(i);
target = menu.getChildAt(menu.getChildCount() - 1);
break;
}
}
if (target == null) {
LOG.warning("No Overflow Icon found!");
return;
}
}
if (target == null) {
LOG.warning("No Overflow Icon found!");
return;
}
PromptStateChangeListener listener = (prompt, state) -> {
if (state == STATE_DISMISSED || state == STATE_FINISHED) {
viewModel.onIntroductionOnboardingSeen();
PromptStateChangeListener listener = (prompt, state) -> {
if (state == STATE_DISMISSED || state == STATE_FINISHED) {
introductionOnboardingSeen();
}
};
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setPromptStateChangeListener(listener)
.show();
});
}
private void introductionOnboardingSeen() {
runOnDbThread(() -> {
try {
Settings settings = new Settings();
settings.putBoolean(SHOW_ONBOARDING_INTRODUCTION, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
};
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setBackgroundColour(
ContextCompat.getColor(this, R.color.briar_primary))
.setPromptStateChangeListener(listener)
.show();
});
}
@Override
@@ -868,30 +802,6 @@ public class ConversationActivity extends BriarActivity
startActivity(i);
}
@Override
public void onAttachmentClicked(View view,
ConversationMessageItem messageItem, AttachmentItem item) {
String name;
if (messageItem.isIncoming()) {
// must be available when items are being displayed
name = viewModel.getContactDisplayName().getValue();
} else {
name = getString(R.string.you);
}
ArrayList<AttachmentItem> attachments =
new ArrayList<>(messageItem.getAttachments());
Intent i = new Intent(this, ImageActivity.class);
i.putParcelableArrayListExtra(ATTACHMENTS, attachments);
i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item));
i.putExtra(NAME, name);
i.putExtra(DATE, messageItem.getTime());
// restoring list position should not trigger android bug #224270
String transitionName = item.getTransitionName();
ActivityOptionsCompat options =
makeSceneTransitionAnimation(this, view, transitionName);
ActivityCompat.startActivity(this, i, options.toBundle());
}
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException {
@@ -935,5 +845,4 @@ public class ConversationActivity extends BriarActivity
}
return attachments;
}
}

View File

@@ -3,7 +3,6 @@ package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.support.annotation.LayoutRes;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView.RecycledViewPool;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
@@ -22,17 +21,11 @@ class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder> {
private ConversationListener listener;
private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration;
ConversationAdapter(Context ctx,
ConversationListener conversationListener) {
super(ctx, ConversationItem.class);
listener = conversationListener;
// This shares the same pool for view recycling between all image lists
imageViewPool = new RecycledViewPool();
// Share the item decoration as well
imageItemDecoration = new ImageItemDecoration(ctx);
}
@LayoutRes
@@ -49,17 +42,15 @@ class ConversationAdapter
type, viewGroup, false);
switch (type) {
case R.layout.list_item_conversation_msg_in:
return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration);
return new ConversationMessageViewHolder(v, true);
case R.layout.list_item_conversation_msg_out:
return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration);
return new ConversationMessageViewHolder(v, false);
case R.layout.list_item_conversation_notice_in:
return new ConversationNoticeViewHolder(v, listener, true);
return new ConversationNoticeViewHolder(v, true);
case R.layout.list_item_conversation_notice_out:
return new ConversationNoticeViewHolder(v, listener, false);
return new ConversationNoticeViewHolder(v, false);
case R.layout.list_item_conversation_request:
return new ConversationRequestViewHolder(v, listener, true);
return new ConversationRequestViewHolder(v, true);
default:
throw new IllegalArgumentException("Unknown ConversationItem");
}
@@ -68,7 +59,7 @@ class ConversationAdapter
@Override
public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
ConversationItem item = items.get(position);
ui.bind(item);
ui.bind(item, listener);
listener.onItemVisible(item);
}

View File

@@ -18,17 +18,14 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate;
@NotNullByDefault
abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConversationListener listener;
protected final ConstraintLayout layout;
@Nullable
private final OutItemViewHolder outViewHolder;
private final TextView text;
protected final TextView time;
ConversationItemViewHolder(View v, ConversationListener listener,
boolean isIncoming) {
ConversationItemViewHolder(View v, boolean isIncoming) {
super(v);
this.listener = listener;
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text);
@@ -36,7 +33,7 @@ abstract class ConversationItemViewHolder extends ViewHolder {
}
@CallSuper
void bind(ConversationItem item) {
void bind(ConversationItem item, ConversationListener listener) {
if (item.getText() != null) {
text.setText(trim(item.getText()));
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.conversation;
import android.support.annotation.UiThread;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -15,7 +14,4 @@ interface ConversationListener {
void openRequestedShareable(ConversationRequestItem item);
void onAttachmentClicked(View view, ConversationMessageItem messageItem,
AttachmentItem attachmentItem);
}

View File

@@ -1,45 +1,63 @@
package org.briarproject.briar.android.conversation;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.support.annotation.DrawableRes;
import android.support.annotation.UiThread;
import android.support.constraint.ConstraintSet;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.RecycledViewPool;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.load.Transformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import static android.support.constraint.ConstraintSet.WRAP_CONTENT;
import static android.support.v4.content.ContextCompat.getColor;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@UiThread
@NotNullByDefault
class ConversationMessageViewHolder extends ConversationItemViewHolder {
private final ImageAdapter adapter;
@DrawableRes
private static final int ERROR_RES = R.drawable.ic_image_broken;
private final ImageView imageView;
private final ViewGroup statusLayout;
private final int timeColor, timeColorBubble;
private final int radiusBig, radiusSmall;
private final boolean isRtl;
private final ConstraintSet textConstraints = new ConstraintSet();
private final ConstraintSet imageConstraints = new ConstraintSet();
private final ConstraintSet imageTextConstraints = new ConstraintSet();
ConversationMessageViewHolder(View v, ConversationListener listener,
boolean isIncoming, RecycledViewPool imageViewPool,
ImageItemDecoration imageItemDecoration) {
super(v, listener, isIncoming);
ConversationMessageViewHolder(View v, boolean isIncoming) {
super(v, isIncoming);
imageView = v.findViewById(R.id.imageView);
statusLayout = v.findViewById(R.id.statusLayout);
// image list
RecyclerView list = v.findViewById(R.id.imageList);
list.setRecycledViewPool(imageViewPool);
adapter = new ImageAdapter(v.getContext(), listener);
list.setAdapter(adapter);
list.addItemDecoration(imageItemDecoration);
radiusBig = v.getContext().getResources()
.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
radiusSmall = v.getContext().getResources()
.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
// remember original status text color
timeColor = time.getCurrentTextColor();
timeColorBubble = getColor(v.getContext(), R.color.briar_white);
timeColorBubble =
ContextCompat.getColor(v.getContext(), R.color.briar_white);
// find out if we are showing a RTL language, Use the configuration,
// because getting the layout direction of views is not reliable
Configuration config =
imageView.getContext().getResources().getConfiguration();
isRtl = SDK_INT >= 17 &&
config.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
// clone constraint sets from layout files
textConstraints
@@ -59,8 +77,9 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
}
@Override
void bind(ConversationItem conversationItem) {
super.bind(conversationItem);
void bind(ConversationItem conversationItem,
ConversationListener listener) {
super.bind(conversationItem, listener);
ConversationMessageItem item =
(ConversationMessageItem) conversationItem;
if (item.getAttachments().isEmpty()) {
@@ -71,43 +90,68 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
}
private void bindTextItem() {
resetStatusLayoutForText();
textConstraints.applyTo(layout);
adapter.clear();
}
private void bindImageItem(ConversationMessageItem item) {
ConstraintSet constraintSet;
if (item.getText() == null) {
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
constraintSet = imageConstraints;
} else {
resetStatusLayoutForText();
constraintSet = imageTextConstraints;
}
if (item.getAttachments().size() == 1) {
// apply image size constraints for a single image
AttachmentItem attachment = item.getAttachments().get(0);
int width = attachment.getThumbnailWidth();
int height = attachment.getThumbnailHeight();
constraintSet.constrainWidth(R.id.imageList, width);
constraintSet.constrainHeight(R.id.imageList, height);
} else {
// bubble adapts to size of image list
constraintSet.constrainWidth(R.id.imageList, WRAP_CONTENT);
constraintSet.constrainHeight(R.id.imageList, WRAP_CONTENT);
}
constraintSet.applyTo(layout);
adapter.setConversationItem(item);
}
private void resetStatusLayoutForText() {
clearImage();
statusLayout.setBackgroundResource(0);
// also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor);
textConstraints.applyTo(layout);
}
private void bindImageItem(ConversationMessageItem item) {
// TODO show more than just the first image
AttachmentItem attachment = item.getAttachments().get(0);
ConstraintSet constraintSet;
if (item.getText() == null) {
statusLayout
.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
constraintSet = imageConstraints;
} else {
statusLayout.setBackgroundResource(0);
// also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor);
constraintSet = imageTextConstraints;
}
// apply image size constraints, so glides picks them up for scaling
int width = attachment.getThumbnailWidth();
int height = attachment.getThumbnailHeight();
constraintSet.constrainWidth(R.id.imageView, width);
constraintSet.constrainHeight(R.id.imageView, height);
constraintSet.applyTo(layout);
if (attachment.hasError()) {
clearImage();
imageView.setImageResource(ERROR_RES);
} else {
loadImage(item, attachment);
}
}
private void clearImage() {
GlideApp.with(imageView)
.clear(imageView);
}
private void loadImage(ConversationMessageItem item,
AttachmentItem attachment) {
boolean leftCornerSmall =
(isIncoming() && !isRtl) || (!isIncoming() && isRtl);
boolean bottomRound = item.getText() == null;
Transformation<Bitmap> transformation = new BriarImageTransformation(
radiusSmall, radiusBig, leftCornerSmall, bottomRound);
GlideApp.with(imageView)
.load(attachment)
.diskCacheStrategy(NONE)
.error(ERROR_RES)
.transform(transformation)
.transition(withCrossFade())
.into(imageView)
.waitForLayout();
}
}

View File

@@ -19,17 +19,16 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder {
private final TextView msgText;
ConversationNoticeViewHolder(View v, ConversationListener listener,
boolean isIncoming) {
super(v, listener, isIncoming);
ConversationNoticeViewHolder(View v, boolean isIncoming) {
super(v, isIncoming);
msgText = v.findViewById(R.id.msgText);
}
@Override
@CallSuper
void bind(ConversationItem item) {
void bind(ConversationItem item, ConversationListener listener) {
ConversationNoticeItem notice = (ConversationNoticeItem) item;
super.bind(notice);
super.bind(notice, listener);
String text = notice.getMsgText();
if (isNullOrEmpty(text)) {

View File

@@ -17,17 +17,16 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder {
private final Button acceptButton;
private final Button declineButton;
ConversationRequestViewHolder(View v, ConversationListener listener,
boolean isIncoming) {
super(v, listener, isIncoming);
ConversationRequestViewHolder(View v, boolean isIncoming) {
super(v, isIncoming);
acceptButton = v.findViewById(R.id.acceptButton);
declineButton = v.findViewById(R.id.declineButton);
}
@Override
void bind(ConversationItem item) {
void bind(ConversationItem item, ConversationListener listener) {
ConversationRequestItem request = (ConversationRequestItem) item;
super.bind(request);
super.bind(request, listener);
if (request.wasAnswered() && request.canBeOpened()) {
acceptButton.setVisibility(VISIBLE);

View File

@@ -5,40 +5,19 @@ import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations;
import android.content.ContentResolver;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -47,34 +26,19 @@ import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault
public class ConversationViewModel extends AndroidViewModel {
private static Logger LOG =
getLogger(ConversationViewModel.class.getName());
private static final String SHOW_ONBOARDING_IMAGE =
"showOnboardingImage";
private static final String SHOW_ONBOARDING_INTRODUCTION =
"showOnboardingIntroduction";
@DatabaseExecutor
private final Executor dbExecutor;
@CryptoExecutor
private final Executor cryptoExecutor;
// TODO replace with TransactionManager once it exists
private final DatabaseComponent db;
private final MessagingManager messagingManager;
private final ContactManager contactManager;
private final SettingsManager settingsManager;
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentController attachmentController;
@Nullable
@@ -84,64 +48,38 @@ public class ConversationViewModel extends AndroidViewModel {
Transformations.map(contact, c -> c.getAuthor().getId());
private final LiveData<String> contactName =
Transformations.map(contact, UiUtils::getContactDisplayName);
private final MutableLiveData<Boolean> imageSupport =
new MutableLiveData<>();
private final MutableLiveData<Boolean> showImageOnboarding =
new MutableLiveData<>();
private final MutableLiveData<Boolean> showIntroductionOnboarding =
new MutableLiveData<>();
private final MutableLiveData<Boolean> showIntroductionAction =
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>();
private final MutableLiveData<GroupId> messagingGroupId =
new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
@Inject
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor, DatabaseComponent db,
MessagingManager messagingManager, ContactManager contactManager,
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory) {
ContactManager contactManager, MessagingManager messagingManager) {
super(application);
this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.db = db;
this.messagingManager = messagingManager;
this.contactManager = contactManager;
this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory;
this.attachmentController = new AttachmentController(messagingManager,
getAttachmentDimensions(application.getResources()));
application.getResources());
contactDeleted.setValue(false);
}
/**
* Setting the {@link ContactId} automatically triggers loading of other
* data.
*/
void setContactId(ContactId contactId) {
if (this.contactId == null) {
this.contactId = contactId;
loadContact(contactId);
loadContact();
} else if (!contactId.equals(this.contactId)) {
throw new IllegalStateException();
}
}
private void loadContact(ContactId contactId) {
private void loadContact() {
dbExecutor.execute(() -> {
try {
long start = now();
Contact c = contactManager.getContact(contactId);
Contact c =
contactManager.getContact(requireNonNull(contactId));
contact.postValue(c);
logDuration(LOG, "Loading contact", start);
start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) {
contactDeleted.postValue(true);
} catch (DbException e) {
@@ -155,179 +93,13 @@ public class ConversationViewModel extends AndroidViewModel {
try {
contactManager.setContactAlias(requireNonNull(contactId),
alias.isEmpty() ? null : alias);
loadContact(contactId);
loadContact();
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) {
if (messagingGroupId.getValue() == null) loadGroupId();
observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return;
// calls through to creating and storing the message
storeAttachments(groupId, text, uris, timestamp);
});
}
private void loadGroupId() {
if (contactId == null) throw new IllegalStateException();
dbExecutor.execute(() -> {
try {
messagingGroupId.postValue(
messagingManager.getConversationId(contactId));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@DatabaseExecutor
private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
// check if images are supported
boolean imagesSupported = db.transactionWithResult(true, txn ->
messagingManager.contactSupportsImages(txn, c));
imageSupport.postValue(imagesSupported);
// check if introductions are supported
Collection<Contact> contacts = contactManager.getActiveContacts();
boolean introductionSupported = contacts.size() > 1;
showIntroductionAction.postValue(introductionSupported);
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
if (imagesSupported &&
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
// check if we should show onboarding, only if images are supported
showImageOnboarding.postValue(true);
// allow observer to stop listening for changes
showIntroductionOnboarding.postValue(false);
} else {
// allow observer to stop listening for changes
showImageOnboarding.postValue(false);
// we only show one onboarding dialog at a time
if (introductionSupported &&
settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION, true)) {
showIntroductionOnboarding.postValue(true);
} else {
// allow observer to stop listening for changes
showIntroductionOnboarding.postValue(false);
}
}
}
void onImageOnboardingSeen() {
onOnboardingSeen(SHOW_ONBOARDING_IMAGE);
}
void onIntroductionOnboardingSeen() {
onOnboardingSeen(SHOW_ONBOARDING_INTRODUCTION);
}
private void onOnboardingSeen(String key) {
dbExecutor.execute(() -> {
try {
Settings settings = new Settings();
settings.putBoolean(key, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void storeAttachments(GroupId groupId, @Nullable String text,
List<Uri> uris, long timestamp) {
dbExecutor.execute(() -> {
long start = now();
List<AttachmentHeader> attachments = new ArrayList<>();
List<AttachmentItem> items = new ArrayList<>();
boolean needsSize = uris.size() == 1;
for (Uri uri : uris) {
Pair<AttachmentHeader, AttachmentItem> pair =
createAttachmentHeader(groupId, uri, timestamp,
needsSize);
if (pair == null) continue;
attachments.add(pair.getFirst());
items.add(pair.getSecond());
}
logDuration(LOG, "Storing attachments", start);
createMessage(groupId, text, attachments, items, timestamp);
});
}
@Nullable
@DatabaseExecutor
private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
GroupId groupId, Uri uri, long timestamp, boolean needsSize) {
InputStream is = null;
try {
ContentResolver contentResolver =
getApplication().getContentResolver();
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
is.close();
// re-open stream to get AttachmentItem
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
AttachmentItem item = attachmentController
.getAttachmentItem(h, new Attachment(is), needsSize);
return new Pair<>(h, item);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
return null;
} finally {
if (is != null) tryToClose(is, LOG, WARNING);
}
}
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
long timestamp) {
cryptoExecutor.execute(() -> {
try {
// TODO remove when text can be null in the backend
String msgText = text == null ? "null" : text;
PrivateMessage pm = privateMessageFactory
.createPrivateMessage(groupId, timestamp, msgText,
attachments);
attachmentController.put(pm.getMessage().getId(), aItems);
storeMessage(pm, msgText, attachments);
} catch (FormatException e) {
throw new RuntimeException(e);
}
});
}
private void storeMessage(PrivateMessage m, @Nullable String text,
List<AttachmentHeader> attachments) {
dbExecutor.execute(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
text != null, attachments);
// TODO add text to cache when available here
addedHeader.postValue(h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@UiThread
void onAddedPrivateMessageSeen() {
addedHeader.setValue(null);
}
AttachmentController getAttachmentController() {
return attachmentController;
}
@@ -344,28 +116,8 @@ public class ConversationViewModel extends AndroidViewModel {
return contactName;
}
LiveData<Boolean> hasImageSupport() {
return imageSupport;
}
LiveData<Boolean> showImageOnboarding() {
return showImageOnboarding;
}
LiveData<Boolean> showIntroductionOnboarding() {
return showIntroductionOnboarding;
}
LiveData<Boolean> showIntroductionAction() {
return showIntroductionAction;
}
LiveData<Boolean> isContactDeleted() {
return contactDeleted;
}
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
return addedHeader;
}
}

View File

@@ -1,326 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog.Builder;
import android.support.v7.widget.Toolbar;
import android.transition.Fade;
import android.transition.Transition;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.view.PullDownLayout;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import static android.content.Intent.ACTION_CREATE_DOCUMENT;
import static android.content.Intent.CATEGORY_OPENABLE;
import static android.content.Intent.EXTRA_TITLE;
import static android.graphics.Color.TRANSPARENT;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static android.view.View.GONE;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT;
import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ImageActivity extends BriarActivity
implements PullDownLayout.Callback, OnGlobalLayoutListener {
final static String ATTACHMENTS = "attachments";
final static String ATTACHMENT_POSITION = "position";
final static String NAME = "name";
final static String DATE = "date";
@Inject
ViewModelProvider.Factory viewModelFactory;
private ImageViewModel viewModel;
private PullDownLayout layout;
private AppBarLayout appBarLayout;
private ViewPager viewPager;
private List<AttachmentItem> attachments;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
// Transitions
if (state == null) supportPostponeEnterTransition();
Window window = getWindow();
if (SDK_INT >= 21) {
Transition transition = new Fade();
setSceneTransitionAnimation(transition, null, transition);
}
// get View Model
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ImageViewModel.class);
viewModel.getSaveState().observe(this, this::onImageSaveStateChanged);
// inflate layout
setContentView(R.layout.activity_image);
layout = findViewById(R.id.layout);
layout.setCallback(this);
layout.getViewTreeObserver().addOnGlobalLayoutListener(this);
// Status Bar
if (SDK_INT >= 21) {
window.setStatusBarColor(TRANSPARENT);
} else if (SDK_INT >= 19) {
// we can't make the status bar transparent, but translucent
window.addFlags(FLAG_TRANSLUCENT_STATUS);
}
// Toolbar
appBarLayout = findViewById(R.id.appBarLayout);
Toolbar toolbar = requireNonNull(setUpCustomToolbar(true));
TextView contactName = toolbar.findViewById(R.id.contactName);
TextView dateView = toolbar.findViewById(R.id.dateView);
// Intent Extras
Intent i = getIntent();
attachments = i.getParcelableArrayListExtra(ATTACHMENTS);
int position = i.getIntExtra(ATTACHMENT_POSITION, -1);
if (position == -1) throw new IllegalStateException();
String name = i.getStringExtra(NAME);
long time = i.getLongExtra(DATE, 0);
String date = formatDateAbsolute(this, time);
contactName.setText(name);
dateView.setText(date);
// Set up image ViewPager
viewPager = findViewById(R.id.viewPager);
ImagePagerAdapter pagerAdapter =
new ImagePagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(pagerAdapter);
viewPager.setCurrentItem(position);
if (SDK_INT >= 16) {
viewModel.getOnImageClicked().observe(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility(
SYSTEM_UI_FLAG_LAYOUT_STABLE |
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.image_actions, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.action_save_image:
showSaveImageDialog();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onGlobalLayout() {
viewModel.setToolbarPosition(
appBarLayout.getTop(), appBarLayout.getBottom()
);
if (SDK_INT >= 16) {
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK &&
data != null) {
viewModel.saveImage(getVisibleAttachment(), data.getData());
}
}
@Override
public void onPullStart() {
appBarLayout.animate()
.alpha(0f)
.start();
}
@Override
public void onPull(float progress) {
}
@Override
public void onPullCancel() {
appBarLayout.animate()
.alpha(1f)
.start();
}
@Override
public void onPullComplete() {
supportFinishAfterTransition();
}
@RequiresApi(api = 16)
private void onImageClicked(@Nullable Boolean clicked) {
if (clicked != null && clicked) {
toggleSystemUi();
viewModel.onOnImageClickSeen();
}
}
@RequiresApi(api = 16)
private void toggleSystemUi() {
View decorView = getWindow().getDecorView();
if (appBarLayout.getVisibility() == VISIBLE) {
hideSystemUi(decorView);
} else {
showSystemUi(decorView);
}
}
@RequiresApi(api = 16)
private void hideSystemUi(View decorView) {
decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN |
SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
);
appBarLayout.animate()
.translationYBy(-1 * appBarLayout.getHeight())
.alpha(0f)
.withEndAction(() -> appBarLayout.setVisibility(GONE))
.start();
}
@RequiresApi(api = 16)
private void showSystemUi(View decorView) {
decorView.setSystemUiVisibility(
SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
);
appBarLayout.animate()
.translationYBy(appBarLayout.getHeight())
.alpha(1f)
.withStartAction(() -> appBarLayout.setVisibility(VISIBLE))
.start();
}
private void showSaveImageDialog() {
OnClickListener okListener = (dialog, which) -> {
if (SDK_INT >= 19) {
Intent intent = getCreationIntent();
startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT);
} else {
viewModel.saveImage(getVisibleAttachment());
}
};
Builder builder = new Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_save_image));
builder.setMessage(getString(R.string.dialog_message_save_image));
Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_security);
DrawableCompat.setTint(requireNonNull(icon),
ContextCompat.getColor(this, R.color.color_primary));
builder.setIcon(icon);
builder.setPositiveButton(R.string.save_image, okListener);
builder.setNegativeButton(R.string.cancel, null);
builder.show();
}
@RequiresApi(api = 19)
private Intent getCreationIntent() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
Locale.getDefault());
String fileName = sdf.format(new Date());
Intent intent = new Intent(ACTION_CREATE_DOCUMENT);
intent.addCategory(CATEGORY_OPENABLE);
intent.setType(getVisibleAttachment().getMimeType());
intent.putExtra(EXTRA_TITLE, fileName);
return intent;
}
private void onImageSaveStateChanged(@Nullable Boolean error) {
if (error == null) return;
int stringRes = error ?
R.string.save_image_error : R.string.save_image_success;
int colorRes = error ?
R.color.briar_red : R.color.briar_primary;
Snackbar s = Snackbar.make(layout, stringRes, LENGTH_LONG);
s.getView().setBackgroundResource(colorRes);
s.show();
viewModel.onSaveStateSeen();
}
AttachmentItem getVisibleAttachment() {
return attachments.get(viewPager.getCurrentItem());
}
private class ImagePagerAdapter extends FragmentStatePagerAdapter {
private boolean isFirst = true;
private ImagePagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Fragment f = ImageFragment
.newInstance(attachments.get(position), isFirst);
isFirst = false;
return f;
}
@Override
public int getCount() {
return attachments.size();
}
}
}

View File

@@ -1,155 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView.Adapter;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.Radii;
import java.util.ArrayList;
import java.util.List;
import static android.content.Context.WINDOW_SERVICE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.util.UiUtils.isRtl;
@NotNullByDefault
class ImageAdapter extends Adapter<ImageViewHolder> {
private final List<AttachmentItem> items = new ArrayList<>();
private final ConversationListener listener;
private final int imageSize;
private final int radiusBig, radiusSmall;
private final boolean isRtl;
@Nullable
private ConversationMessageItem conversationItem;
ImageAdapter(Context ctx, ConversationListener listener) {
this.listener = listener;
imageSize = getImageSize(ctx);
Resources res = ctx.getResources();
radiusBig =
res.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
radiusSmall =
res.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
isRtl = isRtl(ctx);
}
@Override
public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_image, viewGroup, false);
return new ImageViewHolder(v, imageSize);
}
@Override
public void onBindViewHolder(ImageViewHolder imageViewHolder,
int position) {
// get item
requireNonNull(conversationItem);
AttachmentItem item = items.get(position);
// set onClick listener
imageViewHolder.itemView.setOnClickListener(v ->
listener.onAttachmentClicked(v, conversationItem, item)
);
// bind view holder
int size = items.size();
boolean isIncoming = conversationItem.isIncoming();
boolean hasText = conversationItem.getText() != null;
Radii r = getRadii(position, size, isIncoming, hasText);
imageViewHolder.bind(item, r, size == 1, singleInRow(position, size));
}
@Override
public int getItemCount() {
return items.size();
}
void setConversationItem(ConversationMessageItem item) {
this.conversationItem = item;
this.items.clear();
this.items.addAll(item.getAttachments());
notifyDataSetChanged();
}
private int getImageSize(Context ctx) {
Resources res = ctx.getResources();
WindowManager windowManager =
(WindowManager) ctx.getSystemService(WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
if (windowManager == null) {
return res.getDimensionPixelSize(
R.dimen.message_bubble_image_default);
}
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
int imageSize = displayMetrics.widthPixels / 3;
int maxSize = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_width);
return Math.min(imageSize, maxSize);
}
private Radii getRadii(int pos, int num, boolean isIncoming,
boolean hasText) {
boolean left = isLeft(pos);
boolean single = num == 1;
// Top Row
int topLeft;
int topRight;
if (single) {
topLeft = isIncoming ? radiusSmall : radiusBig;
topRight = !isIncoming ? radiusSmall : radiusBig;
} else if (isTopRow(pos)) {
topLeft = left ? (isIncoming ? radiusSmall : radiusBig) : 0;
topRight = !left ? (!isIncoming ? radiusSmall : radiusBig) : 0;
} else {
topLeft = 0;
topRight = 0;
}
// Bottom Row
boolean singleInRow = singleInRow(pos, num);
int bottomLeft;
int bottomRight;
if (!hasText && isBottomRow(pos, num)) {
bottomLeft = singleInRow || left ? radiusBig : 0;
bottomRight = singleInRow || !left ? radiusBig : 0;
} else {
bottomLeft = 0;
bottomRight = 0;
}
if (isRtl) return new Radii(topRight, topLeft, bottomRight, bottomLeft);
return new Radii(topLeft, topRight, bottomLeft, bottomRight);
}
void clear() {
items.clear();
notifyDataSetChanged();
}
static boolean isTopRow(int pos) {
return pos < 2;
}
static boolean isLeft(int pos) {
return pos % 2 == 0;
}
static boolean isBottomRow(int pos, int num) {
return num % 2 == 0 ?
pos >= num - 2 : // last two, if even
pos > num - 2; // last one, if odd
}
static boolean singleInRow(int pos, int num) {
// last item of an odd number
return num % 2 != 0 && pos == num -1;
}
}

View File

@@ -1,133 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
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 com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT;
import static android.widget.ImageView.ScaleType.FIT_START;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
@MethodsNotNullByDefault
@ParametersAreNonnullByDefault
public class ImageFragment extends Fragment {
private final static String IS_FIRST = "isFirst";
@Inject
ViewModelProvider.Factory viewModelFactory;
private AttachmentItem attachment;
private boolean isFirst;
private ImageViewModel viewModel;
private PhotoView photoView;
static ImageFragment newInstance(AttachmentItem a, boolean isFirst) {
ImageFragment f = new ImageFragment();
Bundle args = new Bundle();
args.putParcelable(ATTACHMENT_POSITION, a);
args.putBoolean(IS_FIRST, isFirst);
f.setArguments(args);
return f;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
BaseActivity a = (BaseActivity) requireNonNull(getActivity());
a.getActivityComponent().inject(this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = requireNonNull(getArguments());
attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION));
isFirst = args.getBoolean(IS_FIRST);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_image, container,
false);
viewModel = ViewModelProviders.of(requireNonNull(getActivity()),
viewModelFactory).get(ImageViewModel.class);
photoView = v.findViewById(R.id.photoView);
photoView.setScaleLevels(1, 2, 4);
photoView.setOnClickListener(view -> viewModel.clickImage());
// Request Listener
RequestListener<Drawable> listener = new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
if (getActivity() != null && isFirst)
getActivity().supportStartPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
// set transition name only when not animatable,
// because the animation won't start otherwise
photoView.setTransitionName(
attachment.getTransitionName());
}
// Move image to the top if overlapping toolbar
if (viewModel.isOverlappingToolbar(photoView, resource)) {
photoView.setScaleType(FIT_START);
}
if (getActivity() != null && isFirst) {
getActivity().supportStartPostponedEnterTransition();
}
return false;
}
};
// Load Image
GlideApp.with(this)
.load(attachment)
// TODO allow if size < maxTextureSize ?
// .override(SIZE_ORIGINAL)
.diskCacheStrategy(NONE)
.error(R.drawable.ic_image_broken)
.addListener(listener)
.into(photoView);
return v;
}
}

View File

@@ -1,29 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
@NotNullByDefault
interface ImageHelper {
DecodeResult decodeStream(InputStream is);
@Nullable
String getExtensionFromMimeType(String mimeType);
class DecodeResult {
final int width;
final int height;
final String mimeType;
DecodeResult(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
}
}
}

View File

@@ -1,54 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemDecoration;
import android.support.v7.widget.RecyclerView.State;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import static org.briarproject.briar.android.conversation.ImageAdapter.isBottomRow;
import static org.briarproject.briar.android.conversation.ImageAdapter.isLeft;
import static org.briarproject.briar.android.conversation.ImageAdapter.isTopRow;
import static org.briarproject.briar.android.conversation.ImageAdapter.singleInRow;
import static org.briarproject.briar.android.util.UiUtils.isRtl;
@NotNullByDefault
class ImageItemDecoration extends ItemDecoration {
private final int border;
private final boolean isRtl;
ImageItemDecoration(Context ctx) {
Resources res = ctx.getResources();
// for pixel perfection, add a pixel to the border if it has an odd size
int b = res.getDimensionPixelSize(R.dimen.message_bubble_border);
int realBorderSize = b % 2 == 0 ? b : b + 1;
// we are applying half the border around the insides of each image
// to prevent differently sized images looking slightly broken
border = realBorderSize / 2;
// find out if we are showing a RTL language
isRtl = isRtl(ctx);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
State state) {
if (state.getItemCount() == 1) return;
int pos = parent.getChildAdapterPosition(view);
int num = state.getItemCount();
boolean start = isLeft(pos) ^ isRtl;
outRect.top = isTopRow(pos) ? 0 : border;
outRect.left = start ? 0 : border;
outRect.right = start && !singleInRow(pos, num) ? border : 0;
outRect.bottom = isBottomRow(pos, num) ? 0 : border;
}
}

View File

@@ -1,74 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.graphics.Bitmap;
import android.support.annotation.DrawableRes;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.load.Transformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.conversation.glide.Radii;
import static android.os.Build.VERSION.SDK_INT;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@NotNullByDefault
class ImageViewHolder extends ViewHolder {
@DrawableRes
private static final int ERROR_RES = R.drawable.ic_image_broken;
protected final ImageView imageView;
private final int imageSize;
ImageViewHolder(View v, int imageSize) {
super(v);
imageView = v.findViewById(R.id.imageView);
this.imageSize = imageSize;
}
void bind(AttachmentItem attachment, Radii r, boolean single,
boolean needsStretch) {
if (attachment.hasError()) {
GlideApp.with(imageView)
.clear(imageView);
imageView.setImageResource(ERROR_RES);
} else {
setImageViewDimensions(attachment, single, needsStretch);
loadImage(attachment, r);
if (SDK_INT >= 21) {
imageView.setTransitionName(attachment.getTransitionName());
}
}
}
private void setImageViewDimensions(AttachmentItem a, boolean single,
boolean needsStretch) {
LayoutParams params = (LayoutParams) imageView.getLayoutParams();
int width = needsStretch ? imageSize * 2 : imageSize;
params.width = single ? a.getThumbnailWidth() : width;
params.height = single ? a.getThumbnailHeight() : imageSize;
params.setFullSpan(!single && needsStretch);
imageView.setLayoutParams(params);
}
private void loadImage(AttachmentItem a, Radii r) {
Transformation<Bitmap> transformation = new BriarImageTransformation(r);
GlideApp.with(imageView)
.load(a)
.diskCacheStrategy(NONE)
.error(ERROR_RES)
.transform(transformation)
.transition(withCrossFade())
.into(imageView)
.waitForLayout();
}
}

View File

@@ -1,214 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.View;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.media.MediaScannerConnection.scanFile;
import static android.os.Environment.DIRECTORY_PICTURES;
import static android.os.Environment.getExternalStoragePublicDirectory;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class ImageViewModel extends AndroidViewModel {
private static Logger LOG = getLogger(ImageViewModel.class.getName());
private final MessagingManager messagingManager;
@DatabaseExecutor
private final Executor dbExecutor;
@IoExecutor
private final Executor ioExecutor;
/**
* true means there was an error saving the image, false if image was saved.
*/
private final MutableLiveData<Boolean> saveState = new MutableLiveData<>();
private final MutableLiveData<Boolean> imageClicked =
new MutableLiveData<>();
private int toolbarTop, toolbarBottom;
@Inject
ImageViewModel(Application application,
MessagingManager messagingManager,
@DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor) {
super(application);
this.messagingManager = messagingManager;
this.dbExecutor = dbExecutor;
this.ioExecutor = ioExecutor;
}
void clickImage() {
imageClicked.setValue(true);
}
/**
* A LiveData that is true if the image was clicked,
* false if it wasn't.
*
* Call {@link #onOnImageClickSeen()} after consuming an update.
*/
LiveData<Boolean> getOnImageClicked() {
return imageClicked;
}
@UiThread
void onOnImageClickSeen() {
imageClicked.setValue(false);
}
void setToolbarPosition(int top, int bottom) {
toolbarTop = top;
toolbarBottom = bottom;
}
boolean isOverlappingToolbar(View screenView, Drawable drawable) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
float widthPercentage = screenView.getWidth() / (float) width;
float heightPercentage = screenView.getHeight() / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
int realWidth = (int) (width * scaleFactor);
int realHeight = (int) (height * scaleFactor);
// return if image doesn't use the full width,
// because it will be moved to the right otherwise
if (realWidth < screenView.getWidth()) return false;
int drawableTop = (screenView.getHeight() - realHeight) / 2;
return drawableTop < toolbarBottom && drawableTop != toolbarTop;
}
/**
* A LiveData that is true if there was an error
* and false if the image was saved.
* It can be null otherwise, if no image was saved recently.
*
* Call {@link #onSaveStateSeen()} after consuming an update.
*/
LiveData<Boolean> getSaveState() {
return saveState;
}
@UiThread
void onSaveStateSeen() {
saveState.setValue(null);
}
/**
* Saves the attachment to a writeable {@link Uri}.
*/
@UiThread
void saveImage(AttachmentItem attachment, @Nullable Uri uri) {
if (uri == null) {
saveState.setValue(true);
} else {
saveImage(attachment, () -> getOutputStream(uri), null);
}
}
/**
* Saves the attachment on external storage,
* assuming the permission was granted during install time.
*/
void saveImage(AttachmentItem attachment) {
File file = getImageFile(attachment);
saveImage(attachment, () -> getOutputStream(file), () -> scanFile(
getApplication(), new String[] {file.toString()}, null, null));
}
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> {
try {
Attachment a = messagingManager.getAttachment(messageId);
copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) {
logException(LOG, WARNING, e);
saveState.postValue(true);
}
});
}
private void copyImageFromDb(Attachment a, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
ioExecutor.execute(() -> {
try {
InputStream is = a.getStream();
OutputStream os = osp.getOutputStream();
copyAndClose(is, os);
if (afterCopy != null) afterCopy.run();
saveState.postValue(false);
} catch (IOException e) {
logException(LOG, WARNING, e);
saveState.postValue(true);
}
});
}
private String getFileName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
Locale.getDefault());
return sdf.format(new Date());
}
private File getImageFile(AttachmentItem attachment) {
File path = getExternalStoragePublicDirectory(DIRECTORY_PICTURES);
//noinspection ResultOfMethodCallIgnored
path.mkdirs();
String fileName = getFileName();
String ext = "." + attachment.getExtension();
File file = new File(path, fileName + ext);
int i = 1;
while (file.exists()) {
file = new File(path, fileName + " (" + i + ")" + ext);
}
return file;
}
private OutputStream getOutputStream(File file) throws IOException {
return new FileOutputStream(file);
}
private OutputStream getOutputStream(Uri uri) throws IOException {
OutputStream os =
getApplication().getContentResolver().openOutputStream(uri);
if (os == null) throw new IOException();
return os;
}
private interface OutputStreamProvider {
OutputStream getOutputStream() throws IOException;
}
}

View File

@@ -40,7 +40,7 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
private volatile boolean cancel = false;
@Inject
BriarDataFetcher(MessagingManager messagingManager,
public BriarDataFetcher(MessagingManager messagingManager,
@DatabaseExecutor Executor dbExecutor, AttachmentItem attachment) {
this.messagingManager = messagingManager;
this.dbExecutor = dbExecutor;

View File

@@ -7,8 +7,10 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
public class BriarImageTransformation extends MultiTransformation<Bitmap> {
public BriarImageTransformation(Radii r) {
super(new CenterCrop(), new CustomCornersTransformation(r));
public BriarImageTransformation(int smallRadius, int radius,
boolean leftCornerSmall, boolean bottomRound) {
super(new CenterCrop(), new ImageCornerTransformation(
smallRadius, radius, leftCornerSmall, bottomRound));
}
}

View File

@@ -22,7 +22,7 @@ public final class BriarModelLoader
@Inject
BriarDataFetcherFactory dataFetcherFactory;
BriarModelLoader(BriarApplication app) {
public BriarModelLoader(BriarApplication app) {
app.getApplicationComponent().inject(this);
}

View File

@@ -16,7 +16,7 @@ class BriarModelLoaderFactory
private final BriarApplication app;
BriarModelLoaderFactory(BriarApplication app) {
public BriarModelLoaderFactory(BriarApplication app) {
this.app = app;
}

View File

@@ -1,129 +0,0 @@
package org.briarproject.briar.android.conversation.glide;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.security.MessageDigest;
import javax.annotation.concurrent.Immutable;
import static android.graphics.Bitmap.Config.ARGB_8888;
import static android.graphics.Shader.TileMode.CLAMP;
@Immutable
@NotNullByDefault
class CustomCornersTransformation extends BitmapTransformation {
private static final String ID =
CustomCornersTransformation.class.getName();
private final Radii radii;
CustomCornersTransformation(Radii radii) {
this.radii = radii;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform,
int outWidth, int outHeight) {
int width = toTransform.getWidth();
int height = toTransform.getHeight();
Bitmap bitmap = pool.get(width, height, ARGB_8888);
bitmap.setHasAlpha(true);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP));
drawRect(canvas, paint, width, height);
return bitmap;
}
private void drawRect(Canvas canvas, Paint paint, float width,
float height) {
drawTopLeft(canvas, paint, radii.topLeft, width, height);
drawTopRight(canvas, paint, radii.topRight, width, height);
drawBottomLeft(canvas, paint, radii.bottomLeft, width, height);
drawBottomRight(canvas, paint, radii.bottomRight, width, height);
}
private void drawTopLeft(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
0,
0,
width / 2 + radius + 1,
height / 2 + radius + 1
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
private void drawTopRight(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
width / 2 - radius,
0,
width,
height / 2 + radius + 1
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
private void drawBottomLeft(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
0,
height / 2 - radius,
width / 2 + radius + 1,
height
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
private void drawBottomRight(Canvas canvas, Paint paint, int radius,
float width, float height) {
RectF rect = new RectF(
width / 2 - radius,
height / 2 - radius,
width,
height
);
if (radius == 0) canvas.drawRect(rect, paint);
else canvas.drawRoundRect(rect, radius, radius, paint);
}
@Override
public String toString() {
return "ImageCornerTransformation(" + radii + ")";
}
@Override
public boolean equals(Object o) {
return o instanceof CustomCornersTransformation &&
radii.equals(((CustomCornersTransformation) o).radii);
}
@Override
public int hashCode() {
return ID.hashCode() + radii.hashCode();
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update((ID + radii).getBytes(CHARSET));
}
}

View File

@@ -0,0 +1,111 @@
package org.briarproject.briar.android.conversation.glide;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.security.MessageDigest;
import javax.annotation.concurrent.Immutable;
import static android.graphics.Bitmap.Config.ARGB_8888;
import static android.graphics.Shader.TileMode.CLAMP;
@Immutable
@NotNullByDefault
class ImageCornerTransformation extends BitmapTransformation {
private static final String ID = ImageCornerTransformation.class.getName();
private final int smallRadius, radius;
private final boolean leftCornerSmall, bottomRound;
ImageCornerTransformation(int smallRadius, int radius,
boolean leftCornerSmall, boolean bottomRound) {
this.smallRadius = smallRadius;
this.radius = radius;
this.leftCornerSmall = leftCornerSmall;
this.bottomRound = bottomRound;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform,
int outWidth, int outHeight) {
int width = toTransform.getWidth();
int height = toTransform.getHeight();
Bitmap bitmap = pool.get(width, height, ARGB_8888);
bitmap.setHasAlpha(true);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP));
drawRect(canvas, paint, width, height);
return bitmap;
}
private void drawRect(Canvas canvas, Paint paint, float width,
float height) {
drawSmallCorner(canvas, paint, width);
drawBigCorners(canvas, paint, width, height);
}
private void drawSmallCorner(Canvas canvas, Paint paint, float width) {
float left = leftCornerSmall ? 0 : width - radius;
float right = leftCornerSmall ? radius : width;
canvas.drawRoundRect(new RectF(left, 0, right, radius),
smallRadius, smallRadius, paint);
}
private void drawBigCorners(Canvas canvas, Paint paint, float width,
float height) {
float top = bottomRound ? 0 : radius;
RectF rect = new RectF(0, top, width, height);
if (bottomRound) {
canvas.drawRoundRect(rect, radius, radius, paint);
} else {
canvas.drawRect(rect, paint);
canvas.drawRoundRect(new RectF(0, 0, width, radius * 2),
radius, radius, paint);
}
}
@Override
public String toString() {
return "ImageCornerTransformation(smallRadius=" + smallRadius +
", radius=" + radius + ", leftCornerSmall=" + leftCornerSmall +
", bottomRound=" + bottomRound + ")";
}
@Override
public boolean equals(Object o) {
return o instanceof ImageCornerTransformation &&
((ImageCornerTransformation) o).smallRadius == smallRadius &&
((ImageCornerTransformation) o).radius == radius &&
((ImageCornerTransformation) o).leftCornerSmall ==
leftCornerSmall &&
((ImageCornerTransformation) o).bottomRound == bottomRound;
}
@Override
public int hashCode() {
return ID.hashCode() + (smallRadius << 16) ^ (radius << 2) ^
(leftCornerSmall ? 2 : 0) ^ (bottomRound ? 1 : 0);
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update((ID + '|' + smallRadius + '|' + radius + '|' +
leftCornerSmall + '|' + bottomRound).getBytes(CHARSET));
}
}

View File

@@ -1,41 +0,0 @@
package org.briarproject.briar.android.conversation.glide;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class Radii {
public final int topLeft, topRight, bottomLeft, bottomRight;
public Radii(int topLeft, int topRight, int bottomLeft, int bottomRight) {
this.topLeft = topLeft;
this.topRight = topRight;
this.bottomLeft = bottomLeft;
this.bottomRight = bottomRight;
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof Radii &&
topLeft == ((Radii) o).topLeft &&
topRight == ((Radii) o).topRight &&
bottomLeft == ((Radii) o).bottomLeft &&
bottomRight == ((Radii) o).bottomRight;
}
@Override
public int hashCode() {
return topLeft << 24 ^ topRight << 16 ^ bottomLeft << 8 ^ bottomRight;
}
@Override
public String toString() {
return "Radii(topLeft=" + topLeft +
",topRight=" + topRight +
",bottomLeft=" + bottomLeft +
",bottomRight=" + bottomRight;
}
}

View File

@@ -44,7 +44,6 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -81,18 +80,13 @@ public class ForumListFragment extends BaseEventFragment implements
return fragment;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(R.string.forums_button);
getActivity().setTitle(R.string.forums_button);
View contentView =
inflater.inflate(R.layout.fragment_forum_list, container,
@@ -108,7 +102,7 @@ public class ForumListFragment extends BaseEventFragment implements
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setAction(R.string.show, this);
snackbar.setActionTextColor(ContextCompat
.getColor(getActivity(), R.color.briar_button_text_positive));
.getColor(getContext(), R.color.briar_button_text_positive));
return contentView;
}
@@ -118,6 +112,11 @@ public class ForumListFragment extends BaseEventFragment implements
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();

View File

@@ -10,15 +10,11 @@ import android.support.v4.app.FragmentActivity;
import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BaseFragment extends Fragment
implements DestroyableContext {
@@ -26,15 +22,12 @@ public abstract class BaseFragment extends Fragment
public abstract String getUniqueTag();
public abstract void injectFragment(ActivityComponent component);
@Override
public void onAttach(Context context) {
super.onAttach(context);
listener = (BaseFragmentListener) context;
injectFragment(listener.getActivityComponent());
}
public void injectFragment(ActivityComponent component) {
// fragments that need to inject, can override this method
}
@Override
@@ -45,6 +38,12 @@ public abstract class BaseFragment extends Fragment
setHasOptionsMenu(true);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
injectFragment(listener.getActivityComponent());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {

View File

@@ -10,6 +10,7 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@MethodsNotNullByDefault
@@ -56,4 +57,9 @@ public class ErrorFragment extends BaseFragment {
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
// not necessary
}
}

View File

@@ -1,8 +1,7 @@
package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
@@ -12,8 +11,6 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@@ -31,14 +28,10 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ContactChooserFragment extends BaseFragment {
public static final String TAG = ContactChooserFragment.class.getName();
@@ -58,6 +51,7 @@ public class ContactChooserFragment extends BaseFragment {
volatile ConnectionRegistry connectionRegistry;
public static ContactChooserFragment newInstance(ContactId id) {
Bundle args = new Bundle();
ContactChooserFragment fragment = new ContactChooserFragment();
@@ -67,13 +61,13 @@ public class ContactChooserFragment extends BaseFragment {
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
public void onAttach(Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View contentView = inflater.inflate(R.layout.list, container, false);
@@ -83,16 +77,14 @@ public class ContactChooserFragment extends BaseFragment {
Contact c2 = item.getContact();
showMessageScreen(c1, c2);
};
adapter = new ContactListAdapter(requireNonNull(getActivity()),
onContactClickListener);
adapter = new ContactListAdapter(getActivity(), onContactClickListener);
list = contentView.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
list.setEmptyText(R.string.no_contacts);
contactId = new ContactId(
requireNonNull(getArguments()).getInt(CONTACT_ID));
contactId = new ContactId(getArguments().getInt(CONTACT_ID));
return contentView;
}
@@ -115,6 +107,11 @@ public class ContactChooserFragment extends BaseFragment {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
private void loadContacts() {
listener.runOnDbThread(() -> {
try {

View File

@@ -1,8 +1,8 @@
package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
@@ -17,17 +17,14 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.api.introduction.IntroductionManager;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -39,16 +36,13 @@ import static android.app.Activity.RESULT_OK;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class IntroductionMessageFragment extends BaseFragment
implements SendListener {
implements TextInputListener {
public static final String TAG =
IntroductionMessageFragment.class.getName();
@@ -78,6 +72,11 @@ public class IntroductionMessageFragment extends BaseFragment
return fragment;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@@ -85,14 +84,8 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
// change toolbar text
ActionBar actionBar = introductionActivity.getSupportActionBar();
@@ -100,26 +93,11 @@ public class IntroductionMessageFragment extends BaseFragment
actionBar.setTitle(R.string.introduction_message_title);
}
// get contact IDs from fragment arguments
Bundle args = requireNonNull(getArguments());
int contactId1 = args.getInt(CONTACT_ID_1, -1);
int contactId2 = args.getInt(CONTACT_ID_2, -1);
if (contactId1 == -1 || contactId2 == -1) {
throw new AssertionError("Use newInstance() to instantiate");
}
// inflate view
View v = inflater.inflate(R.layout.introduction_message, container,
false);
ui = new ViewHolder(v);
TextSendController sendController =
new TextSendController(ui.message, this, true);
ui.message.setSendController(sendController);
ui.message.setMaxTextLength(MAX_INTRODUCTION_TEXT_LENGTH);
ui.message.setEnabled(false);
// get contacts and then show view
prepareToSetUpViews(contactId1, contactId2);
ui.message.setSendButtonEnabled(false);
return v;
}
@@ -127,6 +105,16 @@ public class IntroductionMessageFragment extends BaseFragment
@Override
public void onStart() {
super.onStart();
// get contact IDs from fragment arguments
int contactId1 = getArguments().getInt(CONTACT_ID_1, -1);
int contactId2 = getArguments().getInt(CONTACT_ID_2, -1);
if (contactId1 == -1 || contactId2 == -1) {
throw new java.lang.InstantiationError(
"You need to use newInstance() to instantiate");
}
// get contacts and then show view
prepareToSetUpViews(contactId1, contactId2);
}
@Override
@@ -168,10 +156,13 @@ public class IntroductionMessageFragment extends BaseFragment
ui.progressBar.setVisibility(GONE);
if (possible) {
// set button action
ui.message.setListener(IntroductionMessageFragment.this);
// show views
ui.notPossible.setVisibility(GONE);
ui.message.setVisibility(VISIBLE);
ui.message.setEnabled(true);
ui.message.setSendButtonEnabled(true);
ui.message.showSoftKeyboard();
} else {
ui.notPossible.setVisibility(VISIBLE);
@@ -193,11 +184,14 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@NonNull String text) {
// disable button to prevent accidental double invitations
ui.message.setEnabled(false);
ui.message.setSendButtonEnabled(false);
makeIntroduction(contact1, contact2, text);
String txt = ui.message.getText().toString();
if (txt.isEmpty()) txt = null;
else txt = StringUtils.truncateUtf8(txt, MAX_INTRODUCTION_TEXT_LENGTH);
makeIntroduction(contact1, contact2, txt);
// don't wait for the introduction to be made before finishing activity
introductionActivity.hideSoftKeyboard(ui.message);

View File

@@ -44,11 +44,6 @@ public class ContactExchangeErrorFragment extends BaseFragment {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@@ -79,6 +74,11 @@ public class ContactExchangeErrorFragment extends BaseFragment {
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
private void triggerFeedback() {
finish();
UiUtils.triggerFeedback(androidExecutor);

View File

@@ -10,6 +10,7 @@ import android.widget.ScrollView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
@@ -38,6 +39,11 @@ public class IntroFragment extends BaseFragment {
return fragment;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);

View File

@@ -53,7 +53,6 @@ import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -137,8 +136,7 @@ public class KeyAgreementFragment extends BaseEventFragment
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
requireNonNull(getActivity())
.setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
cameraView.setPreviewConsumer(new QrCodeDecoder(this));
}

View File

@@ -18,7 +18,6 @@ import javax.annotation.Nullable;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.setError;
@@ -36,16 +35,11 @@ public class AuthorNameFragment extends SetupFragment {
return new AuthorNameFragment();
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(getString(R.string.setup_title));
getActivity().setTitle(getString(R.string.setup_title));
View v = inflater.inflate(R.layout.fragment_setup_author_name,
container, false);
authorNameWrapper = v.findViewById(R.id.nickname_entry_wrapper);
@@ -63,6 +57,11 @@ public class AuthorNameFragment extends SetupFragment {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
protected String getHelpText() {
return getString(R.string.setup_name_explanation);

View File

@@ -19,7 +19,6 @@ import org.briarproject.briar.android.util.UiUtils;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@@ -40,16 +39,11 @@ public class DozeFragment extends SetupFragment
return new DozeFragment();
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(getString(R.string.setup_doze_title));
getActivity().setTitle(getString(R.string.setup_doze_title));
setHasOptionsMenu(false);
View v = inflater.inflate(R.layout.fragment_setup_doze, container,
false);
@@ -71,6 +65,11 @@ public class DozeFragment extends SetupFragment
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
protected String getHelpText() {
return getString(R.string.setup_doze_explanation);

View File

@@ -23,7 +23,6 @@ import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
@MethodsNotNullByDefault
@@ -44,16 +43,11 @@ public class PasswordFragment extends SetupFragment {
return new PasswordFragment();
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(getString(R.string.setup_password_intro));
getActivity().setTitle(getString(R.string.setup_password_intro));
View v = inflater.inflate(R.layout.fragment_setup_password, container,
false);
@@ -70,11 +64,6 @@ public class PasswordFragment extends SetupFragment {
passwordConfirmation.addTextChangedListener(this);
nextButton.setOnClickListener(this);
if (!setupController.needToShowDozeFragment()) {
nextButton.setText(R.string.create_account_button);
passwordConfirmation.setImeOptions(IME_ACTION_DONE);
}
return v;
}
@@ -83,6 +72,17 @@ public class PasswordFragment extends SetupFragment {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
// the controller is not yet available in onCreateView()
if (!setupController.needToShowDozeFragment()) {
nextButton.setText(R.string.create_account_button);
passwordConfirmation.setImeOptions(IME_ACTION_DONE);
}
}
@Override
protected String getHelpText() {
return getString(R.string.setup_password_explanation);

View File

@@ -11,8 +11,7 @@ import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
@@ -23,8 +22,7 @@ import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@NotNullByDefault
abstract class SetupFragment extends BaseFragment implements TextWatcher,
OnEditorActionListener, OnClickListener {

View File

@@ -1,13 +1,14 @@
package org.briarproject.briar.android.logout;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import java.util.logging.Logger;
import static android.os.Build.VERSION.SDK_INT;
public class ExitActivity extends Activity {
public class ExitActivity extends BaseActivity {
private static final Logger LOG =
Logger.getLogger(ExitActivity.class.getName());
@@ -15,9 +16,14 @@ public class ExitActivity extends Activity {
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
if (SDK_INT >= 21) finishAndRemoveTask();
if (Build.VERSION.SDK_INT >= 21) finishAndRemoveTask();
else finish();
LOG.info("Exiting");
System.exit(0);
}
@Override
public void injectActivity(ActivityComponent component) {
}
}

View File

@@ -1,13 +1,20 @@
package org.briarproject.briar.android.logout;
import android.app.Activity;
import android.os.Bundle;
public class HideUiActivity extends Activity {
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
public class HideUiActivity extends BaseActivity {
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
finish();
}
@Override
public void injectActivity(ActivityComponent component) {
}
}

View File

@@ -5,21 +5,19 @@ 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 org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SignOutFragment extends BaseFragment {
public static final String TAG = SignOutFragment.class.getName();
@Override
public View onCreateView(LayoutInflater inflater,
public View onCreateView(@Nonnull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_sign_out, container, false);
@@ -29,4 +27,9 @@ public class SignOutFragment extends BaseFragment {
public String getUniqueTag() {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
// no need to inject
}
}

View File

@@ -5,7 +5,6 @@ import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.NavigationView;
import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener;
import android.support.v4.app.ActivityCompat;
@@ -13,7 +12,6 @@ import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
@@ -27,8 +25,6 @@ import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
@@ -58,7 +54,6 @@ import static android.support.v4.view.GravityCompat.START;
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
@@ -66,8 +61,6 @@ import static org.briarproject.briar.android.navdrawer.NavDrawerController.Expir
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE;
import static org.briarproject.briar.android.util.UiUtils.getDaysUntilExpiry;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class NavDrawerActivity extends BriarActivity implements
BaseFragmentListener, TransportStateListener,
OnNavigationItemSelectedListener {
@@ -119,8 +112,9 @@ public class NavDrawerActivity extends BriarActivity implements
component.inject(this);
}
@SuppressWarnings("ConstantConditions")
@Override
public void onCreate(@Nullable Bundle state) {
public void onCreate(Bundle state) {
super.onCreate(state);
exitIfStartupFailed(getIntent());
setContentView(R.layout.activity_nav_drawer);
@@ -131,9 +125,8 @@ public class NavDrawerActivity extends BriarActivity implements
GridView transportsView = findViewById(R.id.transportsView);
setSupportActionBar(toolbar);
ActionBar actionBar = requireNonNull(getSupportActionBar());
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar,
R.string.nav_drawer_open_description,
@@ -172,8 +165,7 @@ public class NavDrawerActivity extends BriarActivity implements
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_PASSWORD && result == RESULT_OK) {
controller.shouldAskForDozeWhitelisting(this,
@@ -223,7 +215,6 @@ public class NavDrawerActivity extends BriarActivity implements
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
controller.countFakes();
drawerLayout.closeDrawer(START);
clearBackStack();
if (item.getItemId() == R.id.nav_btn_lock) {
@@ -262,7 +253,7 @@ public class NavDrawerActivity extends BriarActivity implements
}
@Override
public void onPostCreate(@Nullable Bundle savedInstanceState) {
public void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
drawerToggle.syncState();
}

View File

@@ -21,5 +21,4 @@ public interface NavDrawerController extends ActivityLifecycleController {
void shouldAskForDozeWhitelisting(Context ctx,
ResultHandler<Boolean> handler);
void countFakes();
}

View File

@@ -3,8 +3,6 @@ package org.briarproject.briar.android.navdrawer;
import android.app.Activity;
import android.content.Context;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
@@ -23,9 +21,6 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -54,7 +49,6 @@ public class NavDrawerControllerImpl extends DbControllerImpl
private static final String EXPIRY_DATE_WARNING = "expiryDateWarning";
private static final String EXPIRY_SHOW_UPDATE = "expiryShowUpdate";
private final DatabaseComponent db;
private final PluginManager pluginManager;
private final SettingsManager settingsManager;
private final EventBus eventBus;
@@ -63,11 +57,9 @@ public class NavDrawerControllerImpl extends DbControllerImpl
@Inject
NavDrawerControllerImpl(@DatabaseExecutor Executor dbExecutor,
DatabaseComponent db, LifecycleManager lifecycleManager,
PluginManager pluginManager, SettingsManager settingsManager,
EventBus eventBus) {
LifecycleManager lifecycleManager, PluginManager pluginManager,
SettingsManager settingsManager, EventBus eventBus) {
super(dbExecutor, lifecycleManager);
this.db = db;
this.pluginManager = pluginManager;
this.settingsManager = settingsManager;
this.eventBus = eventBus;
@@ -193,25 +185,6 @@ public class NavDrawerControllerImpl extends DbControllerImpl
});
}
@Override
public void countFakes() {
int numFakes = 1024;
Random random = new Random();
List<byte[]> ids = new ArrayList<>(numFakes);
for (int i = 0; i < numFakes; i++) {
byte[] id = new byte[UniqueId.LENGTH];
random.nextBytes(id);
ids.add(id);
}
runOnDbThread(() -> {
try {
db.transaction(true, txn -> db.countFakes(txn, ids));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@Override
public boolean isTransportRunning(TransportId transportId) {
Plugin plugin = pluginManager.getPlugin(transportId);

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.panic;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.preference.PreferenceManager;
@@ -19,7 +20,6 @@ import info.guardianproject.panic.Panic;
import info.guardianproject.panic.PanicResponder;
import info.guardianproject.trustedintents.TrustedIntents;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.panic.PanicPreferencesFragment.KEY_LOCK;
import static org.briarproject.briar.android.panic.PanicPreferencesFragment.KEY_PURGE;
@@ -69,7 +69,7 @@ public class PanicResponderActivity extends BriarActivity {
}
}
if (SDK_INT >= 21) {
if (Build.VERSION.SDK_INT >= 21) {
finishAndRemoveTask();
} else {
finish();

View File

@@ -196,7 +196,7 @@ public class GroupActivity extends
private void setGroupEnabled(boolean enabled) {
isDissolved = !enabled;
textInput.setEnabled(enabled);
textInput.setSendButtonEnabled(enabled);
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
if (!enabled) {

View File

@@ -1,7 +1,5 @@
package org.briarproject.briar.android.privategroup.creation;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -20,7 +18,6 @@ public interface CreateGroupController
ResultExceptionHandler<GroupId, DbException> result);
void sendInvitation(GroupId g, Collection<ContactId> contacts,
@Nullable String text,
ResultExceptionHandler<Void, DbException> result);
String text, ResultExceptionHandler<Void, DbException> result);
}

View File

@@ -1,7 +1,5 @@
package org.briarproject.briar.android.privategroup.creation;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -125,8 +123,7 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
@Override
public void sendInvitation(GroupId g, Collection<ContactId> contactIds,
@Nullable String text,
ResultExceptionHandler<Void, DbException> handler) {
String text, ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
@@ -147,7 +144,7 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
}
private void signInvitations(GroupId g, LocalAuthor localAuthor,
Collection<Contact> contacts, @Nullable String text,
Collection<Contact> contacts, String text,
ResultExceptionHandler<Void, DbException> handler) {
cryptoExecutor.execute(() -> {
long timestamp = clock.currentTimeMillis();
@@ -163,14 +160,15 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
}
private void sendInvitations(GroupId g,
Collection<InvitationContext> contexts, @Nullable String text,
Collection<InvitationContext> contexts, String text,
ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> {
try {
String txt = text.isEmpty() ? null : text;
for (InvitationContext context : contexts) {
try {
groupInvitationManager.sendInvitation(g,
context.contactId, text, context.timestamp,
context.contactId, txt, context.timestamp,
context.signature);
} catch (NoSuchContactException e) {
// Continue

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.privategroup.creation;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.text.TextWatcher;
@@ -13,8 +12,6 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@@ -26,8 +23,6 @@ import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class CreateGroupFragment extends BaseFragment {
public final static String TAG = CreateGroupFragment.class.getName();
@@ -45,13 +40,8 @@ public class CreateGroupFragment extends BaseFragment {
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_create_group, container,
false);
@@ -97,6 +87,11 @@ public class CreateGroupFragment extends BaseFragment {
listener.showSoftKeyboard(nameEntry);
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public String getUniqueTag() {
return TAG;

View File

@@ -2,13 +2,10 @@ package org.briarproject.briar.android.privategroup.creation;
import android.support.annotation.StringRes;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.sharing.BaseMessageFragment;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class CreateGroupMessageFragment extends BaseMessageFragment {
private final static String TAG =
@@ -31,4 +28,9 @@ public class CreateGroupMessageFragment extends BaseMessageFragment {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
}

View File

@@ -55,7 +55,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
}
@Override
public void onButtonClick(@Nullable String text) {
public boolean onButtonClick(String text) {
if (groupId == null)
throw new IllegalStateException("GroupId was not initialized");
controller.sendInvitation(groupId, contacts, text,
@@ -72,6 +72,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
handleDbException(exception);
}
});
return true;
}
@Override

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.privategroup.creation;
import android.os.Bundle;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -14,7 +13,6 @@ import org.briarproject.briar.android.contactselection.SelectableContactItem;
import javax.inject.Inject;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@MethodsNotNullByDefault
@@ -35,14 +33,14 @@ public class GroupInviteFragment extends ContactSelectorFragment {
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActivity().setTitle(R.string.groups_invite_members);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requireNonNull(getActivity()).setTitle(R.string.groups_invite_members);
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override

View File

@@ -37,7 +37,6 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -58,23 +57,17 @@ public class GroupListFragment extends BaseFragment implements
private GroupListAdapter adapter;
private Snackbar snackbar;
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
controller.setGroupListListener(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireNonNull(getActivity()).setTitle(R.string.groups_button);
getActivity().setTitle(R.string.groups_button);
View v = inflater.inflate(R.layout.list, container, false);
adapter = new GroupListAdapter(getActivity(), this);
adapter = new GroupListAdapter(getContext(), this);
list = v.findViewById(R.id.list);
list.setEmptyImage(R.drawable.ic_empty_state_group_list);
list.setEmptyText(R.string.groups_list_empty);
@@ -86,11 +79,17 @@ public class GroupListFragment extends BaseFragment implements
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setAction(R.string.show, this);
snackbar.setActionTextColor(ContextCompat
.getColor(getActivity(), R.color.briar_button_text_positive));
.getColor(getContext(), R.color.briar_button_text_positive));
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
controller.setGroupListListener(this);
}
@Override
public void onStart() {
super.onStart();

View File

@@ -42,7 +42,6 @@ import static android.content.Context.WIFI_SERVICE;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
public class BriarReportPrimer implements ReportPrimer {
@@ -193,7 +192,7 @@ public class BriarReportPrimer implements ReportPrimer {
int ip3 = (ip >> 16) & 0xFF;
int ip4 = (ip >> 24) & 0xFF;
String address = ip1 + "." + ip2 + "." + ip3 + "." + ip4;
customData.put("Wi-Fi address", scrubInetAddress(address));
customData.put("Wi-Fi address", address);
}
}

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