mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 11:19:04 +01:00
Compare commits
30 Commits
beta-0.16.
...
beta-0.16.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
812522a900 | ||
|
|
98db9da4bc | ||
|
|
eda3c964aa | ||
|
|
68df606146 | ||
|
|
52bd699d2d | ||
|
|
abb8db10db | ||
|
|
30edb90426 | ||
|
|
ffc94b2812 | ||
|
|
35a7bb4576 | ||
|
|
2d87e34aa2 | ||
|
|
088564f22f | ||
|
|
8c8c1158f4 | ||
|
|
8faa456eb2 | ||
|
|
4c61158326 | ||
|
|
6792abc00a | ||
|
|
63442aea1d | ||
|
|
a58443eaa8 | ||
|
|
14a9614c35 | ||
|
|
f1011b97b3 | ||
|
|
1935b1e09a | ||
|
|
ac9df9d5d8 | ||
|
|
30a800a4d0 | ||
|
|
69537b67a2 | ||
|
|
92982f98a8 | ||
|
|
ea5fa72224 | ||
|
|
5a1651d483 | ||
|
|
fcbf6dfb7f | ||
|
|
7aebf92a6f | ||
|
|
1b9f8d4f0b | ||
|
|
93db4eb986 |
@@ -12,8 +12,8 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 26
|
targetSdkVersion 26
|
||||||
versionCode 1617
|
versionCode 1618
|
||||||
versionName "0.16.17"
|
versionName "0.16.18"
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.briarproject.bramble.api.db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the database uses a newer schema than the current code.
|
||||||
|
*/
|
||||||
|
public class DataTooNewException extends DbException {
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.briarproject.bramble.api.db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the database uses an older schema than the current code and
|
||||||
|
* cannot be migrated.
|
||||||
|
*/
|
||||||
|
public class DataTooOldException extends DbException {
|
||||||
|
}
|
||||||
@@ -37,6 +37,11 @@ public interface DatabaseComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the database and returns true if the database already existed.
|
* Opens the database and returns true if the database already existed.
|
||||||
|
*
|
||||||
|
* @throws DataTooNewException if the data uses a newer schema than the
|
||||||
|
* current code
|
||||||
|
* @throws DataTooOldException if the data uses an older schema than the
|
||||||
|
* current code and cannot be migrated
|
||||||
*/
|
*/
|
||||||
boolean open() throws DbException;
|
boolean open() throws DbException;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package org.briarproject.bramble.db;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.db.DataTooNewException;
|
||||||
|
import org.briarproject.bramble.api.db.DataTooOldException;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.Metadata;
|
import org.briarproject.bramble.api.db.Metadata;
|
||||||
import org.briarproject.bramble.api.identity.Author;
|
import org.briarproject.bramble.api.identity.Author;
|
||||||
@@ -37,6 +39,11 @@ interface Database<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the database and returns true if the database already existed.
|
* Opens the database and returns true if the database already existed.
|
||||||
|
*
|
||||||
|
* @throws DataTooNewException if the data uses a newer schema than the
|
||||||
|
* current code
|
||||||
|
* @throws DataTooOldException if the data uses an older schema than the
|
||||||
|
* current code and cannot be migrated
|
||||||
*/
|
*/
|
||||||
boolean open() throws DbException;
|
boolean open() throws DbException;
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,4 @@ interface DatabaseConstants {
|
|||||||
*/
|
*/
|
||||||
String SCHEMA_VERSION_KEY = "schemaVersion";
|
String SCHEMA_VERSION_KEY = "schemaVersion";
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link Settings} key under which the minimum supported database
|
|
||||||
* schema version is stored.
|
|
||||||
*/
|
|
||||||
String MIN_SCHEMA_VERSION_KEY = "minSchemaVersion";
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.briarproject.bramble.db;
|
|||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
|
import org.briarproject.bramble.api.db.DataTooNewException;
|
||||||
|
import org.briarproject.bramble.api.db.DataTooOldException;
|
||||||
import org.briarproject.bramble.api.db.DbClosedException;
|
import org.briarproject.bramble.api.db.DbClosedException;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.Metadata;
|
import org.briarproject.bramble.api.db.Metadata;
|
||||||
@@ -47,6 +49,7 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
@@ -57,7 +60,6 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
|
|||||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
|
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
|
||||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
|
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.MIN_SCHEMA_VERSION_KEY;
|
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
||||||
import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
|
import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
|
||||||
|
|
||||||
@@ -68,8 +70,8 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
abstract class JdbcDatabase implements Database<Connection> {
|
abstract class JdbcDatabase implements Database<Connection> {
|
||||||
|
|
||||||
private static final int SCHEMA_VERSION = 30;
|
// Package access for testing
|
||||||
private static final int MIN_SCHEMA_VERSION = 30;
|
static final int CODE_SCHEMA_VERSION = 31;
|
||||||
|
|
||||||
private static final String CREATE_SETTINGS =
|
private static final String CREATE_SETTINGS =
|
||||||
"CREATE TABLE settings"
|
"CREATE TABLE settings"
|
||||||
@@ -148,11 +150,16 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
private static final String CREATE_MESSAGE_METADATA =
|
private static final String CREATE_MESSAGE_METADATA =
|
||||||
"CREATE TABLE messageMetadata"
|
"CREATE TABLE messageMetadata"
|
||||||
+ " (messageId HASH NOT NULL,"
|
+ " (messageId HASH NOT NULL,"
|
||||||
|
+ " groupId HASH NOT NULL," // Denormalised
|
||||||
|
+ " state INT NOT NULL," // Denormalised
|
||||||
+ " key VARCHAR NOT NULL,"
|
+ " key VARCHAR NOT NULL,"
|
||||||
+ " value BINARY NOT NULL,"
|
+ " value BINARY NOT NULL,"
|
||||||
+ " PRIMARY KEY (messageId, key),"
|
+ " PRIMARY KEY (messageId, key),"
|
||||||
+ " FOREIGN KEY (messageId)"
|
+ " FOREIGN KEY (messageId)"
|
||||||
+ " REFERENCES messages (messageId)"
|
+ " REFERENCES messages (messageId)"
|
||||||
|
+ " ON DELETE CASCADE,"
|
||||||
|
+ " FOREIGN KEY (groupId)"
|
||||||
|
+ " REFERENCES groups (groupId)"
|
||||||
+ " ON DELETE CASCADE)";
|
+ " ON DELETE CASCADE)";
|
||||||
|
|
||||||
private static final String CREATE_MESSAGE_DEPENDENCIES =
|
private static final String CREATE_MESSAGE_DEPENDENCIES =
|
||||||
@@ -236,25 +243,13 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
"CREATE INDEX IF NOT EXISTS contactsByAuthorId"
|
"CREATE INDEX IF NOT EXISTS contactsByAuthorId"
|
||||||
+ " ON contacts (authorId)";
|
+ " ON contacts (authorId)";
|
||||||
|
|
||||||
private static final String INDEX_MESSAGES_BY_GROUP_ID =
|
|
||||||
"CREATE INDEX IF NOT EXISTS messagesByGroupId"
|
|
||||||
+ " ON messages (groupId)";
|
|
||||||
|
|
||||||
private static final String INDEX_OFFERS_BY_CONTACT_ID =
|
|
||||||
"CREATE INDEX IF NOT EXISTS offersByContactId"
|
|
||||||
+ " ON offers (contactId)";
|
|
||||||
|
|
||||||
private static final String INDEX_GROUPS_BY_CLIENT_ID =
|
private static final String INDEX_GROUPS_BY_CLIENT_ID =
|
||||||
"CREATE INDEX IF NOT EXISTS groupsByClientId"
|
"CREATE INDEX IF NOT EXISTS groupsByClientId"
|
||||||
+ " ON groups (clientId)";
|
+ " ON groups (clientId)";
|
||||||
|
|
||||||
private static final String INDEX_MESSAGE_METADATA_BY_MESSAGE_ID =
|
private static final String INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE =
|
||||||
"CREATE INDEX IF NOT EXISTS messageMetadataByMessageId"
|
"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
|
||||||
+ " ON messageMetadata (messageId)";
|
+ " ON messageMetadata (groupId, state)";
|
||||||
|
|
||||||
private static final String INDEX_GROUP_METADATA_BY_GROUP_ID =
|
|
||||||
"CREATE INDEX IF NOT EXISTS groupMetadataByGroupId"
|
|
||||||
+ " ON groupMetadata (groupId)";
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(JdbcDatabase.class.getName());
|
Logger.getLogger(JdbcDatabase.class.getName());
|
||||||
@@ -295,10 +290,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
Connection txn = startTransaction();
|
Connection txn = startTransaction();
|
||||||
try {
|
try {
|
||||||
if (reopen) {
|
if (reopen) {
|
||||||
if (!checkSchemaVersion(txn)) throw new DbException();
|
checkSchemaVersion(txn);
|
||||||
} else {
|
} else {
|
||||||
createTables(txn);
|
createTables(txn);
|
||||||
storeSchemaVersion(txn);
|
storeSchemaVersion(txn, CODE_SCHEMA_VERSION);
|
||||||
}
|
}
|
||||||
createIndexes(txn);
|
createIndexes(txn);
|
||||||
commitTransaction(txn);
|
commitTransaction(txn);
|
||||||
@@ -308,19 +303,49 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkSchemaVersion(Connection txn) throws DbException {
|
/**
|
||||||
|
* Compares the schema version stored in the database with the schema
|
||||||
|
* version used by the current code and applies any suitable migrations to
|
||||||
|
* the data if necessary.
|
||||||
|
*
|
||||||
|
* @throws DataTooNewException if the data uses a newer schema than the
|
||||||
|
* current code
|
||||||
|
* @throws DataTooOldException if the data uses an older schema than the
|
||||||
|
* current code and cannot be migrated
|
||||||
|
*/
|
||||||
|
private void checkSchemaVersion(Connection txn) throws DbException {
|
||||||
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
||||||
int schemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1);
|
int dataSchemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1);
|
||||||
if (schemaVersion == SCHEMA_VERSION) return true;
|
if (dataSchemaVersion == -1) throw new DbException();
|
||||||
if (schemaVersion < MIN_SCHEMA_VERSION) return false;
|
if (dataSchemaVersion == CODE_SCHEMA_VERSION) return;
|
||||||
int minSchemaVersion = s.getInt(MIN_SCHEMA_VERSION_KEY, -1);
|
if (CODE_SCHEMA_VERSION < dataSchemaVersion)
|
||||||
return SCHEMA_VERSION >= minSchemaVersion;
|
throw new DataTooNewException();
|
||||||
|
// Apply any suitable migrations in order
|
||||||
|
for (Migration<Connection> m : getMigrations()) {
|
||||||
|
int start = m.getStartVersion(), end = m.getEndVersion();
|
||||||
|
if (start == dataSchemaVersion) {
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Migrating from schema " + start + " to " + end);
|
||||||
|
// Apply the migration
|
||||||
|
m.migrate(txn);
|
||||||
|
// Store the new schema version
|
||||||
|
storeSchemaVersion(txn, end);
|
||||||
|
dataSchemaVersion = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dataSchemaVersion != CODE_SCHEMA_VERSION)
|
||||||
|
throw new DataTooOldException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storeSchemaVersion(Connection txn) throws DbException {
|
// Package access for testing
|
||||||
|
List<Migration<Connection>> getMigrations() {
|
||||||
|
return Collections.singletonList(new Migration30_31());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeSchemaVersion(Connection txn, int version)
|
||||||
|
throws DbException {
|
||||||
Settings s = new Settings();
|
Settings s = new Settings();
|
||||||
s.putInt(SCHEMA_VERSION_KEY, SCHEMA_VERSION);
|
s.putInt(SCHEMA_VERSION_KEY, version);
|
||||||
s.putInt(MIN_SCHEMA_VERSION_KEY, MIN_SCHEMA_VERSION);
|
|
||||||
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,11 +395,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
try {
|
try {
|
||||||
s = txn.createStatement();
|
s = txn.createStatement();
|
||||||
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
|
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
|
||||||
s.executeUpdate(INDEX_MESSAGES_BY_GROUP_ID);
|
|
||||||
s.executeUpdate(INDEX_OFFERS_BY_CONTACT_ID);
|
|
||||||
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
|
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
|
||||||
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_MESSAGE_ID);
|
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
|
||||||
s.executeUpdate(INDEX_GROUP_METADATA_BY_GROUP_ID);
|
|
||||||
s.close();
|
s.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(s);
|
tryToClose(s);
|
||||||
@@ -500,7 +522,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
try {
|
try {
|
||||||
// Create a contact row
|
// Create a contact row
|
||||||
String sql = "INSERT INTO contacts"
|
String sql = "INSERT INTO contacts"
|
||||||
+ " (authorId, name, publicKey, localAuthorId, verified, active)"
|
+ " (authorId, name, publicKey, localAuthorId,"
|
||||||
|
+ " verified, active)"
|
||||||
+ " VALUES (?, ?, ?, ?, ?, ?)";
|
+ " VALUES (?, ?, ?, ?, ?, ?)";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, remote.getId().getBytes());
|
ps.setBytes(1, remote.getId().getBytes());
|
||||||
@@ -1330,16 +1353,13 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
try {
|
try {
|
||||||
// Retrieve the message IDs for each query term and intersect
|
// Retrieve the message IDs for each query term and intersect
|
||||||
Set<MessageId> intersection = null;
|
Set<MessageId> intersection = null;
|
||||||
String sql = "SELECT m.messageId"
|
String sql = "SELECT messageId FROM messageMetadata"
|
||||||
+ " FROM messages AS m"
|
+ " WHERE groupId = ? AND state = ?"
|
||||||
+ " JOIN messageMetadata AS md"
|
|
||||||
+ " ON m.messageId = md.messageId"
|
|
||||||
+ " WHERE state = ? AND groupId = ?"
|
|
||||||
+ " AND key = ? AND value = ?";
|
+ " AND key = ? AND value = ?";
|
||||||
for (Entry<String, byte[]> e : query.entrySet()) {
|
for (Entry<String, byte[]> e : query.entrySet()) {
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setBytes(1, g.getBytes());
|
||||||
ps.setBytes(2, g.getBytes());
|
ps.setInt(2, DELIVERED.getValue());
|
||||||
ps.setString(3, e.getKey());
|
ps.setString(3, e.getKey());
|
||||||
ps.setBytes(4, e.getValue());
|
ps.setBytes(4, e.getValue());
|
||||||
rs = ps.executeQuery();
|
rs = ps.executeQuery();
|
||||||
@@ -1367,25 +1387,20 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
ResultSet rs = null;
|
ResultSet rs = null;
|
||||||
try {
|
try {
|
||||||
String sql = "SELECT m.messageId, key, value"
|
String sql = "SELECT messageId, key, value"
|
||||||
+ " FROM messages AS m"
|
+ " FROM messageMetadata"
|
||||||
+ " JOIN messageMetadata AS md"
|
+ " WHERE groupId = ? AND state = ?";
|
||||||
+ " ON m.messageId = md.messageId"
|
|
||||||
+ " WHERE state = ? AND groupId = ?"
|
|
||||||
+ " ORDER BY m.messageId";
|
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setBytes(1, g.getBytes());
|
||||||
ps.setBytes(2, g.getBytes());
|
ps.setInt(2, DELIVERED.getValue());
|
||||||
rs = ps.executeQuery();
|
rs = ps.executeQuery();
|
||||||
Map<MessageId, Metadata> all = new HashMap<>();
|
Map<MessageId, Metadata> all = new HashMap<>();
|
||||||
Metadata metadata = null;
|
|
||||||
MessageId lastMessageId = null;
|
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
MessageId messageId = new MessageId(rs.getBytes(1));
|
MessageId messageId = new MessageId(rs.getBytes(1));
|
||||||
if (lastMessageId == null || !messageId.equals(lastMessageId)) {
|
Metadata metadata = all.get(messageId);
|
||||||
|
if (metadata == null) {
|
||||||
metadata = new Metadata();
|
metadata = new Metadata();
|
||||||
all.put(messageId, metadata);
|
all.put(messageId, metadata);
|
||||||
lastMessageId = messageId;
|
|
||||||
}
|
}
|
||||||
metadata.put(rs.getString(2), rs.getBytes(3));
|
metadata.put(rs.getString(2), rs.getBytes(3));
|
||||||
}
|
}
|
||||||
@@ -1440,10 +1455,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
ResultSet rs = null;
|
ResultSet rs = null;
|
||||||
try {
|
try {
|
||||||
String sql = "SELECT key, value FROM messageMetadata AS md"
|
String sql = "SELECT key, value FROM messageMetadata"
|
||||||
+ " JOIN messages AS m"
|
+ " WHERE state = ? AND messageId = ?";
|
||||||
+ " ON m.messageId = md.messageId"
|
|
||||||
+ " WHERE m.state = ? AND md.messageId = ?";
|
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setInt(1, DELIVERED.getValue());
|
||||||
ps.setBytes(2, m.getBytes());
|
ps.setBytes(2, m.getBytes());
|
||||||
@@ -1466,11 +1479,9 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
ResultSet rs = null;
|
ResultSet rs = null;
|
||||||
try {
|
try {
|
||||||
String sql = "SELECT key, value FROM messageMetadata AS md"
|
String sql = "SELECT key, value FROM messageMetadata"
|
||||||
+ " JOIN messages AS m"
|
+ " WHERE (state = ? OR state = ?)"
|
||||||
+ " ON m.messageId = md.messageId"
|
+ " AND messageId = ?";
|
||||||
+ " WHERE (m.state = ? OR m.state = ?)"
|
|
||||||
+ " AND md.messageId = ?";
|
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setInt(1, DELIVERED.getValue());
|
||||||
ps.setInt(2, PENDING.getValue());
|
ps.setInt(2, PENDING.getValue());
|
||||||
@@ -2044,7 +2055,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
int[] batchAffected = ps.executeBatch();
|
int[] batchAffected = ps.executeBatch();
|
||||||
if (batchAffected.length != requested.size())
|
if (batchAffected.length != requested.size())
|
||||||
throw new DbStateException();
|
throw new DbStateException();
|
||||||
for (int rows: batchAffected) {
|
for (int rows : batchAffected) {
|
||||||
if (rows < 0) throw new DbStateException();
|
if (rows < 0) throw new DbStateException();
|
||||||
if (rows > 1) throw new DbStateException();
|
if (rows > 1) throw new DbStateException();
|
||||||
}
|
}
|
||||||
@@ -2058,25 +2069,92 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
@Override
|
@Override
|
||||||
public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
|
public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
mergeMetadata(txn, g.getBytes(), meta, "groupMetadata", "groupId");
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
Map<String, byte[]> added = removeOrUpdateMetadata(txn,
|
||||||
|
g.getBytes(), meta, "groupMetadata", "groupId");
|
||||||
|
if (added.isEmpty()) return;
|
||||||
|
// Insert any keys that don't already exist
|
||||||
|
String sql = "INSERT INTO groupMetadata (groupId, key, value)"
|
||||||
|
+ " VALUES (?, ?, ?)";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, g.getBytes());
|
||||||
|
for (Entry<String, byte[]> e : added.entrySet()) {
|
||||||
|
ps.setString(2, e.getKey());
|
||||||
|
ps.setBytes(3, e.getValue());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
int[] batchAffected = ps.executeBatch();
|
||||||
|
if (batchAffected.length != added.size())
|
||||||
|
throw new DbStateException();
|
||||||
|
for (int rows : batchAffected)
|
||||||
|
if (rows != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(ps);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta)
|
public void mergeMessageMetadata(Connection txn, MessageId m,
|
||||||
throws DbException {
|
Metadata meta) throws DbException {
|
||||||
mergeMetadata(txn, m.getBytes(), meta, "messageMetadata", "messageId");
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
Map<String, byte[]> added = removeOrUpdateMetadata(txn,
|
||||||
|
m.getBytes(), meta, "messageMetadata", "messageId");
|
||||||
|
if (added.isEmpty()) return;
|
||||||
|
// Get the group ID and message state for the denormalised columns
|
||||||
|
String sql = "SELECT groupId, state FROM messages"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
GroupId g = new GroupId(rs.getBytes(1));
|
||||||
|
State state = State.fromValue(rs.getInt(2));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
// Insert any keys that don't already exist
|
||||||
|
sql = "INSERT INTO messageMetadata"
|
||||||
|
+ " (messageId, groupId, state, key, value)"
|
||||||
|
+ " VALUES (?, ?, ?, ?, ?)";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
ps.setBytes(2, g.getBytes());
|
||||||
|
ps.setInt(3, state.getValue());
|
||||||
|
for (Entry<String, byte[]> e : added.entrySet()) {
|
||||||
|
ps.setString(4, e.getKey());
|
||||||
|
ps.setBytes(5, e.getValue());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
int[] batchAffected = ps.executeBatch();
|
||||||
|
if (batchAffected.length != added.size())
|
||||||
|
throw new DbStateException();
|
||||||
|
for (int rows : batchAffected)
|
||||||
|
if (rows != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs);
|
||||||
|
tryToClose(ps);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeMetadata(Connection txn, byte[] id, Metadata meta,
|
// Removes or updates any existing entries, returns any entries that
|
||||||
String tableName, String columnName) throws DbException {
|
// need to be added
|
||||||
|
private Map<String, byte[]> removeOrUpdateMetadata(Connection txn,
|
||||||
|
byte[] id, Metadata meta, String tableName, String columnName)
|
||||||
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
// Determine which keys are being removed
|
// Determine which keys are being removed
|
||||||
List<String> removed = new ArrayList<>();
|
List<String> removed = new ArrayList<>();
|
||||||
Map<String, byte[]> retained = new HashMap<>();
|
Map<String, byte[]> notRemoved = new HashMap<>();
|
||||||
for (Entry<String, byte[]> e : meta.entrySet()) {
|
for (Entry<String, byte[]> e : meta.entrySet()) {
|
||||||
if (e.getValue() == REMOVE) removed.add(e.getKey());
|
if (e.getValue() == REMOVE) removed.add(e.getKey());
|
||||||
else retained.put(e.getKey(), e.getValue());
|
else notRemoved.put(e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
// Delete any keys that are being removed
|
// Delete any keys that are being removed
|
||||||
if (!removed.isEmpty()) {
|
if (!removed.isEmpty()) {
|
||||||
@@ -2097,45 +2175,33 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
ps.close();
|
ps.close();
|
||||||
}
|
}
|
||||||
if (retained.isEmpty()) return;
|
if (notRemoved.isEmpty()) return Collections.emptyMap();
|
||||||
// Update any keys that already exist
|
// Update any keys that already exist
|
||||||
String sql = "UPDATE " + tableName + " SET value = ?"
|
String sql = "UPDATE " + tableName + " SET value = ?"
|
||||||
+ " WHERE " + columnName + " = ? AND key = ?";
|
+ " WHERE " + columnName + " = ? AND key = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(2, id);
|
ps.setBytes(2, id);
|
||||||
for (Entry<String, byte[]> e : retained.entrySet()) {
|
for (Entry<String, byte[]> e : notRemoved.entrySet()) {
|
||||||
ps.setBytes(1, e.getValue());
|
ps.setBytes(1, e.getValue());
|
||||||
ps.setString(3, e.getKey());
|
ps.setString(3, e.getKey());
|
||||||
ps.addBatch();
|
ps.addBatch();
|
||||||
}
|
}
|
||||||
int[] batchAffected = ps.executeBatch();
|
int[] batchAffected = ps.executeBatch();
|
||||||
if (batchAffected.length != retained.size())
|
if (batchAffected.length != notRemoved.size())
|
||||||
throw new DbStateException();
|
throw new DbStateException();
|
||||||
for (int rows : batchAffected) {
|
for (int rows : batchAffected) {
|
||||||
if (rows < 0) throw new DbStateException();
|
if (rows < 0) throw new DbStateException();
|
||||||
if (rows > 1) throw new DbStateException();
|
if (rows > 1) throw new DbStateException();
|
||||||
}
|
}
|
||||||
// Insert any keys that don't already exist
|
|
||||||
sql = "INSERT INTO " + tableName
|
|
||||||
+ " (" + columnName + ", key, value)"
|
|
||||||
+ " VALUES (?, ?, ?)";
|
|
||||||
ps = txn.prepareStatement(sql);
|
|
||||||
ps.setBytes(1, id);
|
|
||||||
int updateIndex = 0, inserted = 0;
|
|
||||||
for (Entry<String, byte[]> e : retained.entrySet()) {
|
|
||||||
if (batchAffected[updateIndex] == 0) {
|
|
||||||
ps.setString(2, e.getKey());
|
|
||||||
ps.setBytes(3, e.getValue());
|
|
||||||
ps.addBatch();
|
|
||||||
inserted++;
|
|
||||||
}
|
|
||||||
updateIndex++;
|
|
||||||
}
|
|
||||||
batchAffected = ps.executeBatch();
|
|
||||||
if (batchAffected.length != inserted) throw new DbStateException();
|
|
||||||
for (int rows : batchAffected)
|
|
||||||
if (rows != 1) throw new DbStateException();
|
|
||||||
ps.close();
|
ps.close();
|
||||||
|
// Are there any keys that don't already exist?
|
||||||
|
Map<String, byte[]> added = new HashMap<>();
|
||||||
|
int updateIndex = 0;
|
||||||
|
for (Entry<String, byte[]> e : notRemoved.entrySet()) {
|
||||||
|
if (batchAffected[updateIndex++] == 0)
|
||||||
|
added.put(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
return added;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(ps);
|
tryToClose(ps);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
@@ -2488,7 +2554,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageShared(Connection txn, MessageId m) throws DbException {
|
public void setMessageShared(Connection txn, MessageId m)
|
||||||
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
String sql = "UPDATE messages SET shared = TRUE"
|
String sql = "UPDATE messages SET shared = TRUE"
|
||||||
@@ -2516,6 +2583,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
|
// Update denormalised column in messageMetadata
|
||||||
|
sql = "UPDATE messageMetadata SET state = ? WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setInt(1, state.getValue());
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
|
affected = ps.executeUpdate();
|
||||||
|
if (affected < 0) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(ps);
|
tryToClose(ps);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
|
||||||
|
interface Migration<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the schema version from which this migration starts.
|
||||||
|
*/
|
||||||
|
int getStartVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the schema version at which this migration ends.
|
||||||
|
*/
|
||||||
|
int getEndVersion();
|
||||||
|
|
||||||
|
void migrate(T txn) throws DbException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
|
class Migration30_31 implements Migration<Connection> {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
Logger.getLogger(Migration30_31.class.getName());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStartVersion() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getEndVersion() {
|
||||||
|
return 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(Connection txn) throws DbException {
|
||||||
|
Statement s = null;
|
||||||
|
try {
|
||||||
|
s = txn.createStatement();
|
||||||
|
// Add groupId column
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ADD COLUMN groupId BINARY(32) AFTER messageId");
|
||||||
|
// Populate groupId column
|
||||||
|
s.execute("UPDATE messageMetadata AS mm SET groupId ="
|
||||||
|
+ " (SELECT groupId FROM messages AS m"
|
||||||
|
+ " WHERE mm.messageId = m.messageId)");
|
||||||
|
// Add not null constraint now column has been populated
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ALTER COLUMN groupId"
|
||||||
|
+ " SET NOT NULL");
|
||||||
|
// Add foreign key constraint
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ADD CONSTRAINT groupIdForeignKey"
|
||||||
|
+ " FOREIGN KEY (groupId)"
|
||||||
|
+ " REFERENCES groups (groupId)"
|
||||||
|
+ " ON DELETE CASCADE");
|
||||||
|
// Add state column
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ADD COLUMN state INT AFTER groupId");
|
||||||
|
// Populate state column
|
||||||
|
s.execute("UPDATE messageMetadata AS mm SET state ="
|
||||||
|
+ " (SELECT state FROM messages AS m"
|
||||||
|
+ " WHERE mm.messageId = m.messageId)");
|
||||||
|
// Add not null constraint now column has been populated
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ALTER COLUMN state"
|
||||||
|
+ " SET NOT NULL");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(s);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryToClose(@Nullable Statement s) {
|
||||||
|
try {
|
||||||
|
if (s != null) s.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
|||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
|
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
|
||||||
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
|
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
|
||||||
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
|
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
|
||||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||||
import org.briarproject.bramble.api.system.Clock;
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
@@ -25,6 +26,7 @@ import java.security.SecureRandom;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@@ -50,7 +52,7 @@ class Poller implements EventListener {
|
|||||||
private final SecureRandom random;
|
private final SecureRandom random;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final Lock lock;
|
private final Lock lock;
|
||||||
private final Map<TransportId, PollTask> tasks; // Locking: lock
|
private final Map<TransportId, ScheduledPollTask> tasks; // Locking: lock
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Poller(@IoExecutor Executor ioExecutor,
|
Poller(@IoExecutor Executor ioExecutor,
|
||||||
@@ -93,6 +95,10 @@ class Poller implements EventListener {
|
|||||||
TransportEnabledEvent t = (TransportEnabledEvent) e;
|
TransportEnabledEvent t = (TransportEnabledEvent) e;
|
||||||
// Poll the newly enabled transport
|
// Poll the newly enabled transport
|
||||||
pollNow(t.getTransportId());
|
pollNow(t.getTransportId());
|
||||||
|
} else if (e instanceof TransportDisabledEvent) {
|
||||||
|
TransportDisabledEvent t = (TransportDisabledEvent) e;
|
||||||
|
// Cancel polling for the disabled transport
|
||||||
|
cancel(t.getTransportId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,18 +157,31 @@ class Poller implements EventListener {
|
|||||||
TransportId t = p.getId();
|
TransportId t = p.getId();
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
PollTask scheduled = tasks.get(t);
|
ScheduledPollTask scheduled = tasks.get(t);
|
||||||
if (scheduled == null || due < scheduled.due) {
|
if (scheduled == null || due < scheduled.task.due) {
|
||||||
|
// If a later task exists, cancel it. If it's already started
|
||||||
|
// it will abort safely when it finds it's been replaced
|
||||||
|
if (scheduled != null) scheduled.future.cancel(false);
|
||||||
PollTask task = new PollTask(p, due, randomiseNext);
|
PollTask task = new PollTask(p, due, randomiseNext);
|
||||||
tasks.put(t, task);
|
Future future = scheduler.schedule(
|
||||||
scheduler.schedule(
|
|
||||||
() -> ioExecutor.execute(task), delay, MILLISECONDS);
|
() -> ioExecutor.execute(task), delay, MILLISECONDS);
|
||||||
|
tasks.put(t, new ScheduledPollTask(task, future));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cancel(TransportId t) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
ScheduledPollTask scheduled = tasks.remove(t);
|
||||||
|
if (scheduled != null) scheduled.future.cancel(false);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@IoExecutor
|
@IoExecutor
|
||||||
private void poll(Plugin p) {
|
private void poll(Plugin p) {
|
||||||
TransportId t = p.getId();
|
TransportId t = p.getId();
|
||||||
@@ -170,6 +189,17 @@ class Poller implements EventListener {
|
|||||||
p.poll(connectionRegistry.getConnectedContacts(t));
|
p.poll(connectionRegistry.getConnectedContacts(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ScheduledPollTask {
|
||||||
|
|
||||||
|
private final PollTask task;
|
||||||
|
private final Future future;
|
||||||
|
|
||||||
|
private ScheduledPollTask(PollTask task, Future future) {
|
||||||
|
this.task = task;
|
||||||
|
this.future = future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class PollTask implements Runnable {
|
private class PollTask implements Runnable {
|
||||||
|
|
||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
@@ -188,7 +218,9 @@ class Poller implements EventListener {
|
|||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
TransportId t = plugin.getId();
|
TransportId t = plugin.getId();
|
||||||
if (tasks.get(t) != this) return; // Replaced by another task
|
ScheduledPollTask scheduled = tasks.get(t);
|
||||||
|
if (scheduled != null && scheduled.task != this)
|
||||||
|
return; // Replaced by another task
|
||||||
tasks.remove(t);
|
tasks.remove(t);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.net.SocketAddress;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@@ -44,6 +44,9 @@ class LanTcpPlugin extends TcpPlugin {
|
|||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(LanTcpPlugin.class.getName());
|
Logger.getLogger(LanTcpPlugin.class.getName());
|
||||||
|
|
||||||
|
private static final LanAddressComparator ADDRESS_COMPARATOR =
|
||||||
|
new LanAddressComparator();
|
||||||
|
|
||||||
private static final int MAX_ADDRESSES = 4;
|
private static final int MAX_ADDRESSES = 4;
|
||||||
private static final String SEPARATOR = ",";
|
private static final String SEPARATOR = ",";
|
||||||
|
|
||||||
@@ -63,19 +66,18 @@ class LanTcpPlugin extends TcpPlugin {
|
|||||||
TransportProperties p = callback.getLocalProperties();
|
TransportProperties p = callback.getLocalProperties();
|
||||||
String oldIpPorts = p.get(PROP_IP_PORTS);
|
String oldIpPorts = p.get(PROP_IP_PORTS);
|
||||||
List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts);
|
List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts);
|
||||||
List<InetSocketAddress> locals = new LinkedList<>();
|
List<InetSocketAddress> locals = new ArrayList<>();
|
||||||
for (InetAddress local : getLocalIpAddresses()) {
|
for (InetAddress local : getLocalIpAddresses()) {
|
||||||
if (isAcceptableAddress(local)) {
|
if (isAcceptableAddress(local)) {
|
||||||
// If this is the old address, try to use the same port
|
// If this is the old address, try to use the same port
|
||||||
for (InetSocketAddress old : olds) {
|
for (InetSocketAddress old : olds) {
|
||||||
if (old.getAddress().equals(local)) {
|
if (old.getAddress().equals(local))
|
||||||
int port = old.getPort();
|
locals.add(new InetSocketAddress(local, old.getPort()));
|
||||||
locals.add(0, new InetSocketAddress(local, port));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
locals.add(new InetSocketAddress(local, 0));
|
locals.add(new InetSocketAddress(local, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Collections.sort(locals, ADDRESS_COMPARATOR);
|
||||||
return locals;
|
return locals;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,17 +155,39 @@ class LanTcpPlugin extends TcpPlugin {
|
|||||||
// Package access for testing
|
// Package access for testing
|
||||||
boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) {
|
boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) {
|
||||||
// 10.0.0.0/8
|
// 10.0.0.0/8
|
||||||
if (localIp[0] == 10) return remoteIp[0] == 10;
|
if (isPrefix10(localIp)) return isPrefix10(remoteIp);
|
||||||
// 172.16.0.0/12
|
// 172.16.0.0/12
|
||||||
if (localIp[0] == (byte) 172 && (localIp[1] & 0xF0) == 16)
|
if (isPrefix172(localIp)) return isPrefix172(remoteIp);
|
||||||
return remoteIp[0] == (byte) 172 && (remoteIp[1] & 0xF0) == 16;
|
|
||||||
// 192.168.0.0/16
|
// 192.168.0.0/16
|
||||||
if (localIp[0] == (byte) 192 && localIp[1] == (byte) 168)
|
if (isPrefix192(localIp)) return isPrefix192(remoteIp);
|
||||||
return remoteIp[0] == (byte) 192 && remoteIp[1] == (byte) 168;
|
|
||||||
// Unrecognised prefix - may be compatible
|
// Unrecognised prefix - may be compatible
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isPrefix10(byte[] ipv4) {
|
||||||
|
return ipv4[0] == 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPrefix172(byte[] ipv4) {
|
||||||
|
return ipv4[0] == (byte) 172 && (ipv4[1] & 0xF0) == 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPrefix192(byte[] ipv4) {
|
||||||
|
return ipv4[0] == (byte) 192 && ipv4[1] == (byte) 168;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the prefix length for an RFC 1918 address, or 0 for any other
|
||||||
|
// address
|
||||||
|
private static int getRfc1918PrefixLength(InetAddress addr) {
|
||||||
|
if (!(addr instanceof Inet4Address)) return 0;
|
||||||
|
if (!addr.isSiteLocalAddress()) return 0;
|
||||||
|
byte[] ipv4 = addr.getAddress();
|
||||||
|
if (isPrefix10(ipv4)) return 8;
|
||||||
|
if (isPrefix172(ipv4)) return 12;
|
||||||
|
if (isPrefix192(ipv4)) return 16;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsKeyAgreement() {
|
public boolean supportsKeyAgreement() {
|
||||||
return true;
|
return true;
|
||||||
@@ -278,4 +302,19 @@ class LanTcpPlugin extends TcpPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class LanAddressComparator implements Comparator<InetSocketAddress> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(InetSocketAddress a, InetSocketAddress b) {
|
||||||
|
// Prefer addresses with non-zero ports
|
||||||
|
int aPort = a.getPort(), bPort = b.getPort();
|
||||||
|
if (aPort > 0 && bPort == 0) return -1;
|
||||||
|
if (aPort == 0 && bPort > 0) return 1;
|
||||||
|
// Prefer addresses with longer RFC 1918 prefixes
|
||||||
|
int aPrefix = getRfc1918PrefixLength(a.getAddress());
|
||||||
|
int bPrefix = getRfc1918PrefixLength(b.getAddress());
|
||||||
|
return bPrefix - aPrefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import java.util.Collection;
|
|||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.annotation.concurrent.ThreadSafe;
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
@@ -54,7 +55,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(DuplexOutgoingSession.class.getName());
|
Logger.getLogger(DuplexOutgoingSession.class.getName());
|
||||||
|
|
||||||
private static final ThrowingRunnable<IOException> CLOSE = () -> {};
|
private static final ThrowingRunnable<IOException> CLOSE = () -> {
|
||||||
|
};
|
||||||
|
|
||||||
private final DatabaseComponent db;
|
private final DatabaseComponent db;
|
||||||
private final Executor dbExecutor;
|
private final Executor dbExecutor;
|
||||||
@@ -65,6 +67,12 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
private final RecordWriter recordWriter;
|
private final RecordWriter recordWriter;
|
||||||
private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
|
private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
|
||||||
|
|
||||||
|
private final AtomicBoolean generateAckQueued = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean generateBatchQueued = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean generateOfferQueued = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean generateRequestQueued =
|
||||||
|
new AtomicBoolean(false);
|
||||||
|
|
||||||
private volatile boolean interrupted = false;
|
private volatile boolean interrupted = false;
|
||||||
|
|
||||||
DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
|
DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
|
||||||
@@ -87,10 +95,10 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
eventBus.addListener(this);
|
eventBus.addListener(this);
|
||||||
try {
|
try {
|
||||||
// Start a query for each type of record
|
// Start a query for each type of record
|
||||||
dbExecutor.execute(new GenerateAck());
|
generateAck();
|
||||||
dbExecutor.execute(new GenerateBatch());
|
generateBatch();
|
||||||
dbExecutor.execute(new GenerateOffer());
|
generateOffer();
|
||||||
dbExecutor.execute(new GenerateRequest());
|
generateRequest();
|
||||||
long now = clock.currentTimeMillis();
|
long now = clock.currentTimeMillis();
|
||||||
long nextKeepalive = now + maxIdleTime;
|
long nextKeepalive = now + maxIdleTime;
|
||||||
long nextRetxQuery = now + RETX_QUERY_INTERVAL;
|
long nextRetxQuery = now + RETX_QUERY_INTERVAL;
|
||||||
@@ -115,8 +123,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
now = clock.currentTimeMillis();
|
now = clock.currentTimeMillis();
|
||||||
if (now >= nextRetxQuery) {
|
if (now >= nextRetxQuery) {
|
||||||
// Check for retransmittable records
|
// Check for retransmittable records
|
||||||
dbExecutor.execute(new GenerateBatch());
|
generateBatch();
|
||||||
dbExecutor.execute(new GenerateOffer());
|
generateOffer();
|
||||||
nextRetxQuery = now + RETX_QUERY_INTERVAL;
|
nextRetxQuery = now + RETX_QUERY_INTERVAL;
|
||||||
}
|
}
|
||||||
if (now >= nextKeepalive) {
|
if (now >= nextKeepalive) {
|
||||||
@@ -142,6 +150,26 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void generateAck() {
|
||||||
|
if (generateAckQueued.compareAndSet(false, true))
|
||||||
|
dbExecutor.execute(new GenerateAck());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateBatch() {
|
||||||
|
if (generateBatchQueued.compareAndSet(false, true))
|
||||||
|
dbExecutor.execute(new GenerateBatch());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateOffer() {
|
||||||
|
if (generateOfferQueued.compareAndSet(false, true))
|
||||||
|
dbExecutor.execute(new GenerateOffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateRequest() {
|
||||||
|
if (generateRequestQueued.compareAndSet(false, true))
|
||||||
|
dbExecutor.execute(new GenerateRequest());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void interrupt() {
|
public void interrupt() {
|
||||||
interrupted = true;
|
interrupted = true;
|
||||||
@@ -154,20 +182,20 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
ContactRemovedEvent c = (ContactRemovedEvent) e;
|
ContactRemovedEvent c = (ContactRemovedEvent) e;
|
||||||
if (c.getContactId().equals(contactId)) interrupt();
|
if (c.getContactId().equals(contactId)) interrupt();
|
||||||
} else if (e instanceof MessageSharedEvent) {
|
} else if (e instanceof MessageSharedEvent) {
|
||||||
dbExecutor.execute(new GenerateOffer());
|
generateOffer();
|
||||||
} else if (e instanceof GroupVisibilityUpdatedEvent) {
|
} else if (e instanceof GroupVisibilityUpdatedEvent) {
|
||||||
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
|
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
|
||||||
if (g.getAffectedContacts().contains(contactId))
|
if (g.getAffectedContacts().contains(contactId))
|
||||||
dbExecutor.execute(new GenerateOffer());
|
generateOffer();
|
||||||
} else if (e instanceof MessageRequestedEvent) {
|
} else if (e instanceof MessageRequestedEvent) {
|
||||||
if (((MessageRequestedEvent) e).getContactId().equals(contactId))
|
if (((MessageRequestedEvent) e).getContactId().equals(contactId))
|
||||||
dbExecutor.execute(new GenerateBatch());
|
generateBatch();
|
||||||
} else if (e instanceof MessageToAckEvent) {
|
} else if (e instanceof MessageToAckEvent) {
|
||||||
if (((MessageToAckEvent) e).getContactId().equals(contactId))
|
if (((MessageToAckEvent) e).getContactId().equals(contactId))
|
||||||
dbExecutor.execute(new GenerateAck());
|
generateAck();
|
||||||
} else if (e instanceof MessageToRequestEvent) {
|
} else if (e instanceof MessageToRequestEvent) {
|
||||||
if (((MessageToRequestEvent) e).getContactId().equals(contactId))
|
if (((MessageToRequestEvent) e).getContactId().equals(contactId))
|
||||||
dbExecutor.execute(new GenerateRequest());
|
generateRequest();
|
||||||
} else if (e instanceof ShutdownEvent) {
|
} else if (e instanceof ShutdownEvent) {
|
||||||
interrupt();
|
interrupt();
|
||||||
}
|
}
|
||||||
@@ -179,6 +207,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
|
if (!generateAckQueued.getAndSet(false)) throw new AssertionError();
|
||||||
try {
|
try {
|
||||||
Ack a;
|
Ack a;
|
||||||
Transaction txn = db.startTransaction(false);
|
Transaction txn = db.startTransaction(false);
|
||||||
@@ -212,7 +241,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
recordWriter.writeAck(ack);
|
recordWriter.writeAck(ack);
|
||||||
LOG.info("Sent ack");
|
LOG.info("Sent ack");
|
||||||
dbExecutor.execute(new GenerateAck());
|
generateAck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +251,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
|
if (!generateBatchQueued.getAndSet(false))
|
||||||
|
throw new AssertionError();
|
||||||
try {
|
try {
|
||||||
Collection<byte[]> b;
|
Collection<byte[]> b;
|
||||||
Transaction txn = db.startTransaction(false);
|
Transaction txn = db.startTransaction(false);
|
||||||
@@ -256,7 +287,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
for (byte[] raw : batch) recordWriter.writeMessage(raw);
|
for (byte[] raw : batch) recordWriter.writeMessage(raw);
|
||||||
LOG.info("Sent batch");
|
LOG.info("Sent batch");
|
||||||
dbExecutor.execute(new GenerateBatch());
|
generateBatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +297,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
|
if (!generateOfferQueued.getAndSet(false))
|
||||||
|
throw new AssertionError();
|
||||||
try {
|
try {
|
||||||
Offer o;
|
Offer o;
|
||||||
Transaction txn = db.startTransaction(false);
|
Transaction txn = db.startTransaction(false);
|
||||||
@@ -300,7 +333,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
recordWriter.writeOffer(offer);
|
recordWriter.writeOffer(offer);
|
||||||
LOG.info("Sent offer");
|
LOG.info("Sent offer");
|
||||||
dbExecutor.execute(new GenerateOffer());
|
generateOffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +343,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
|
if (!generateRequestQueued.getAndSet(false))
|
||||||
|
throw new AssertionError();
|
||||||
try {
|
try {
|
||||||
Request r;
|
Request r;
|
||||||
Transaction txn = db.startTransaction(false);
|
Transaction txn = db.startTransaction(false);
|
||||||
@@ -343,7 +378,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
|||||||
if (interrupted) return;
|
if (interrupted) return;
|
||||||
recordWriter.writeRequest(request);
|
recordWriter.writeRequest(request);
|
||||||
LOG.info("Sent request");
|
LOG.info("Sent request");
|
||||||
dbExecutor.execute(new GenerateRequest());
|
generateRequest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DataTooNewException;
|
||||||
|
import org.briarproject.bramble.api.db.DataTooOldException;
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseConfig;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.settings.Settings;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.system.SystemClock;
|
||||||
|
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||||
|
import org.briarproject.bramble.test.TestDatabaseConfig;
|
||||||
|
import org.briarproject.bramble.test.TestUtils;
|
||||||
|
import org.jmock.Expectations;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
|
||||||
|
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
||||||
|
import static org.briarproject.bramble.db.JdbcDatabase.CODE_SCHEMA_VERSION;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||||
|
|
||||||
|
private final File testDir = TestUtils.getTestDirectory();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private final Migration<Connection> migration =
|
||||||
|
context.mock(Migration.class, "migration");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private final Migration<Connection> migration1 =
|
||||||
|
context.mock(Migration.class, "migration1");
|
||||||
|
|
||||||
|
protected final DatabaseConfig config =
|
||||||
|
new TestDatabaseConfig(testDir, 1024 * 1024);
|
||||||
|
protected final Clock clock = new SystemClock();
|
||||||
|
|
||||||
|
abstract Database<Connection> createDatabase(
|
||||||
|
List<Migration<Connection>> migrations) throws Exception;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
assertTrue(testDir.mkdirs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
TestUtils.deleteTestDirectory(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotRunMigrationsWhenCreatingDatabase()
|
||||||
|
throws Exception {
|
||||||
|
Database<Connection> db = createDatabase(singletonList(migration));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = DbException.class)
|
||||||
|
public void testThrowsExceptionIfDataSchemaVersionIsMissing()
|
||||||
|
throws Exception {
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
// Override the data schema version
|
||||||
|
setDataSchemaVersion(db, -1);
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - an exception should be thrown
|
||||||
|
db = createDatabase(asList(migration, migration1));
|
||||||
|
db.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoesNotRunMigrationsIfSchemaVersionsMatch()
|
||||||
|
throws Exception {
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - migrations should not be run
|
||||||
|
db = createDatabase(asList(migration, migration1));
|
||||||
|
assertTrue(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = DataTooNewException.class)
|
||||||
|
public void testThrowsExceptionIfDataIsNewerThanCode() throws Exception {
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
// Override the data schema version
|
||||||
|
setDataSchemaVersion(db, CODE_SCHEMA_VERSION + 1);
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - an exception should be thrown
|
||||||
|
db = createDatabase(asList(migration, migration1));
|
||||||
|
db.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = DataTooOldException.class)
|
||||||
|
public void testThrowsExceptionIfCodeIsNewerThanDataAndNoMigrations()
|
||||||
|
throws Exception {
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(emptyList());
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 1);
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - an exception should be thrown
|
||||||
|
db = createDatabase(emptyList());
|
||||||
|
db.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = DataTooOldException.class)
|
||||||
|
public void testThrowsExceptionIfCodeIsNewerThanDataAndNoSuitableMigration()
|
||||||
|
throws Exception {
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(migration).getStartVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 2));
|
||||||
|
oneOf(migration).getEndVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 1));
|
||||||
|
oneOf(migration1).getStartVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 1));
|
||||||
|
oneOf(migration1).getEndVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION));
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
// Override the data schema version
|
||||||
|
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 3);
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - an exception should be thrown
|
||||||
|
db = createDatabase(asList(migration, migration1));
|
||||||
|
db.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRunsMigrationIfCodeIsNewerThanDataAndSuitableMigration()
|
||||||
|
throws Exception {
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
// First migration should be run, increasing schema version by 2
|
||||||
|
oneOf(migration).getStartVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 2));
|
||||||
|
oneOf(migration).getEndVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION));
|
||||||
|
oneOf(migration).migrate(with(any(Connection.class)));
|
||||||
|
// Second migration is not suitable and should be skipped
|
||||||
|
oneOf(migration1).getStartVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 1));
|
||||||
|
oneOf(migration1).getEndVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION));
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
// Override the data schema version
|
||||||
|
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - the first migration should be run
|
||||||
|
db = createDatabase(asList(migration, migration1));
|
||||||
|
assertTrue(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRunsMigrationsIfCodeIsNewerThanDataAndSuitableMigrations()
|
||||||
|
throws Exception {
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
// First migration should be run, incrementing schema version
|
||||||
|
oneOf(migration).getStartVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 2));
|
||||||
|
oneOf(migration).getEndVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 1));
|
||||||
|
oneOf(migration).migrate(with(any(Connection.class)));
|
||||||
|
// Second migration should be run, incrementing schema version again
|
||||||
|
oneOf(migration1).getStartVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION - 1));
|
||||||
|
oneOf(migration1).getEndVersion();
|
||||||
|
will(returnValue(CODE_SCHEMA_VERSION));
|
||||||
|
oneOf(migration1).migrate(with(any(Connection.class)));
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Open the DB for the first time
|
||||||
|
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||||
|
assertFalse(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
// Override the data schema version
|
||||||
|
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
|
||||||
|
db.close();
|
||||||
|
// Reopen the DB - both migrations should be run
|
||||||
|
db = createDatabase(asList(migration, migration1));
|
||||||
|
assertTrue(db.open());
|
||||||
|
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getDataSchemaVersion(Database<Connection> db)
|
||||||
|
throws Exception {
|
||||||
|
Connection txn = db.startTransaction();
|
||||||
|
Settings s = db.getSettings(txn, DB_SETTINGS_NAMESPACE);
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
return s.getInt(SCHEMA_VERSION_KEY, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDataSchemaVersion(Database<Connection> db, int version)
|
||||||
|
throws Exception {
|
||||||
|
Settings s = new Settings();
|
||||||
|
s.putInt(SCHEMA_VERSION_KEY, version);
|
||||||
|
Connection txn = db.startTransaction();
|
||||||
|
db.mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||||
|
db.commitTransaction(txn);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public class H2MigrationTest extends DatabaseMigrationTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Database<Connection> createDatabase(List<Migration<Connection>> migrations)
|
||||||
|
throws Exception {
|
||||||
|
return new H2Database(config, clock) {
|
||||||
|
@Override
|
||||||
|
List<Migration<Connection>> getMigrations() {
|
||||||
|
return migrations;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.Metadata;
|
||||||
|
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.ValidationManager.State;
|
||||||
|
import org.briarproject.bramble.test.BrambleTestCase;
|
||||||
|
import org.briarproject.bramble.test.TestUtils;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertNotNull;
|
||||||
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
|
||||||
|
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
||||||
|
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||||
|
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class Migration30_31Test extends BrambleTestCase {
|
||||||
|
|
||||||
|
private static final String CREATE_GROUPS_STUB =
|
||||||
|
"CREATE TABLE groups"
|
||||||
|
+ " (groupID BINARY(32) NOT NULL,"
|
||||||
|
+ " PRIMARY KEY (groupId))";
|
||||||
|
|
||||||
|
private static final String CREATE_MESSAGES =
|
||||||
|
"CREATE TABLE messages"
|
||||||
|
+ " (messageId BINARY(32) NOT NULL,"
|
||||||
|
+ " groupId BINARY(32) NOT NULL,"
|
||||||
|
+ " timestamp BIGINT NOT NULL,"
|
||||||
|
+ " state INT NOT NULL,"
|
||||||
|
+ " shared BOOLEAN NOT NULL,"
|
||||||
|
+ " length INT NOT NULL,"
|
||||||
|
+ " raw BLOB," // Null if message has been deleted
|
||||||
|
+ " PRIMARY KEY (messageId),"
|
||||||
|
+ " FOREIGN KEY (groupId)"
|
||||||
|
+ " REFERENCES groups (groupId)"
|
||||||
|
+ " ON DELETE CASCADE)";
|
||||||
|
|
||||||
|
private static final String CREATE_MESSAGE_METADATA_30 =
|
||||||
|
"CREATE TABLE messageMetadata"
|
||||||
|
+ " (messageId BINARY(32) NOT NULL,"
|
||||||
|
+ " key VARCHAR NOT NULL,"
|
||||||
|
+ " value BINARY NOT NULL,"
|
||||||
|
+ " PRIMARY KEY (messageId, key),"
|
||||||
|
+ " FOREIGN KEY (messageId)"
|
||||||
|
+ " REFERENCES messages (messageId)"
|
||||||
|
+ " ON DELETE CASCADE)";
|
||||||
|
|
||||||
|
private final File testDir = TestUtils.getTestDirectory();
|
||||||
|
private final File db = new File(testDir, "db");
|
||||||
|
private final String url = "jdbc:h2:" + db.getAbsolutePath();
|
||||||
|
private final GroupId groupId = new GroupId(getRandomId());
|
||||||
|
private final GroupId groupId1 = new GroupId(getRandomId());
|
||||||
|
private final Message message = TestUtils.getMessage(groupId);
|
||||||
|
private final Message message1 = TestUtils.getMessage(groupId1);
|
||||||
|
private final Metadata meta = new Metadata(), meta1 = new Metadata();
|
||||||
|
|
||||||
|
private Connection connection = null;
|
||||||
|
|
||||||
|
public Migration30_31Test() {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
meta.put(getRandomString(123 + i), getRandomBytes(123 + i));
|
||||||
|
meta1.put(getRandomString(123 + i), getRandomBytes(123 + i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
assertTrue(testDir.mkdirs());
|
||||||
|
Class.forName("org.h2.Driver");
|
||||||
|
connection = DriverManager.getConnection(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
if (connection != null) connection.close();
|
||||||
|
TestUtils.deleteTestDirectory(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMigration() throws Exception {
|
||||||
|
try {
|
||||||
|
Statement s = connection.createStatement();
|
||||||
|
s.execute(CREATE_GROUPS_STUB);
|
||||||
|
s.execute(CREATE_MESSAGES);
|
||||||
|
s.execute(CREATE_MESSAGE_METADATA_30);
|
||||||
|
s.close();
|
||||||
|
|
||||||
|
addGroup(groupId);
|
||||||
|
addMessage(message, DELIVERED, true);
|
||||||
|
addMessageMetadata30(message, meta);
|
||||||
|
assertMetadataEquals(meta, getMessageMetadata(message.getId()));
|
||||||
|
|
||||||
|
addGroup(groupId1);
|
||||||
|
addMessage(message1, UNKNOWN, false);
|
||||||
|
addMessageMetadata30(message1, meta1);
|
||||||
|
assertMetadataEquals(meta1, getMessageMetadata(message1.getId()));
|
||||||
|
|
||||||
|
new Migration30_31().migrate(connection);
|
||||||
|
|
||||||
|
assertMetadataEquals(meta, getMessageMetadata(message.getId()));
|
||||||
|
for (String key : meta.keySet()) {
|
||||||
|
GroupId g = getMessageMetadataGroupId31(message.getId(), key);
|
||||||
|
assertEquals(groupId, g);
|
||||||
|
State state = getMessageMetadataState31(message.getId(), key);
|
||||||
|
assertEquals(DELIVERED, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMetadataEquals(meta1, getMessageMetadata(message1.getId()));
|
||||||
|
for (String key : meta1.keySet()) {
|
||||||
|
GroupId g = getMessageMetadataGroupId31(message1.getId(), key);
|
||||||
|
assertEquals(groupId1, g);
|
||||||
|
State state = getMessageMetadataState31(message1.getId(), key);
|
||||||
|
assertEquals(UNKNOWN, state);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
connection.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addGroup(GroupId g) throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "INSERT INTO groups (groupId) VALUES (?)";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, g.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMessage(Message m, State state, boolean shared)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
|
||||||
|
+ " state, shared, length, raw)"
|
||||||
|
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getId().getBytes());
|
||||||
|
ps.setBytes(2, m.getGroupId().getBytes());
|
||||||
|
ps.setLong(3, m.getTimestamp());
|
||||||
|
ps.setInt(4, state.getValue());
|
||||||
|
ps.setBoolean(5, shared);
|
||||||
|
byte[] raw = m.getRaw();
|
||||||
|
ps.setInt(6, raw.length);
|
||||||
|
ps.setBytes(7, raw);
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMessageMetadata30(Message m, Metadata meta)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "INSERT INTO messageMetadata"
|
||||||
|
+ " (messageId, key, value)"
|
||||||
|
+ " VALUES (?, ?, ?)";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getId().getBytes());
|
||||||
|
for (Entry<String, byte[]> e : meta.entrySet()) {
|
||||||
|
ps.setString(2, e.getKey());
|
||||||
|
ps.setBytes(3, e.getValue());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
int[] batchAffected = ps.executeBatch();
|
||||||
|
if (batchAffected.length != meta.size())
|
||||||
|
throw new DbStateException();
|
||||||
|
for (int rows : batchAffected)
|
||||||
|
if (rows != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Metadata getMessageMetadata(MessageId m) throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT key, value FROM messageMetadata"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
Metadata meta = new Metadata();
|
||||||
|
while (rs.next()) meta.put(rs.getString(1), rs.getBytes(2));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return meta;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (rs != null) rs.close();
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupId getMessageMetadataGroupId31(MessageId m, String key)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT groupId FROM messageMetadata"
|
||||||
|
+ " WHERE messageId = ? AND key = ?";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
ps.setString(2, key);
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
GroupId g = new GroupId(rs.getBytes(1));
|
||||||
|
if (rs.next()) throw new DbStateException();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return g;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (rs != null) rs.close();
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private State getMessageMetadataState31(MessageId m, String key)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT state FROM messageMetadata"
|
||||||
|
+ " WHERE messageId = ? AND key = ?";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
ps.setString(2, key);
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
State state = State.fromValue(rs.getInt(1));
|
||||||
|
if (rs.next()) throw new DbStateException();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return state;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (rs != null) rs.close();
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMetadataEquals(Metadata expected, Metadata actual) {
|
||||||
|
assertEquals(expected.size(), actual.size());
|
||||||
|
for (Entry<String, byte[]> e : expected.entrySet()) {
|
||||||
|
byte[] value = actual.get(e.getKey());
|
||||||
|
assertNotNull(value);
|
||||||
|
assertArrayEquals(e.getValue(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,14 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
|||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
|
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
|
||||||
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
|
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
|
||||||
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
|
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
|
||||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||||
import org.briarproject.bramble.api.system.Clock;
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
import org.briarproject.bramble.test.BrambleTestCase;
|
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||||
import org.briarproject.bramble.test.ImmediateExecutor;
|
import org.briarproject.bramble.test.ImmediateExecutor;
|
||||||
import org.briarproject.bramble.test.RunAction;
|
import org.briarproject.bramble.test.RunAction;
|
||||||
import org.jmock.Expectations;
|
import org.jmock.Expectations;
|
||||||
import org.jmock.Mockery;
|
|
||||||
import org.jmock.lib.legacy.ClassImposteriser;
|
import org.jmock.lib.legacy.ClassImposteriser;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -29,30 +29,37 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
public class PollerTest extends BrambleTestCase {
|
public class PollerTest extends BrambleMockTestCase {
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler =
|
||||||
|
context.mock(ScheduledExecutorService.class);
|
||||||
|
private final ConnectionManager connectionManager =
|
||||||
|
context.mock(ConnectionManager.class);
|
||||||
|
private final ConnectionRegistry connectionRegistry =
|
||||||
|
context.mock(ConnectionRegistry.class);
|
||||||
|
private final PluginManager pluginManager =
|
||||||
|
context.mock(PluginManager.class);
|
||||||
|
private final Clock clock = context.mock(Clock.class);
|
||||||
|
private final ScheduledFuture future = context.mock(ScheduledFuture.class);
|
||||||
|
private final SecureRandom random;
|
||||||
|
|
||||||
|
private final Executor ioExecutor = new ImmediateExecutor();
|
||||||
|
private final TransportId transportId = new TransportId("id");
|
||||||
private final ContactId contactId = new ContactId(234);
|
private final ContactId contactId = new ContactId(234);
|
||||||
private final int pollingInterval = 60 * 1000;
|
private final int pollingInterval = 60 * 1000;
|
||||||
private final long now = System.currentTimeMillis();
|
private final long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
public PollerTest() {
|
||||||
|
context.setImposteriser(ClassImposteriser.INSTANCE);
|
||||||
|
random = context.mock(SecureRandom.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConnectOnContactStatusChanged() throws Exception {
|
public void testConnectOnContactStatusChanged() throws Exception {
|
||||||
Mockery context = new Mockery();
|
|
||||||
context.setImposteriser(ClassImposteriser.INSTANCE);
|
|
||||||
Executor ioExecutor = new ImmediateExecutor();
|
|
||||||
ScheduledExecutorService scheduler =
|
|
||||||
context.mock(ScheduledExecutorService.class);
|
|
||||||
ConnectionManager connectionManager =
|
|
||||||
context.mock(ConnectionManager.class);
|
|
||||||
ConnectionRegistry connectionRegistry =
|
|
||||||
context.mock(ConnectionRegistry.class);
|
|
||||||
PluginManager pluginManager = context.mock(PluginManager.class);
|
|
||||||
SecureRandom random = context.mock(SecureRandom.class);
|
|
||||||
Clock clock = context.mock(Clock.class);
|
|
||||||
|
|
||||||
// Two simplex plugins: one supports polling, the other doesn't
|
// Two simplex plugins: one supports polling, the other doesn't
|
||||||
SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
|
SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
|
||||||
SimplexPlugin simplexPlugin1 =
|
SimplexPlugin simplexPlugin1 =
|
||||||
@@ -120,28 +127,12 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
connectionRegistry, pluginManager, random, clock);
|
connectionRegistry, pluginManager, random, clock);
|
||||||
|
|
||||||
p.eventOccurred(new ContactStatusChangedEvent(contactId, true));
|
p.eventOccurred(new ContactStatusChangedEvent(contactId, true));
|
||||||
|
|
||||||
context.assertIsSatisfied();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRescheduleAndReconnectOnConnectionClosed()
|
public void testRescheduleAndReconnectOnConnectionClosed()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Mockery context = new Mockery();
|
|
||||||
context.setImposteriser(ClassImposteriser.INSTANCE);
|
|
||||||
Executor ioExecutor = new ImmediateExecutor();
|
|
||||||
ScheduledExecutorService scheduler =
|
|
||||||
context.mock(ScheduledExecutorService.class);
|
|
||||||
ConnectionManager connectionManager =
|
|
||||||
context.mock(ConnectionManager.class);
|
|
||||||
ConnectionRegistry connectionRegistry =
|
|
||||||
context.mock(ConnectionRegistry.class);
|
|
||||||
PluginManager pluginManager = context.mock(PluginManager.class);
|
|
||||||
SecureRandom random = context.mock(SecureRandom.class);
|
|
||||||
Clock clock = context.mock(Clock.class);
|
|
||||||
|
|
||||||
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
|
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
|
||||||
TransportId transportId = new TransportId("id");
|
|
||||||
DuplexTransportConnection duplexConnection =
|
DuplexTransportConnection duplexConnection =
|
||||||
context.mock(DuplexTransportConnection.class);
|
context.mock(DuplexTransportConnection.class);
|
||||||
|
|
||||||
@@ -168,6 +159,7 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||||
with((long) pollingInterval), with(MILLISECONDS));
|
with((long) pollingInterval), with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
// connectToContact()
|
// connectToContact()
|
||||||
// Check whether the contact is already connected
|
// Check whether the contact is already connected
|
||||||
oneOf(connectionRegistry).isConnected(contactId, transportId);
|
oneOf(connectionRegistry).isConnected(contactId, transportId);
|
||||||
@@ -185,28 +177,12 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
|
|
||||||
p.eventOccurred(new ConnectionClosedEvent(contactId, transportId,
|
p.eventOccurred(new ConnectionClosedEvent(contactId, transportId,
|
||||||
false));
|
false));
|
||||||
|
|
||||||
context.assertIsSatisfied();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRescheduleOnConnectionOpened() throws Exception {
|
public void testRescheduleOnConnectionOpened() throws Exception {
|
||||||
Mockery context = new Mockery();
|
Plugin plugin = context.mock(Plugin.class);
|
||||||
context.setImposteriser(ClassImposteriser.INSTANCE);
|
|
||||||
Executor ioExecutor = new ImmediateExecutor();
|
|
||||||
ScheduledExecutorService scheduler =
|
|
||||||
context.mock(ScheduledExecutorService.class);
|
|
||||||
ConnectionManager connectionManager =
|
|
||||||
context.mock(ConnectionManager.class);
|
|
||||||
ConnectionRegistry connectionRegistry =
|
|
||||||
context.mock(ConnectionRegistry.class);
|
|
||||||
PluginManager pluginManager = context.mock(PluginManager.class);
|
|
||||||
SecureRandom random = context.mock(SecureRandom.class);
|
|
||||||
Clock clock = context.mock(Clock.class);
|
|
||||||
|
|
||||||
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
|
|
||||||
TransportId transportId = new TransportId("id");
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
allowing(plugin).getId();
|
allowing(plugin).getId();
|
||||||
@@ -224,6 +200,7 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||||
with((long) pollingInterval), with(MILLISECONDS));
|
with((long) pollingInterval), with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
}});
|
}});
|
||||||
|
|
||||||
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
|
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
|
||||||
@@ -231,27 +208,11 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
|
|
||||||
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
|
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
|
||||||
false));
|
false));
|
||||||
|
|
||||||
context.assertIsSatisfied();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRescheduleDoesNotReplaceEarlierTask() throws Exception {
|
public void testRescheduleDoesNotReplaceEarlierTask() throws Exception {
|
||||||
Mockery context = new Mockery();
|
Plugin plugin = context.mock(Plugin.class);
|
||||||
context.setImposteriser(ClassImposteriser.INSTANCE);
|
|
||||||
Executor ioExecutor = new ImmediateExecutor();
|
|
||||||
ScheduledExecutorService scheduler =
|
|
||||||
context.mock(ScheduledExecutorService.class);
|
|
||||||
ConnectionManager connectionManager =
|
|
||||||
context.mock(ConnectionManager.class);
|
|
||||||
ConnectionRegistry connectionRegistry =
|
|
||||||
context.mock(ConnectionRegistry.class);
|
|
||||||
PluginManager pluginManager = context.mock(PluginManager.class);
|
|
||||||
SecureRandom random = context.mock(SecureRandom.class);
|
|
||||||
Clock clock = context.mock(Clock.class);
|
|
||||||
|
|
||||||
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
|
|
||||||
TransportId transportId = new TransportId("id");
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
allowing(plugin).getId();
|
allowing(plugin).getId();
|
||||||
@@ -270,6 +231,7 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||||
with((long) pollingInterval), with(MILLISECONDS));
|
with((long) pollingInterval), with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
// Second event
|
// Second event
|
||||||
// Get the plugin
|
// Get the plugin
|
||||||
oneOf(pluginManager).getPlugin(transportId);
|
oneOf(pluginManager).getPlugin(transportId);
|
||||||
@@ -291,27 +253,59 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
false));
|
false));
|
||||||
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
|
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
|
||||||
false));
|
false));
|
||||||
|
|
||||||
context.assertIsSatisfied();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPollOnTransportEnabled() throws Exception {
|
public void testRescheduleReplacesLaterTask() throws Exception {
|
||||||
Mockery context = new Mockery();
|
Plugin plugin = context.mock(Plugin.class);
|
||||||
context.setImposteriser(ClassImposteriser.INSTANCE);
|
|
||||||
Executor ioExecutor = new ImmediateExecutor();
|
context.checking(new Expectations() {{
|
||||||
ScheduledExecutorService scheduler =
|
allowing(plugin).getId();
|
||||||
context.mock(ScheduledExecutorService.class);
|
will(returnValue(transportId));
|
||||||
ConnectionManager connectionManager =
|
// First event
|
||||||
context.mock(ConnectionManager.class);
|
// Get the plugin
|
||||||
ConnectionRegistry connectionRegistry =
|
oneOf(pluginManager).getPlugin(transportId);
|
||||||
context.mock(ConnectionRegistry.class);
|
will(returnValue(plugin));
|
||||||
PluginManager pluginManager = context.mock(PluginManager.class);
|
// The plugin supports polling
|
||||||
SecureRandom random = context.mock(SecureRandom.class);
|
oneOf(plugin).shouldPoll();
|
||||||
Clock clock = context.mock(Clock.class);
|
will(returnValue(true));
|
||||||
|
// Schedule the next poll
|
||||||
|
oneOf(plugin).getPollingInterval();
|
||||||
|
will(returnValue(pollingInterval));
|
||||||
|
oneOf(clock).currentTimeMillis();
|
||||||
|
will(returnValue(now));
|
||||||
|
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||||
|
with((long) pollingInterval), with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
|
// Second event
|
||||||
|
// Get the plugin
|
||||||
|
oneOf(pluginManager).getPlugin(transportId);
|
||||||
|
will(returnValue(plugin));
|
||||||
|
// The plugin supports polling
|
||||||
|
oneOf(plugin).shouldPoll();
|
||||||
|
will(returnValue(true));
|
||||||
|
// Replace the previously scheduled task, due later
|
||||||
|
oneOf(plugin).getPollingInterval();
|
||||||
|
will(returnValue(pollingInterval - 2));
|
||||||
|
oneOf(clock).currentTimeMillis();
|
||||||
|
will(returnValue(now + 1));
|
||||||
|
oneOf(future).cancel(false);
|
||||||
|
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||||
|
with((long) pollingInterval - 2), with(MILLISECONDS));
|
||||||
|
}});
|
||||||
|
|
||||||
|
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
|
||||||
|
connectionRegistry, pluginManager, random, clock);
|
||||||
|
|
||||||
|
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
|
||||||
|
false));
|
||||||
|
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPollsOnTransportEnabled() throws Exception {
|
||||||
Plugin plugin = context.mock(Plugin.class);
|
Plugin plugin = context.mock(Plugin.class);
|
||||||
TransportId transportId = new TransportId("id");
|
|
||||||
List<ContactId> connected = Collections.singletonList(contactId);
|
List<ContactId> connected = Collections.singletonList(contactId);
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
@@ -328,6 +322,7 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L),
|
oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L),
|
||||||
with(MILLISECONDS));
|
with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
will(new RunAction());
|
will(new RunAction());
|
||||||
// Running the polling task schedules the next polling task
|
// Running the polling task schedules the next polling task
|
||||||
oneOf(plugin).getPollingInterval();
|
oneOf(plugin).getPollingInterval();
|
||||||
@@ -338,6 +333,7 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||||
with((long) (pollingInterval * 0.5)), with(MILLISECONDS));
|
with((long) (pollingInterval * 0.5)), with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
// Poll the plugin
|
// Poll the plugin
|
||||||
oneOf(connectionRegistry).getConnectedContacts(transportId);
|
oneOf(connectionRegistry).getConnectedContacts(transportId);
|
||||||
will(returnValue(connected));
|
will(returnValue(connected));
|
||||||
@@ -348,7 +344,36 @@ public class PollerTest extends BrambleTestCase {
|
|||||||
connectionRegistry, pluginManager, random, clock);
|
connectionRegistry, pluginManager, random, clock);
|
||||||
|
|
||||||
p.eventOccurred(new TransportEnabledEvent(transportId));
|
p.eventOccurred(new TransportEnabledEvent(transportId));
|
||||||
|
}
|
||||||
|
|
||||||
context.assertIsSatisfied();
|
@Test
|
||||||
|
public void testCancelsPollingOnTransportDisabled() throws Exception {
|
||||||
|
Plugin plugin = context.mock(Plugin.class);
|
||||||
|
List<ContactId> connected = Collections.singletonList(contactId);
|
||||||
|
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
allowing(plugin).getId();
|
||||||
|
will(returnValue(transportId));
|
||||||
|
// Get the plugin
|
||||||
|
oneOf(pluginManager).getPlugin(transportId);
|
||||||
|
will(returnValue(plugin));
|
||||||
|
// The plugin supports polling
|
||||||
|
oneOf(plugin).shouldPoll();
|
||||||
|
will(returnValue(true));
|
||||||
|
// Schedule a polling task immediately
|
||||||
|
oneOf(clock).currentTimeMillis();
|
||||||
|
will(returnValue(now));
|
||||||
|
oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L),
|
||||||
|
with(MILLISECONDS));
|
||||||
|
will(returnValue(future));
|
||||||
|
// The plugin is disabled before the task runs - cancel the task
|
||||||
|
oneOf(future).cancel(false);
|
||||||
|
}});
|
||||||
|
|
||||||
|
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
|
||||||
|
connectionRegistry, pluginManager, random, clock);
|
||||||
|
|
||||||
|
p.eventOccurred(new TransportEnabledEvent(transportId));
|
||||||
|
p.eventOccurred(new TransportDisabledEvent(transportId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
|||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||||
import org.briarproject.bramble.api.settings.Settings;
|
import org.briarproject.bramble.api.settings.Settings;
|
||||||
|
import org.briarproject.bramble.plugin.tcp.LanTcpPlugin.LanAddressComparator;
|
||||||
import org.briarproject.bramble.test.BrambleTestCase;
|
import org.briarproject.bramble.test.BrambleTestCase;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import java.net.NetworkInterface;
|
|||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
@@ -56,6 +58,9 @@ public class LanTcpPluginTest extends BrambleTestCase {
|
|||||||
// Local and remote in 192.168.0.0/16 should return true
|
// Local and remote in 192.168.0.0/16 should return true
|
||||||
assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
|
assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
|
||||||
makeAddress(192, 168, 255, 255)));
|
makeAddress(192, 168, 255, 255)));
|
||||||
|
// Local and remote in 169.254.0.0/16 (link-local) should return true
|
||||||
|
assertTrue(plugin.addressesAreOnSameLan(makeAddress(169, 254, 0, 0),
|
||||||
|
makeAddress(169, 254, 255, 255)));
|
||||||
// Local and remote in different recognised prefixes should return false
|
// Local and remote in different recognised prefixes should return false
|
||||||
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
|
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
|
||||||
makeAddress(172, 31, 255, 255)));
|
makeAddress(172, 31, 255, 255)));
|
||||||
@@ -275,6 +280,57 @@ public class LanTcpPluginTest extends BrambleTestCase {
|
|||||||
plugin.stop();
|
plugin.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testComparatorPrefersNonZeroPorts() throws Exception {
|
||||||
|
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
|
||||||
|
InetSocketAddress nonZero = new InetSocketAddress("1.2.3.4", 1234);
|
||||||
|
InetSocketAddress zero = new InetSocketAddress("1.2.3.4", 0);
|
||||||
|
|
||||||
|
assertEquals(0, comparator.compare(nonZero, nonZero));
|
||||||
|
assertTrue(comparator.compare(nonZero, zero) < 0);
|
||||||
|
|
||||||
|
assertTrue(comparator.compare(zero, nonZero) > 0);
|
||||||
|
assertEquals(0, comparator.compare(zero, zero));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testComparatorPrefersLongerPrefixes() throws Exception {
|
||||||
|
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
|
||||||
|
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
|
||||||
|
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
|
||||||
|
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
|
||||||
|
|
||||||
|
assertEquals(0, comparator.compare(prefix192, prefix192));
|
||||||
|
assertTrue(comparator.compare(prefix192, prefix172) < 0);
|
||||||
|
assertTrue(comparator.compare(prefix192, prefix10) < 0);
|
||||||
|
|
||||||
|
assertTrue(comparator.compare(prefix172, prefix192) > 0);
|
||||||
|
assertEquals(0, comparator.compare(prefix172, prefix172));
|
||||||
|
assertTrue(comparator.compare(prefix172, prefix10) < 0);
|
||||||
|
|
||||||
|
assertTrue(comparator.compare(prefix10, prefix192) > 0);
|
||||||
|
assertTrue(comparator.compare(prefix10, prefix172) > 0);
|
||||||
|
assertEquals(0, comparator.compare(prefix10, prefix10));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testComparatorPrefersSiteLocalToLinkLocal() throws Exception {
|
||||||
|
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
|
||||||
|
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
|
||||||
|
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
|
||||||
|
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
|
||||||
|
InetSocketAddress linkLocal = new InetSocketAddress("169.254.0.1", 0);
|
||||||
|
|
||||||
|
assertTrue(comparator.compare(prefix192, linkLocal) < 0);
|
||||||
|
assertTrue(comparator.compare(prefix172, linkLocal) < 0);
|
||||||
|
assertTrue(comparator.compare(prefix10, linkLocal) < 0);
|
||||||
|
|
||||||
|
assertTrue(comparator.compare(linkLocal, prefix192) > 0);
|
||||||
|
assertTrue(comparator.compare(linkLocal, prefix172) > 0);
|
||||||
|
assertTrue(comparator.compare(linkLocal, prefix10) > 0);
|
||||||
|
assertEquals(0, comparator.compare(linkLocal, linkLocal));
|
||||||
|
}
|
||||||
|
|
||||||
private boolean systemHasLocalIpv4Address() throws Exception {
|
private boolean systemHasLocalIpv4Address() throws Exception {
|
||||||
for (NetworkInterface i : Collections.list(
|
for (NetworkInterface i : Collections.list(
|
||||||
NetworkInterface.getNetworkInterfaces())) {
|
NetworkInterface.getNetworkInterfaces())) {
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 26
|
targetSdkVersion 26
|
||||||
versionCode 1617
|
versionCode 1618
|
||||||
versionName "0.16.17"
|
versionName "0.16.18"
|
||||||
applicationId "org.briarproject.briar.beta"
|
applicationId "org.briarproject.briar.beta"
|
||||||
resValue "string", "app_package", "org.briarproject.briar.beta"
|
resValue "string", "app_package", "org.briarproject.briar.beta"
|
||||||
resValue "string", "app_name", "Briar Beta"
|
resValue "string", "app_name", "Briar Beta"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.briarproject.briar.android;
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import org.briarproject.bramble.BrambleAndroidModule;
|
import org.briarproject.bramble.BrambleAndroidModule;
|
||||||
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||||
import org.briarproject.bramble.BrambleCoreModule;
|
import org.briarproject.bramble.BrambleCoreModule;
|
||||||
@@ -89,6 +91,8 @@ public interface AndroidComponent
|
|||||||
|
|
||||||
AndroidNotificationManager androidNotificationManager();
|
AndroidNotificationManager androidNotificationManager();
|
||||||
|
|
||||||
|
SharedPreferences sharedPreferences();
|
||||||
|
|
||||||
ScreenFilterMonitor screenFilterMonitor();
|
ScreenFilterMonitor screenFilterMonitor();
|
||||||
|
|
||||||
ConnectionRegistry connectionRegistry();
|
ConnectionRegistry connectionRegistry();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.briar.android;
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
@@ -157,6 +158,11 @@ public class AppModule {
|
|||||||
return devConfig;
|
return devConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
SharedPreferences provideSharedPreferences(Application app) {
|
||||||
|
return app.getSharedPreferences("db", MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
ReferenceManager provideReferenceManager() {
|
ReferenceManager provideReferenceManager() {
|
||||||
@@ -174,8 +180,11 @@ public class AppModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@Singleton
|
||||||
ScreenFilterMonitor provideScreenFilterMonitor(
|
ScreenFilterMonitor provideScreenFilterMonitor(
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
ScreenFilterMonitorImpl screenFilterMonitor) {
|
ScreenFilterMonitorImpl screenFilterMonitor) {
|
||||||
|
lifecycleManager.registerService(screenFilterMonitor);
|
||||||
return screenFilterMonitor;
|
return screenFilterMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package org.briarproject.briar.android;
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.PackageManager.NameNotFoundException;
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||||||
import android.content.pm.Signature;
|
import android.content.pm.Signature;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.lifecycle.Service;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.ServiceException;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||||
import org.briarproject.bramble.util.StringUtils;
|
import org.briarproject.bramble.util.StringUtils;
|
||||||
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
||||||
|
|
||||||
@@ -16,23 +25,33 @@ import java.io.InputStream;
|
|||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static android.Manifest.permission.SYSTEM_ALERT_WINDOW;
|
import static android.Manifest.permission.SYSTEM_ALERT_WINDOW;
|
||||||
|
import static android.content.Intent.ACTION_PACKAGE_ADDED;
|
||||||
|
import static android.content.Intent.ACTION_PACKAGE_CHANGED;
|
||||||
|
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
|
||||||
|
import static android.content.Intent.ACTION_PACKAGE_REPLACED;
|
||||||
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
|
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
|
||||||
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
|
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
|
||||||
|
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
|
||||||
import static android.content.pm.PackageManager.GET_PERMISSIONS;
|
import static android.content.pm.PackageManager.GET_PERMISSIONS;
|
||||||
import static android.content.pm.PackageManager.GET_SIGNATURES;
|
import static android.content.pm.PackageManager.GET_SIGNATURES;
|
||||||
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
|
class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
|
Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
|
||||||
@@ -56,54 +75,93 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
|
|||||||
"82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" +
|
"82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" +
|
||||||
"0B145B6AA192858E79020103";
|
"0B145B6AA192858E79020103";
|
||||||
|
|
||||||
|
private static final String PREF_KEY_ALLOWED = "allowedOverlayApps";
|
||||||
|
|
||||||
private final PackageManager pm;
|
private final PackageManager pm;
|
||||||
|
private final Application app;
|
||||||
|
private final AndroidExecutor androidExecutor;
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
// UiThread
|
||||||
|
@Nullable
|
||||||
|
private BroadcastReceiver receiver = null;
|
||||||
|
|
||||||
|
// UiThread
|
||||||
|
@Nullable
|
||||||
|
private Collection<AppDetails> cachedApps = null;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ScreenFilterMonitorImpl(Application app) {
|
ScreenFilterMonitorImpl(Application app, AndroidExecutor androidExecutor,
|
||||||
|
SharedPreferences prefs) {
|
||||||
pm = app.getPackageManager();
|
pm = app.getPackageManager();
|
||||||
|
this.app = app;
|
||||||
|
this.androidExecutor = androidExecutor;
|
||||||
|
this.prefs = prefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@UiThread
|
@UiThread
|
||||||
public Set<String> getApps() {
|
public Collection<AppDetails> getApps() {
|
||||||
Set<String> screenFilterApps = new TreeSet<>();
|
if (cachedApps != null) return cachedApps;
|
||||||
|
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
|
||||||
|
Collections.emptySet());
|
||||||
|
List<AppDetails> apps = new ArrayList<>();
|
||||||
List<PackageInfo> packageInfos =
|
List<PackageInfo> packageInfos =
|
||||||
pm.getInstalledPackages(GET_PERMISSIONS);
|
pm.getInstalledPackages(GET_PERMISSIONS);
|
||||||
for (PackageInfo packageInfo : packageInfos) {
|
for (PackageInfo packageInfo : packageInfos) {
|
||||||
if (isOverlayApp(packageInfo)) {
|
if (!allowed.contains(packageInfo.packageName)
|
||||||
String name = pkgToString(packageInfo);
|
&& isOverlayApp(packageInfo)) {
|
||||||
if (name != null) {
|
String name = getAppName(packageInfo);
|
||||||
screenFilterApps.add(name);
|
apps.add(new AppDetails(name, packageInfo.packageName));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return screenFilterApps;
|
Collections.sort(apps, (a, b) -> a.name.compareTo(b.name));
|
||||||
|
apps = Collections.unmodifiableList(apps);
|
||||||
|
cachedApps = apps;
|
||||||
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the application name for a given package.
|
@Override
|
||||||
@Nullable
|
@UiThread
|
||||||
private String pkgToString(PackageInfo pkgInfo) {
|
public void allowApps(Collection<String> packageNames) {
|
||||||
|
cachedApps = null;
|
||||||
|
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
|
||||||
|
Collections.emptySet());
|
||||||
|
Set<String> merged = new HashSet<>(allowed);
|
||||||
|
merged.addAll(packageNames);
|
||||||
|
prefs.edit().putStringSet(PREF_KEY_ALLOWED, merged).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the application name for a given package, or the package name
|
||||||
|
// if no application name is available
|
||||||
|
private String getAppName(PackageInfo pkgInfo) {
|
||||||
CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo);
|
CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo);
|
||||||
if (seq != null) {
|
return seq == null ? pkgInfo.packageName : seq.toString();
|
||||||
return seq.toString();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if an installed package is a user app using the permission.
|
// Checks if an installed package is a user app using the permission.
|
||||||
private boolean isOverlayApp(PackageInfo packageInfo) {
|
private boolean isOverlayApp(PackageInfo packageInfo) {
|
||||||
int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP;
|
int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP;
|
||||||
// Ignore system apps
|
// Ignore system apps
|
||||||
if ((packageInfo.applicationInfo.flags & mask) != 0) {
|
if ((packageInfo.applicationInfo.flags & mask) != 0) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Ignore Play Services, it's effectively a system app
|
// Ignore Play Services, it's effectively a system app
|
||||||
if (isPlayServices(packageInfo.packageName)) {
|
if (isPlayServices(packageInfo.packageName)) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Get permissions
|
// Get permissions
|
||||||
String[] requestedPermissions = packageInfo.requestedPermissions;
|
String[] requestedPermissions = packageInfo.requestedPermissions;
|
||||||
if (requestedPermissions != null) {
|
if (requestedPermissions == null) return false;
|
||||||
|
if (SDK_INT >= 16 && SDK_INT < 23) {
|
||||||
|
// Check whether the permission has been requested and granted
|
||||||
|
int[] flags = packageInfo.requestedPermissionsFlags;
|
||||||
|
for (int i = 0; i < requestedPermissions.length; i++) {
|
||||||
|
if (requestedPermissions[i].equals(SYSTEM_ALERT_WINDOW)) {
|
||||||
|
// 'flags' may be null on Robolectric
|
||||||
|
return flags == null ||
|
||||||
|
(flags[i] & REQUESTED_PERMISSION_GRANTED) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check whether the permission has been requested
|
||||||
for (String requestedPermission : requestedPermissions) {
|
for (String requestedPermission : requestedPermissions) {
|
||||||
if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) {
|
if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -113,6 +171,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PackageManagerGetSignatures")
|
||||||
private boolean isPlayServices(String pkg) {
|
private boolean isPlayServices(String pkg) {
|
||||||
if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false;
|
if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false;
|
||||||
try {
|
try {
|
||||||
@@ -135,4 +194,36 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startService() throws ServiceException {
|
||||||
|
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||||
|
androidExecutor.runOnUiThread(() -> {
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(ACTION_PACKAGE_ADDED);
|
||||||
|
filter.addAction(ACTION_PACKAGE_CHANGED);
|
||||||
|
filter.addAction(ACTION_PACKAGE_REMOVED);
|
||||||
|
filter.addAction(ACTION_PACKAGE_REPLACED);
|
||||||
|
filter.addDataScheme("package");
|
||||||
|
receiver = new PackageBroadcastReceiver();
|
||||||
|
app.registerReceiver(receiver, filter);
|
||||||
|
cachedApps = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopService() throws ServiceException {
|
||||||
|
androidExecutor.runOnUiThread(() -> {
|
||||||
|
if (receiver != null) app.unregisterReceiver(receiver);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PackageBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@UiThread
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
cachedApps = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.briarproject.briar.android.forum.CreateForumActivity;
|
|||||||
import org.briarproject.briar.android.forum.ForumActivity;
|
import org.briarproject.briar.android.forum.ForumActivity;
|
||||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||||
import org.briarproject.briar.android.forum.ForumModule;
|
import org.briarproject.briar.android.forum.ForumModule;
|
||||||
|
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||||
import org.briarproject.briar.android.introduction.ContactChooserFragment;
|
import org.briarproject.briar.android.introduction.ContactChooserFragment;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
||||||
@@ -152,7 +153,9 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
// Fragments
|
// Fragments
|
||||||
void inject(AuthorNameFragment fragment);
|
void inject(AuthorNameFragment fragment);
|
||||||
|
|
||||||
void inject(PasswordFragment fragment);
|
void inject(PasswordFragment fragment);
|
||||||
|
|
||||||
void inject(DozeFragment fragment);
|
void inject(DozeFragment fragment);
|
||||||
|
|
||||||
void inject(ContactListFragment fragment);
|
void inject(ContactListFragment fragment);
|
||||||
@@ -189,4 +192,5 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(SettingsFragment fragment);
|
void inject(SettingsFragment fragment);
|
||||||
|
|
||||||
|
void inject(ScreenFilterDialogFragment fragment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.briarproject.briar.android.activity;
|
package org.briarproject.briar.android.activity;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import org.briarproject.briar.android.controller.BriarController;
|
import org.briarproject.briar.android.controller.BriarController;
|
||||||
import org.briarproject.briar.android.controller.BriarControllerImpl;
|
import org.briarproject.briar.android.controller.BriarControllerImpl;
|
||||||
@@ -19,7 +18,6 @@ import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
|
||||||
import static org.briarproject.briar.android.BriarService.BriarServiceConnection;
|
import static org.briarproject.briar.android.BriarService.BriarServiceConnection;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -57,12 +55,6 @@ public class ActivityModule {
|
|||||||
return configController;
|
return configController;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@Provides
|
|
||||||
SharedPreferences provideSharedPreferences(Activity activity) {
|
|
||||||
return activity.getSharedPreferences("db", MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
@Provides
|
@Provides
|
||||||
PasswordController providePasswordController(
|
PasswordController providePasswordController(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Bundle;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.support.annotation.LayoutRes;
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
@@ -21,13 +22,15 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
|||||||
import org.briarproject.briar.android.forum.ForumModule;
|
import org.briarproject.briar.android.forum.ForumModule;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||||
|
import org.briarproject.briar.android.util.UiUtils;
|
||||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
|
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
|
||||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
|
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
|
||||||
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
||||||
|
import org.briarproject.briar.api.android.ScreenFilterMonitor.AppDetails;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -48,7 +51,10 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
private final List<ActivityLifecycleController> lifecycleControllers =
|
private final List<ActivityLifecycleController> lifecycleControllers =
|
||||||
new ArrayList<>();
|
new ArrayList<>();
|
||||||
private boolean destroyed = false;
|
private boolean destroyed = false;
|
||||||
private ScreenFilterDialogFragment dialogFrag;
|
|
||||||
|
@Nullable
|
||||||
|
private Toolbar toolbar = null;
|
||||||
|
private boolean searchedForToolbar = false;
|
||||||
|
|
||||||
public abstract void injectActivity(ActivityComponent component);
|
public abstract void injectActivity(ActivityComponent component);
|
||||||
|
|
||||||
@@ -57,8 +63,8 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle state) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(state);
|
||||||
|
|
||||||
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
|
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
|
||||||
|
|
||||||
@@ -97,6 +103,16 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
for (ActivityLifecycleController alc : lifecycleControllers) {
|
for (ActivityLifecycleController alc : lifecycleControllers) {
|
||||||
alc.onActivityStart();
|
alc.onActivityStart();
|
||||||
}
|
}
|
||||||
|
protectToolbar();
|
||||||
|
ScreenFilterDialogFragment f = findDialogFragment();
|
||||||
|
if (f != null) f.setDismissListener(this::protectToolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ScreenFilterDialogFragment findDialogFragment() {
|
||||||
|
Fragment f = getSupportFragmentManager().findFragmentByTag(
|
||||||
|
ScreenFilterDialogFragment.TAG);
|
||||||
|
return (ScreenFilterDialogFragment) f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -107,15 +123,6 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (dialogFrag != null) {
|
|
||||||
dialogFrag.dismiss();
|
|
||||||
dialogFrag = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void showInitialFragment(BaseFragment f) {
|
protected void showInitialFragment(BaseFragment f) {
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||||
@@ -132,16 +139,27 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showScreenFilterWarning() {
|
private boolean showScreenFilterWarning() {
|
||||||
if (dialogFrag != null && dialogFrag.isVisible()) return;
|
// If the dialog is already visible, filter the tap
|
||||||
Set<String> apps = screenFilterMonitor.getApps();
|
ScreenFilterDialogFragment f = findDialogFragment();
|
||||||
if (apps.isEmpty()) return;
|
if (f != null && f.isVisible()) return false;
|
||||||
dialogFrag =
|
Collection<AppDetails> apps = screenFilterMonitor.getApps();
|
||||||
ScreenFilterDialogFragment.newInstance(new ArrayList<>(apps));
|
// If all overlay apps have been allowed, allow the tap
|
||||||
dialogFrag.setCancelable(false);
|
if (apps.isEmpty()) return true;
|
||||||
// Show dialog unless onSaveInstanceState() has been called, see #1112
|
// Show dialog unless onSaveInstanceState() has been called, see #1112
|
||||||
FragmentManager fm = getSupportFragmentManager();
|
FragmentManager fm = getSupportFragmentManager();
|
||||||
if (!fm.isStateSaved()) dialogFrag.show(fm, dialogFrag.getTag());
|
if (!fm.isStateSaved()) {
|
||||||
|
// Create dialog
|
||||||
|
f = ScreenFilterDialogFragment.newInstance(apps);
|
||||||
|
// When dialog is dismissed, update protection of toolbar
|
||||||
|
f.setDismissListener(this::protectToolbar);
|
||||||
|
// Hide soft keyboard when (re)showing dialog
|
||||||
|
View focus = getCurrentFocus();
|
||||||
|
if (focus != null) hideSoftKeyboard(focus);
|
||||||
|
f.show(fm, ScreenFilterDialogFragment.TAG);
|
||||||
|
}
|
||||||
|
// Filter the tap
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -195,15 +213,25 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
* is outside the wrapper.
|
* is outside the wrapper.
|
||||||
*/
|
*/
|
||||||
private void protectToolbar() {
|
private void protectToolbar() {
|
||||||
View decorView = getWindow().getDecorView();
|
findToolbar();
|
||||||
if (decorView instanceof ViewGroup) {
|
if (toolbar != null) {
|
||||||
Toolbar toolbar = findToolbar((ViewGroup) decorView);
|
boolean filter = !screenFilterMonitor.getApps().isEmpty();
|
||||||
if (toolbar != null) toolbar.setFilterTouchesWhenObscured(true);
|
UiUtils.setFilterTouchesWhenObscured(toolbar, filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void findToolbar() {
|
||||||
|
if (searchedForToolbar) return;
|
||||||
|
View decorView = getWindow().getDecorView();
|
||||||
|
if (decorView instanceof ViewGroup)
|
||||||
|
toolbar = findToolbar((ViewGroup) decorView);
|
||||||
|
searchedForToolbar = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Toolbar findToolbar(ViewGroup vg) {
|
private Toolbar findToolbar(ViewGroup vg) {
|
||||||
|
// Views inside tap-safe layouts are already protected
|
||||||
|
if (vg instanceof TapSafeFrameLayout) return null;
|
||||||
for (int i = 0, len = vg.getChildCount(); i < len; i++) {
|
for (int i = 0, len = vg.getChildCount(); i < len; i++) {
|
||||||
View child = vg.getChildAt(i);
|
View child = vg.getChildAt(i);
|
||||||
if (child instanceof Toolbar) return (Toolbar) child;
|
if (child instanceof Toolbar) return (Toolbar) child;
|
||||||
@@ -223,23 +251,20 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
public void setContentView(View v) {
|
public void setContentView(View v) {
|
||||||
super.setContentView(makeTapSafeWrapper(v));
|
super.setContentView(makeTapSafeWrapper(v));
|
||||||
protectToolbar();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContentView(View v, LayoutParams layoutParams) {
|
public void setContentView(View v, LayoutParams layoutParams) {
|
||||||
super.setContentView(makeTapSafeWrapper(v), layoutParams);
|
super.setContentView(makeTapSafeWrapper(v), layoutParams);
|
||||||
protectToolbar();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addContentView(View v, LayoutParams layoutParams) {
|
public void addContentView(View v, LayoutParams layoutParams) {
|
||||||
super.addContentView(makeTapSafeWrapper(v), layoutParams);
|
super.addContentView(makeTapSafeWrapper(v), layoutParams);
|
||||||
protectToolbar();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTapFiltered() {
|
public boolean shouldAllowTap() {
|
||||||
showScreenFilterWarning();
|
return showScreenFilterWarning();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public abstract class BriarActivity extends BaseActivity {
|
|||||||
public void setSceneTransitionAnimation() {
|
public void setSceneTransitionAnimation() {
|
||||||
if (SDK_INT < 21) return;
|
if (SDK_INT < 21) return;
|
||||||
// workaround for #1007
|
// workaround for #1007
|
||||||
if (isSamsung7(this)) {
|
if (isSamsung7()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Transition slide = new Slide(Gravity.RIGHT);
|
Transition slide = new Slide(Gravity.RIGHT);
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
i.putExtra(GROUP_ID, item.getGroupId().getBytes());
|
i.putExtra(GROUP_ID, item.getGroupId().getBytes());
|
||||||
i.putExtra(POST_ID, item.getId().getBytes());
|
i.putExtra(POST_ID, item.getId().getBytes());
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 23 && !isSamsung7(ctx)) {
|
if (Build.VERSION.SDK_INT >= 23 && !isSamsung7()) {
|
||||||
ActivityOptionsCompat options =
|
ActivityOptionsCompat options =
|
||||||
makeSceneTransitionAnimation((Activity) ctx, layout,
|
makeSceneTransitionAnimation((Activity) ctx, layout,
|
||||||
getTransitionName(item.getId()));
|
getTransitionName(item.getId()));
|
||||||
|
|||||||
@@ -1,40 +1,106 @@
|
|||||||
package org.briarproject.briar.android.fragment;
|
package org.briarproject.briar.android.fragment;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.activity.BaseActivity;
|
||||||
|
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
||||||
|
import org.briarproject.briar.api.android.ScreenFilterMonitor.AppDetails;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@NotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
public class ScreenFilterDialogFragment extends DialogFragment {
|
public class ScreenFilterDialogFragment extends DialogFragment {
|
||||||
|
|
||||||
|
public static final String TAG = ScreenFilterDialogFragment.class.getName();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ScreenFilterMonitor screenFilterMonitor;
|
||||||
|
|
||||||
|
DismissListener dismissListener = null;
|
||||||
|
|
||||||
public static ScreenFilterDialogFragment newInstance(
|
public static ScreenFilterDialogFragment newInstance(
|
||||||
ArrayList<String> apps) {
|
Collection<AppDetails> apps) {
|
||||||
ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment();
|
ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment();
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putStringArrayList("apps", apps);
|
ArrayList<String> appNames = new ArrayList<>();
|
||||||
|
for (AppDetails a : apps) appNames.add(a.name);
|
||||||
|
args.putStringArrayList("appNames", appNames);
|
||||||
|
ArrayList<String> packageNames = new ArrayList<>();
|
||||||
|
for (AppDetails a : apps) packageNames.add(a.packageName);
|
||||||
|
args.putStringArrayList("packageNames", packageNames);
|
||||||
frag.setArguments(args);
|
frag.setArguments(args);
|
||||||
return frag;
|
return frag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDismissListener(DismissListener dismissListener) {
|
||||||
|
this.dismissListener = dismissListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
Activity activity = getActivity();
|
||||||
|
if (activity == null) throw new IllegalStateException();
|
||||||
|
((BaseActivity) activity).getActivityComponent().inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
|
Activity activity = getActivity();
|
||||||
|
if (activity == null) throw new IllegalStateException();
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity,
|
||||||
R.style.BriarDialogThemeNoFilter);
|
R.style.BriarDialogThemeNoFilter);
|
||||||
builder.setTitle(R.string.screen_filter_title);
|
builder.setTitle(R.string.screen_filter_title);
|
||||||
ArrayList<String> apps = getArguments().getStringArrayList("apps");
|
Bundle args = getArguments();
|
||||||
builder.setMessage(getString(R.string.screen_filter_body,
|
if (args == null) throw new IllegalStateException();
|
||||||
TextUtils.join("\n", apps)));
|
ArrayList<String> appNames = args.getStringArrayList("appNames");
|
||||||
builder.setNeutralButton(R.string.continue_button,
|
ArrayList<String> packageNames =
|
||||||
(dialog, which) -> dialog.dismiss());
|
args.getStringArrayList("packageNames");
|
||||||
|
if (appNames == null || packageNames == null)
|
||||||
|
throw new IllegalStateException();
|
||||||
|
LayoutInflater inflater = activity.getLayoutInflater();
|
||||||
|
// See https://stackoverflow.com/a/24720976/6314875
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null);
|
||||||
|
builder.setView(dialogView);
|
||||||
|
TextView message = dialogView.findViewById(R.id.screen_filter_message);
|
||||||
|
message.setText(getString(R.string.screen_filter_body,
|
||||||
|
TextUtils.join("\n", appNames)));
|
||||||
|
CheckBox allow = dialogView.findViewById(R.id.screen_filter_checkbox);
|
||||||
|
builder.setNeutralButton(R.string.continue_button, (dialog, which) -> {
|
||||||
|
if (allow.isChecked()) screenFilterMonitor.allowApps(packageNames);
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
builder.setCancelable(false);
|
||||||
return builder.create();
|
return builder.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
super.onDismiss(dialog);
|
||||||
|
if (dismissListener != null) dismissListener.onDialogDismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface DismissListener {
|
||||||
|
void onDialogDismissed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.util.AttributeSet;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
@@ -38,18 +39,21 @@ import static java.util.logging.Level.WARNING;
|
|||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||||
AutoFocusCallback {
|
AutoFocusCallback, View.OnClickListener {
|
||||||
|
|
||||||
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
|
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(CameraView.class.getName());
|
Logger.getLogger(CameraView.class.getName());
|
||||||
|
|
||||||
|
private final Runnable autoFocusRetry = this::retryAutoFocus;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Camera camera = null;
|
private Camera camera = null;
|
||||||
private PreviewConsumer previewConsumer = null;
|
private PreviewConsumer previewConsumer = null;
|
||||||
private Surface surface = null;
|
private Surface surface = null;
|
||||||
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
|
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
|
||||||
private boolean previewStarted = false, autoFocus = false;
|
private boolean previewStarted = false;
|
||||||
|
private boolean autoFocusSupported = false, autoFocusRunning = false;
|
||||||
|
|
||||||
public CameraView(Context context) {
|
public CameraView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -74,6 +78,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
|||||||
super.onAttachedToWindow();
|
super.onAttachedToWindow();
|
||||||
setKeepScreenOn(true);
|
setKeepScreenOn(true);
|
||||||
getHolder().addCallback(this);
|
getHolder().addCallback(this);
|
||||||
|
setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -157,27 +162,41 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
|||||||
@UiThread
|
@UiThread
|
||||||
private void startConsumer() throws CameraException {
|
private void startConsumer() throws CameraException {
|
||||||
if (camera == null) throw new CameraException("Camera is null");
|
if (camera == null) throw new CameraException("Camera is null");
|
||||||
if (autoFocus) {
|
startAutoFocus();
|
||||||
|
previewConsumer.start(camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
private void startAutoFocus() throws CameraException {
|
||||||
|
if (camera != null && autoFocusSupported && !autoFocusRunning) {
|
||||||
try {
|
try {
|
||||||
|
removeCallbacks(autoFocusRetry);
|
||||||
camera.autoFocus(this);
|
camera.autoFocus(this);
|
||||||
|
autoFocusRunning = true;
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
throw new CameraException(e);
|
throw new CameraException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previewConsumer.start(camera);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void stopConsumer() throws CameraException {
|
private void stopConsumer() throws CameraException {
|
||||||
if (camera == null) throw new CameraException("Camera is null");
|
if (camera == null) throw new CameraException("Camera is null");
|
||||||
if (autoFocus) {
|
cancelAutoFocus();
|
||||||
|
previewConsumer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
private void cancelAutoFocus() throws CameraException {
|
||||||
|
if (camera != null && autoFocusSupported && autoFocusRunning) {
|
||||||
try {
|
try {
|
||||||
|
removeCallbacks(autoFocusRetry);
|
||||||
camera.cancelAutoFocus();
|
camera.cancelAutoFocus();
|
||||||
|
autoFocusRunning = false;
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
throw new CameraException(e);
|
throw new CameraException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previewConsumer.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@@ -325,7 +344,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void enableAutoFocus(String focusMode) {
|
private void enableAutoFocus(String focusMode) {
|
||||||
autoFocus = FOCUS_MODE_AUTO.equals(focusMode) ||
|
autoFocusSupported = FOCUS_MODE_AUTO.equals(focusMode) ||
|
||||||
FOCUS_MODE_MACRO.equals(focusMode);
|
FOCUS_MODE_MACRO.equals(focusMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,16 +446,23 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAutoFocus(boolean success, Camera camera) {
|
public void onAutoFocus(boolean success, Camera camera) {
|
||||||
LOG.info("Auto focus succeeded: " + success);
|
if (LOG.isLoggable(INFO))
|
||||||
postDelayed(this::retryAutoFocus, AUTO_FOCUS_RETRY_DELAY);
|
LOG.info("Auto focus succeeded: " + success);
|
||||||
|
autoFocusRunning = false;
|
||||||
|
postDelayed(autoFocusRetry, AUTO_FOCUS_RETRY_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void retryAutoFocus() {
|
private void retryAutoFocus() {
|
||||||
try {
|
try {
|
||||||
if (camera != null) camera.autoFocus(this);
|
startAutoFocus();
|
||||||
} catch (RuntimeException e) {
|
} catch (CameraException e) {
|
||||||
LOG.log(WARNING, "Error retrying auto focus", e);
|
LOG.log(WARNING, e.toString(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
retryAutoFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +46,8 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static android.os.Build.MANUFACTURER;
|
||||||
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static android.support.v4.app.FragmentManager.POP_BACK_STACK_INCLUSIVE;
|
import static android.support.v4.app.FragmentManager.POP_BACK_STACK_INCLUSIVE;
|
||||||
import static android.support.v4.view.GravityCompat.START;
|
import static android.support.v4.view.GravityCompat.START;
|
||||||
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
|
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
|
||||||
@@ -215,10 +217,15 @@ public class NavDrawerActivity extends BriarActivity implements
|
|||||||
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
|
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
|
||||||
getSupportFragmentManager()
|
getSupportFragmentManager()
|
||||||
.findFragmentByTag(ContactListFragment.TAG) != null) {
|
.findFragmentByTag(ContactListFragment.TAG) != null) {
|
||||||
Intent i = new Intent(Intent.ACTION_MAIN);
|
if (SDK_INT == 19 && MANUFACTURER.equalsIgnoreCase("Samsung")) {
|
||||||
i.addCategory(Intent.CATEGORY_HOME);
|
// workaround for #1116 causes splash screen to show again
|
||||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
super.onBackPressed();
|
||||||
startActivity(i);
|
} else {
|
||||||
|
Intent i = new Intent(Intent.ACTION_MAIN);
|
||||||
|
i.addCategory(Intent.CATEGORY_HOME);
|
||||||
|
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
|
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
|
||||||
getSupportFragmentManager()
|
getSupportFragmentManager()
|
||||||
.findFragmentByTag(ContactListFragment.TAG) == null) {
|
.findFragmentByTag(ContactListFragment.TAG) == null) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Context;
|
|||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.support.design.widget.TextInputLayout;
|
import android.support.design.widget.TextInputLayout;
|
||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
@@ -162,7 +161,7 @@ public class UiUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean needsDozeWhitelisting(Context ctx) {
|
public static boolean needsDozeWhitelisting(Context ctx) {
|
||||||
if (Build.VERSION.SDK_INT < 23) return false;
|
if (SDK_INT < 23) return false;
|
||||||
PowerManager pm = (PowerManager) ctx.getSystemService(POWER_SERVICE);
|
PowerManager pm = (PowerManager) ctx.getSystemService(POWER_SERVICE);
|
||||||
String packageName = ctx.getPackageName();
|
String packageName = ctx.getPackageName();
|
||||||
if (pm == null) throw new AssertionError();
|
if (pm == null) throw new AssertionError();
|
||||||
@@ -178,8 +177,15 @@ public class UiUtils {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isSamsung7(Context context) {
|
public static boolean isSamsung7() {
|
||||||
return SDK_INT == 24 && MANUFACTURER.equalsIgnoreCase("Samsung");
|
return SDK_INT == 24 && MANUFACTURER.equalsIgnoreCase("Samsung");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setFilterTouchesWhenObscured(View v, boolean filter) {
|
||||||
|
v.setFilterTouchesWhenObscured(filter);
|
||||||
|
// Workaround for Android bug #13530806, see
|
||||||
|
// https://android.googlesource.com/platform/frameworks/base/+/aba566589e0011c4b973c0d4f77be4e9ee176089%5E%21/core/java/android/view/View.java
|
||||||
|
if (v.getFilterTouchesWhenObscured() != filter)
|
||||||
|
v.setFilterTouchesWhenObscured(!filter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.MotionEvent;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.android.util.UiUtils;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@@ -20,18 +21,18 @@ public class TapSafeFrameLayout extends FrameLayout {
|
|||||||
|
|
||||||
public TapSafeFrameLayout(Context context) {
|
public TapSafeFrameLayout(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
setFilterTouchesWhenObscured(false);
|
UiUtils.setFilterTouchesWhenObscured(this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TapSafeFrameLayout(Context context, @Nullable AttributeSet attrs) {
|
public TapSafeFrameLayout(Context context, @Nullable AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
setFilterTouchesWhenObscured(false);
|
UiUtils.setFilterTouchesWhenObscured(this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TapSafeFrameLayout(Context context, @Nullable AttributeSet attrs,
|
public TapSafeFrameLayout(Context context, @Nullable AttributeSet attrs,
|
||||||
@AttrRes int defStyleAttr) {
|
@AttrRes int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
setFilterTouchesWhenObscured(false);
|
UiUtils.setFilterTouchesWhenObscured(this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnTapFilteredListener(OnTapFilteredListener listener) {
|
public void setOnTapFilteredListener(OnTapFilteredListener listener) {
|
||||||
@@ -40,12 +41,12 @@ public class TapSafeFrameLayout extends FrameLayout {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onFilterTouchEventForSecurity(MotionEvent e) {
|
public boolean onFilterTouchEventForSecurity(MotionEvent e) {
|
||||||
boolean filter = (e.getFlags() & FLAG_WINDOW_IS_OBSCURED) != 0;
|
boolean obscured = (e.getFlags() & FLAG_WINDOW_IS_OBSCURED) != 0;
|
||||||
if (filter && listener != null) listener.onTapFiltered();
|
if (obscured && listener != null) return listener.shouldAllowTap();
|
||||||
return !filter;
|
else return !obscured;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnTapFilteredListener {
|
public interface OnTapFilteredListener {
|
||||||
void onTapFiltered();
|
boolean shouldAllowTap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,37 @@ package org.briarproject.briar.api.android;
|
|||||||
|
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
|
|
||||||
import java.util.Set;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
public interface ScreenFilterMonitor {
|
public interface ScreenFilterMonitor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the details of all apps that have requested the
|
||||||
|
* SYSTEM_ALERT_WINDOW permission, excluding system apps, Google Play
|
||||||
|
* Services, and any apps that have been allowed by calling
|
||||||
|
* {@link #allowApps(Collection)}.
|
||||||
|
*/
|
||||||
@UiThread
|
@UiThread
|
||||||
Set<String> getApps();
|
Collection<AppDetails> getApps();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the apps with the given package names to use overlay windows.
|
||||||
|
* They will not be returned by future calls to {@link #getApps()}.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void allowApps(Collection<String> packageNames);
|
||||||
|
|
||||||
|
class AppDetails {
|
||||||
|
|
||||||
|
public final String name;
|
||||||
|
public final String packageName;
|
||||||
|
|
||||||
|
public AppDetails(String name, String packageName) {
|
||||||
|
this.name = name;
|
||||||
|
this.packageName = packageName;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
briar-android/src/main/res/layout/dialog_screen_filter.xml
Normal file
28
briar-android/src/main/res/layout/dialog_screen_filter.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/margin_large">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/screen_filter_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/screen_filter_checkbox"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:layout_marginTop="@dimen/margin_large"
|
||||||
|
android:text="@string/screen_filter_allow"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -395,7 +395,8 @@
|
|||||||
|
|
||||||
<!-- Screen Filters & Tapjacking -->
|
<!-- Screen Filters & Tapjacking -->
|
||||||
<string name="screen_filter_title">Screen overlay detected</string>
|
<string name="screen_filter_title">Screen overlay detected</string>
|
||||||
<string name="screen_filter_body">Another app is drawing on top of Briar. To protect your security, Briar will not respond to touches when another app is drawing on top.\n\nTry turning off the following apps when using Briar:\n\n%1$s</string>
|
<string name="screen_filter_body">Another app is drawing on top of Briar. To protect your security, Briar will not respond to touches when another app is drawing on top.\n\nThe following apps might be drawing on top:\n\n%1$s</string>
|
||||||
|
<string name="screen_filter_allow">Allow these apps to draw on top</string>
|
||||||
|
|
||||||
<!-- Permission Requests -->
|
<!-- Permission Requests -->
|
||||||
<string name="permission_camera_title">Camera permission</string>
|
<string name="permission_camera_title">Camera permission</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user