Compare commits

..

33 Commits

Author SHA1 Message Date
Santiago Torres
8bb55d2580 WIP: BluetoothPlugin: adds abstract superclass
The droidtooth plugin and bluetooth plugins share a lot of common code.
Add an abstract superclass that shares the common code between both
classes.
2017-05-01 01:16:38 -04:00
Ernir Erlingsson
6de539a62d Merge branch '791-permanent-input' into 'master'
Show text input permanently in threaded conversations

Closes #791

See merge request !526
2017-04-27 10:38:58 +00:00
Ernir Erlingsson
34704ec04d Merge branch '874-tree-indicator' into 'master'
Darken thread indicator

Closes #874

See merge request !525
2017-04-26 08:38:52 +00:00
akwizgran
9fd6d46583 Merge branch '871-increase-socket-timeout' into 'master'
Increase socket timeout for Tor sockets

See merge request !519
2017-04-19 16:53:59 +00:00
Torsten Grote
920f3581fa Show text input permanently in threaded conversations 2017-04-17 16:22:24 -03:00
Torsten Grote
45e7af31fe Darken thread indicator 2017-04-17 16:14:26 -03:00
Torsten Grote
67d5d8cdf1 Merge branch '941-reblogged-rss-post-has-wrong-icon' into 'master'
Store RSS flag for wrapped blog posts

Closes #941

See merge request !524
2017-04-17 18:23:41 +00:00
Torsten Grote
9d8cadb7a9 Merge branch 'use-original-timestamp-for-rss-posts' into 'master'
Use original timestamp for RSS posts, if available

See merge request !523
2017-04-17 18:22:10 +00:00
Torsten Grote
6425c49d04 Merge branch 'remove-single-top-flag' into 'master'
Don't use single top and clear top flags together

See merge request !522
2017-04-17 18:20:46 +00:00
Torsten Grote
68d98b50f2 Merge branch '938-ignore-play-services-overlay-permission' into 'master'
When checking for overlay apps, ignore Play Services

Closes #938

See merge request !521
2017-04-17 18:19:46 +00:00
akwizgran
84986d393f Added a test for #941, fixed some broken tests. 2017-04-13 17:28:45 +01:00
akwizgran
115d488bc3 Clamp the imported timestamp within reasonable limits. 2017-04-13 16:21:00 +01:00
akwizgran
2eeb2213e3 Store RSS flag for wrapped blog posts. 2017-04-13 15:23:08 +01:00
akwizgran
1b48d661e8 Use original timestamp for RSS posts, if available. 2017-04-13 14:43:43 +01:00
akwizgran
49ba66dee9 Don't use single top and clear top flags together. 2017-04-13 13:56:20 +01:00
akwizgran
46920f3bce Merge branch '892-separate-rss-blog' into 'master'
Separate RSS posts from personal blog posts

Closes #892

See merge request !520
2017-04-13 10:15:00 +00:00
Torsten Grote
4b955809f7 Address review comments 2017-04-12 15:18:27 -03:00
akwizgran
57d4d6546a When checking for overlay apps, ignore Play Services. 2017-04-12 14:24:37 +01:00
Torsten Grote
9bfb58a764 Show blog posts from RSS feeds with a dedicated icon
This adds a field to the post headers and some more tests.
2017-04-12 08:43:24 -03:00
Torsten Grote
0256ec0b8c Show reblog icon only for reblogged posts 2017-04-12 08:43:23 -03:00
Torsten Grote
b0b4a85d15 Add integration test for FeedManager
Attention: This factors out a DnsModule to be able to make actual
non-Tor DNS lookups for testing.
2017-04-12 08:43:23 -03:00
Torsten Grote
d40a058ef5 Change blog descriptor format to include RSS feed flag
This now also handles the case where an RSS blog is deleted via the blog
deletion option and not the feed management.
2017-04-12 08:43:22 -03:00
Torsten Grote
58b9efb24c Open feed's blog when clicking it in 'manage activity' 2017-04-12 08:43:22 -03:00
Torsten Grote
17de785c12 Remove blog as well when removing RSS feed
This also adds a confirmation dialog to the removal process.
2017-04-12 08:43:21 -03:00
Torsten Grote
c7ff1ba974 Store RSS feeds in a separate dedicated blog
A fake LocalAuthor is created for this new blog and stored in the feed's metadata.
2017-04-12 08:43:21 -03:00
akwizgran
d17669f131 Increase socket timeout for Tor sockets. 2017-04-11 14:53:03 +01:00
akwizgran
9755cd9ab4 Merge branch '891-messages-not-acked' into 'master'
Fix MessageId calculation for deprecated MessageQueue

Closes #891

See merge request !514
2017-04-11 12:49:44 +00:00
akwizgran
6d2b18facc Merge branch '799-explain-content-visibility' into 'master'
Show explanation about visibility in member lists

Closes #799

See merge request !516
2017-04-07 14:54:41 +00:00
Torsten Grote
f8cf7034db Show explanation about visibility in member lists 2017-04-07 11:38:33 -03:00
akwizgran
a1e65c9fa7 Merge branch '893-double-introduction-accept' into 'master'
Prevent conversation actions from being executed twice

Closes #893

See merge request !512
2017-04-07 14:03:40 +00:00
Torsten Grote
499d2fe677 Prevent conversation actions from being executed twice 2017-04-07 10:00:55 -03:00
Torsten Grote
85c17b4cb0 Fix MessageId calculation for deprecated MessageQueue
This was preventing introduction messages from getting ACKed.
The introduction tests were modified to check for this.
2017-04-07 09:45:35 -03:00
Torsten Grote
0827b067ec Harmonize position of boolean message variables 2017-04-06 15:42:12 -03:00
80 changed files with 1477 additions and 725 deletions

View File

@@ -19,8 +19,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractBluetoothPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
@@ -75,7 +74,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class DroidtoothPlugin implements DuplexPlugin {
class DroidtoothPlugin<C, S>
extends AbstractBluetoothPlugin<C, S>{
private static final Logger LOG =
Logger.getLogger(DroidtoothPlugin.class.getName());
@@ -84,16 +84,10 @@ class DroidtoothPlugin implements DuplexPlugin {
private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final DuplexPluginCallback callback;
private final int maxLatency;
private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false;
private volatile boolean wasEnabledByUs = false;
private volatile BluetoothStateReceiver receiver = null;
private volatile BluetoothServerSocket socket = null;
@@ -104,29 +98,15 @@ class DroidtoothPlugin implements DuplexPlugin {
DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Backoff backoff,
DuplexPluginCallback callback, int maxLatency) {
this.ioExecutor = ioExecutor;
super(ioExecutor, secureRandom, backoff, maxLatency, callback);
this.androidExecutor = androidExecutor;
this.appContext = appContext;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
protected void close(S ss) throws IOException {
((BluetoothServerSocket)ss).close();
}
@Override
@@ -194,14 +174,14 @@ class DroidtoothPlugin implements DuplexPlugin {
BluetoothServerSocket ss;
try {
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
"RFCOMM", getUuid());
"RFCOMM", UUID.fromString(getUuid()));
} catch (IOException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return;
}
if (!isRunning()) {
tryToClose(ss);
tryToClose((S)ss);
return;
}
LOG.info("Socket bound");
@@ -213,29 +193,6 @@ class DroidtoothPlugin implements DuplexPlugin {
});
}
private UUID getUuid() {
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return UUID.fromString(uuid);
}
private void tryToClose(@Nullable BluetoothServerSocket ss) {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} finally {
callback.transportDisabled();
}
}
private void acceptContactConnections() {
while (isRunning()) {
BluetoothSocket s;
@@ -261,9 +218,9 @@ class DroidtoothPlugin implements DuplexPlugin {
@Override
public void stop() {
running = false;
this.running = false;
if (receiver != null) appContext.unregisterReceiver(receiver);
tryToClose(socket);
tryToClose((S)socket);
// Disable Bluetooth if we enabled it and it's still enabled
if (wasEnabledByUs && adapter.isEnabled()) {
if (adapter.disable()) LOG.info("Disabling Bluetooth");
@@ -276,42 +233,20 @@ class DroidtoothPlugin implements DuplexPlugin {
return running && adapter != null && adapter.isEnabled();
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(Collection<ContactId> connected) {
if (!isRunning()) return;
backoff.increment();
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if (connected.contains(c)) continue;
final String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(new Runnable() {
@Override
public void run() {
if (!running) return;
BluetoothSocket s = connect(address, uuid);
if (s != null) {
backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s));
}
protected Runnable returnPollRunnable(final String address, final String uuid,
final ContactId c) {
return new Runnable() {
@Override
public void run() {
if (!running) return;
BluetoothSocket s = connect(address, uuid);
if (s != null) {
backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s));
}
});
}
}
};
}
@Nullable
@@ -347,37 +282,17 @@ class DroidtoothPlugin implements DuplexPlugin {
LOG.info("Failed to connect to " + scrubMacAddress(address)
+ ": " + e);
}
tryToClose(s);
tryToClose((S)s);
return null;
}
}
private void tryToClose(@Nullable Closeable c) {
try {
if (c != null) c.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
if (!isRunning()) return null;
TransportProperties p = callback.getRemoteProperties().get(c);
if (p == null) return null;
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
public DuplexTransportConnection connectToAddress(String address, String uuid) {
BluetoothSocket s = connect(address, uuid);
if (s == null) return null;
return new DroidtoothTransportConnection(this, s);
return s == null ? null : wrapSocket(s);
}
@Override
public boolean supportsInvitations() {
return true;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
@@ -426,7 +341,7 @@ class DroidtoothPlugin implements DuplexPlugin {
return null;
} finally {
// Closing the socket will terminate the listener task
tryToClose(ss);
tryToClose((S)ss);
closeSockets(futures, chosen);
}
}
@@ -458,11 +373,6 @@ class DroidtoothPlugin implements DuplexPlugin {
});
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!isRunning()) return null;
@@ -487,31 +397,6 @@ class DroidtoothPlugin implements DuplexPlugin {
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) {
if (!isRunning()) return null;
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(commitment);
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
BluetoothSocket s = connect(address, uuid.toString());
if (s == null) return null;
return new DroidtoothTransportConnection(this, s);
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return StringUtils.macToString(mac);
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@@ -523,7 +408,7 @@ class DroidtoothPlugin implements DuplexPlugin {
bind();
} else if (state == STATE_OFF) {
LOG.info("Bluetooth disabled");
tryToClose(socket);
tryToClose((S)socket);
}
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
if (scanMode == SCAN_MODE_NONE) {

View File

@@ -13,7 +13,9 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault
public class Author {
public enum Status {ANONYMOUS, UNKNOWN, UNVERIFIED, VERIFIED, OURSELVES}
public enum Status {
NONE, ANONYMOUS, UNKNOWN, UNVERIFIED, VERIFIED, OURSELVES
}
private final AuthorId id;
private final String name;

View File

@@ -8,6 +8,7 @@ public interface TorConstants {
int CONTROL_PORT = 59051;
int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds
int EXTRA_SOCKET_TIMEOUT = 30000; // Milliseconds
String PREF_TOR_NETWORK = "network";
String PREF_TOR_PORT = "port";

View File

@@ -0,0 +1,190 @@
package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.util.StringUtils;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Created by Santiago Torres-Arias on 1/10/17.
*/
public abstract class AbstractBluetoothPlugin<C, S> implements DuplexPlugin {
private static final Logger LOG =
Logger.getLogger("Halp");
protected final Executor ioExecutor;
protected final SecureRandom secureRandom;
protected final Backoff backoff;
protected final int maxLatency;
protected final DuplexPluginCallback callback;
protected final S ss = null;
protected volatile boolean running = false;
protected Runnable pollRunnable = null;
public AbstractBluetoothPlugin(Executor ioExecutor,
SecureRandom secureRandom,
Backoff backoff, int maxLatency,
DuplexPluginCallback callback) {
this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.maxLatency = maxLatency;
this.callback = callback;
}
@Override
public boolean supportsInvitations() {
return true;
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public boolean isRunning() {
return running;
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
protected String getUuid() {
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return uuid;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
}
protected String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return StringUtils.macToString(mac);
}
protected void tryToClose(@Nullable S ss) {
try {
if (ss != null) close(ss);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} finally {
callback.transportDisabled();
}
}
protected abstract void close(S ss) throws IOException;
public void stop() {
running = false;
tryToClose(ss);
}
@Override
public void poll(final Collection<ContactId> connected) {
if (!running) return;
backoff.increment();
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Map.Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if (connected.contains(c)) continue;
final String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(returnPollRunnable(address,uuid, c));
}
}
protected abstract Runnable returnPollRunnable(String address, String uuid,
ContactId c);
@Override
public DuplexTransportConnection createConnection(ContactId c) {
if (!isRunning()) return null;
TransportProperties p = callback.getRemoteProperties().get(c);
if (p == null) return null;
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
return connectToAddress(address, uuid);
}
protected abstract DuplexTransportConnection connectToAddress(String address, String uuid);
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) {
if (!isRunning()) return null;
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
return connectToAddress(address, uuid);
}
}

View File

@@ -668,7 +668,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
acked.add(m);
}
}
transaction.attach(new MessagesAckedEvent(c, acked));
if (acked.size() > 0) {
transaction.attach(new MessagesAckedEvent(c, acked));
}
}
@Override

View File

@@ -68,8 +68,8 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
@NotNullByDefault
abstract class JdbcDatabase implements Database<Connection> {
private static final int SCHEMA_VERSION = 29;
private static final int MIN_SCHEMA_VERSION = 29;
private static final int SCHEMA_VERSION = 30;
private static final int MIN_SCHEMA_VERSION = 30;
private static final String CREATE_SETTINGS =
"CREATE TABLE settings"

View File

@@ -8,6 +8,7 @@ import dagger.Module;
import dagger.Provides;
import static org.briarproject.bramble.api.plugin.TorConstants.CONNECT_TO_PROXY_TIMEOUT;
import static org.briarproject.bramble.api.plugin.TorConstants.EXTRA_SOCKET_TIMEOUT;
import static org.briarproject.bramble.api.plugin.TorConstants.SOCKS_PORT;
@Module
@@ -17,6 +18,7 @@ public class SocksModule {
SocketFactory provideTorSocketFactory() {
InetSocketAddress proxy = new InetSocketAddress("127.0.0.1",
SOCKS_PORT);
return new SocksSocketFactory(proxy, CONNECT_TO_PROXY_TIMEOUT);
return new SocksSocketFactory(proxy, CONNECT_TO_PROXY_TIMEOUT,
EXTRA_SOCKET_TIMEOUT);
}
}

View File

@@ -29,11 +29,13 @@ class SocksSocket extends Socket {
private static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
private final SocketAddress proxy;
private final int connectToProxyTimeout;
private final int connectToProxyTimeout, extraSocketTimeout;
SocksSocket(SocketAddress proxy, int connectToProxyTimeout) {
SocksSocket(SocketAddress proxy, int connectToProxyTimeout,
int extraSocketTimeout) {
this.proxy = proxy;
this.connectToProxyTimeout = connectToProxyTimeout;
this.extraSocketTimeout = extraSocketTimeout;
}
@Override
@@ -47,7 +49,7 @@ class SocksSocket extends Socket {
InetAddress address = inet.getAddress();
if (address != null
&& !Arrays.equals(address.getAddress(), UNSPECIFIED_ADDRESS)) {
throw new IllegalArgumentException();
throw new IllegalArgumentException();
}
String host = inet.getHostName();
if (host.length() > 255) throw new IllegalArgumentException();
@@ -62,16 +64,16 @@ class SocksSocket extends Socket {
sendMethodRequest(out);
receiveMethodResponse(in);
// Use the supplied timeout temporarily
// Use the supplied timeout temporarily, plus any configured extra
int oldTimeout = getSoTimeout();
setSoTimeout(timeout);
setSoTimeout(timeout + extraSocketTimeout);
// Connect to the endpoint via the proxy
sendConnectRequest(out, host, port);
receiveConnectResponse(in);
// Restore the old timeout
setSoTimeout(oldTimeout);
// Restore the old timeout, plus any configured extra
setSoTimeout(oldTimeout + extraSocketTimeout);
}
private void sendMethodRequest(OutputStream out) throws IOException {

View File

@@ -11,16 +11,18 @@ import javax.net.SocketFactory;
class SocksSocketFactory extends SocketFactory {
private final SocketAddress proxy;
private final int connectToProxyTimeout;
private final int connectToProxyTimeout, extraSocketTimeout;
SocksSocketFactory(SocketAddress proxy, int connectToProxyTimeout) {
SocksSocketFactory(SocketAddress proxy, int connectToProxyTimeout,
int extraSocketTimeout) {
this.proxy = proxy;
this.connectToProxyTimeout = connectToProxyTimeout;
this.extraSocketTimeout = extraSocketTimeout;
}
@Override
public Socket createSocket() {
return new SocksSocket(proxy, connectToProxyTimeout);
return new SocksSocket(proxy, connectToProxyTimeout, extraSocketTimeout);
}
@Override

View File

@@ -0,0 +1,16 @@
package org.briarproject.bramble.test;
import javax.net.SocketFactory;
import dagger.Module;
import dagger.Provides;
@Module
public class TestSocksModule {
@Provides
SocketFactory provideSocketFactory() {
return SocketFactory.getDefault();
}
}

View File

@@ -10,14 +10,14 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractBluetoothPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.util.OsUtils;
import org.briarproject.bramble.util.StringUtils;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
@@ -57,46 +57,22 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class BluetoothPlugin implements DuplexPlugin {
class BluetoothPlugin<C, S> extends AbstractBluetoothPlugin<C, S> {
private static final Logger LOG =
Logger.getLogger(BluetoothPlugin.class.getName());
private final Executor ioExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final DuplexPluginCallback callback;
private final int maxLatency;
private final Semaphore discoverySemaphore = new Semaphore(1);
private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false;
private volatile StreamConnectionNotifier socket = null;
private volatile LocalDevice localDevice = null;
BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom,
Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
BluetoothPlugin(Executor ioExecutor,
SecureRandom secureRandom,
Backoff backoff, int maxLatency,
DuplexPluginCallback callback) {
super(ioExecutor, secureRandom, backoff, maxLatency, callback);
}
@Override
@@ -139,7 +115,7 @@ class BluetoothPlugin implements DuplexPlugin {
return;
}
if (!running) {
tryToClose(ss);
tryToClose((S)ss);
return;
}
socket = ss;
@@ -153,29 +129,16 @@ class BluetoothPlugin implements DuplexPlugin {
private String makeUrl(String address, String uuid) {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
}
private String getUuid() {
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return uuid;
}
private void tryToClose(@Nullable StreamConnectionNotifier ss) {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} finally {
callback.transportDisabled();
}
}
//
// private void tryToClose(@Nullable StreamConnectionNotifier ss) {
// try {
// if (ss != null) ss.close();
// } catch (IOException e) {
// if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
// } finally {
// callback.transportDisabled();
// }
// }
private void acceptContactConnections(StreamConnectionNotifier ss) {
while (true) {
@@ -196,54 +159,33 @@ class BluetoothPlugin implements DuplexPlugin {
private DuplexTransportConnection wrapSocket(StreamConnection s) {
return new BluetoothTransportConnection(this, s);
}
//
// @Override
// public void stop() {
// running = false;
// tryToClose(socket);
// }
@Override
public void stop() {
running = false;
tryToClose(socket);
protected void close(S ss) throws IOException {
((StreamConnection)ss).close();
}
@Override
public boolean isRunning() {
return running;
}
public Runnable returnPollRunnable(final String address, final String uuid,
final ContactId c) {
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(final Collection<ContactId> connected) {
if (!running) return;
backoff.increment();
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if (connected.contains(c)) continue;
final String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(new Runnable() {
@Override
public void run() {
if (!running) return;
StreamConnection s = connect(makeUrl(address, uuid));
if (s != null) {
backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s));
}
return new Runnable() {
@Override
public void run() {
if (!running) return;
StreamConnection s = connect(makeUrl(address, uuid));
if (s != null) {
backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s));
}
});
}
}
};
}
private StreamConnection connect(String url) {
@@ -259,25 +201,13 @@ class BluetoothPlugin implements DuplexPlugin {
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
if (!running) return null;
TransportProperties p = callback.getRemoteProperties().get(c);
if (p == null) return null;
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
String url = makeUrl(address, uuid);
protected DuplexTransportConnection connectToAddress(String address, String uuid) {
String url = makeUrl(address, uuid);
StreamConnection s = connect(url);
if (s == null) return null;
return new BluetoothTransportConnection(this, s);
}
@Override
public boolean supportsInvitations() {
return true;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) {
@@ -297,7 +227,7 @@ class BluetoothPlugin implements DuplexPlugin {
return null;
}
if (!running) {
tryToClose(ss);
tryToClose((S)ss);
return null;
}
// Create the background tasks
@@ -330,7 +260,7 @@ class BluetoothPlugin implements DuplexPlugin {
return null;
} finally {
// Closing the socket will terminate the listener task
tryToClose(ss);
tryToClose((S)ss);
closeSockets(futures, chosen);
}
}
@@ -362,11 +292,6 @@ class BluetoothPlugin implements DuplexPlugin {
});
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!running) return null;
@@ -385,7 +310,7 @@ class BluetoothPlugin implements DuplexPlugin {
return null;
}
if (!running) {
tryToClose(ss);
tryToClose((S)ss);
return null;
}
BdfList descriptor = new BdfList();
@@ -395,33 +320,6 @@ class BluetoothPlugin implements DuplexPlugin {
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) {
if (!isRunning()) return null;
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
String url = makeUrl(address, uuid);
StreamConnection s = connect(url);
if (s == null) return null;
return new BluetoothTransportConnection(this, s);
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return StringUtils.macToString(mac);
}
private void makeDeviceDiscoverable() {
// Try to make the device discoverable (requires root on Linux)
try {

View File

@@ -59,7 +59,6 @@ import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE;
import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE;
import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
@@ -310,7 +309,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
ContactId c = contactCounts.keySet().iterator().next();
i.putExtra(CONTACT_ID, c.getInt());
i.setData(Uri.parse(CONTACT_URI + "/" + c.getInt()));
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(ConversationActivity.class);
t.addNextIntent(i);
@@ -319,7 +318,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.setData(Uri.parse(CONTACT_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
@@ -415,7 +414,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
i.putExtra(GROUP_ID, g.getBytes());
String idHex = StringUtils.toHexString(g.getBytes());
i.setData(Uri.parse(GROUP_URI + "/" + idHex));
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(GroupActivity.class);
t.addNextIntent(i);
@@ -424,7 +423,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the group list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_GROUPS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.setData(Uri.parse(GROUP_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
@@ -507,7 +506,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
i.putExtra(GROUP_ID, g.getBytes());
String idHex = StringUtils.toHexString(g.getBytes());
i.setData(Uri.parse(FORUM_URI + "/" + idHex));
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(ForumActivity.class);
t.addNextIntent(i);
@@ -516,7 +515,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the forum list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_FORUMS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.setData(Uri.parse(FORUM_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
@@ -595,7 +594,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the combined blog feed
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_BLOGS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.setData(Uri.parse(BLOG_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
@@ -651,7 +650,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.setData(Uri.parse(CONTACT_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);

View File

@@ -28,7 +28,6 @@ import javax.inject.Inject;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE;
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
@@ -83,8 +82,7 @@ public class BriarService extends Service {
b.setWhen(0); // Don't show the time
b.setOngoing(true);
Intent i = new Intent(this, NavDrawerActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP |
FLAG_ACTIVITY_SINGLE_TOP);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0));
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_SERVICE);

View File

@@ -6,10 +6,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v7.preference.PreferenceManager;
@@ -19,11 +19,16 @@ import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.android.ScreenFilterMonitor;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
@@ -31,32 +36,57 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.Manifest.permission.SYSTEM_ALERT_WINDOW;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.EXTRA_REPLACING;
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.GET_SIGNATURES;
import static java.util.logging.Level.WARNING;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ScreenFilterMonitorImpl extends BroadcastReceiver
implements Service,
ScreenFilterMonitor {
implements Service, ScreenFilterMonitor {
private static final Logger LOG =
Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
private static final String PREF_SCREEN_FILTER_APPS =
"shownScreenFilterApps";
/*
* Ignore Play Services if it uses this package name and public key - it's
* effectively a system app, but not flagged as such on older systems
*/
private static final String PLAY_SERVICES_PACKAGE =
"com.google.android.gms";
private static final String PLAY_SERVICES_PUBLIC_KEY =
"30820120300D06092A864886F70D01010105000382010D0030820108" +
"0282010100AB562E00D83BA208AE0A966F124E29DA11F2AB56D08F58" +
"E2CCA91303E9B754D372F640A71B1DCB130967624E4656A7776A9219" +
"3DB2E5BFB724A91E77188B0E6A47A43B33D9609B77183145CCDF7B2E" +
"586674C9E1565B1F4C6A5955BFF251A63DABF9C55C27222252E875E4" +
"F8154A645F897168C0B1BFC612EABF785769BB34AA7984DC7E2EA276" +
"4CAE8307D8C17154D7EE5F64A51A44A602C249054157DC02CD5F5C0E" +
"55FBEF8519FBE327F0B1511692C5A06F19D18385F5C4DBC2D6B93F68" +
"CC2979C70E18AB93866B3BD5DB8999552A0E3B4C99DF58FB918BEDC1" +
"82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" +
"0B145B6AA192858E79020103";
private final Context appContext;
private final AndroidExecutor androidExecutor;
private final LinkedList<String> appNames = new LinkedList<>();
private final PackageManager pm;
private final SharedPreferences prefs;
private final AtomicBoolean used = new AtomicBoolean(false);
// The following must only be accessed on the UI thread
private final Set<String> apps = new HashSet<>();
private final Set<String> shownApps;
// Used solely for the UiThread
private boolean serviceStarted = false;
@Inject
@@ -75,7 +105,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
@Override
public Void call() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
intentFilter.addAction(ACTION_PACKAGE_ADDED);
intentFilter.addDataScheme("package");
appContext.registerReceiver(ScreenFilterMonitorImpl.this,
intentFilter);
@@ -109,9 +139,8 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
}
private Set<String> getShownScreenFilterApps() {
// res must not be modified
Set<String> s =
prefs.getStringSet(PREF_SCREEN_FILTER_APPS, null);
// Result must not be modified
Set<String> s = prefs.getStringSet(PREF_SCREEN_FILTER_APPS, null);
HashSet<String> result = new HashSet<>();
if (s != null) {
result.addAll(s);
@@ -121,7 +150,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
if (!intent.getBooleanExtra(EXTRA_REPLACING, false)) {
final String packageName =
intent.getData().getEncodedSchemeSpecificPart();
androidExecutor.runOnUiThread(new Runnable() {
@@ -155,7 +184,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
@Override
@UiThread
public void storeAppsAsShown(Collection<String> s, boolean persistent) {
HashSet<String> buf = new HashSet(s);
HashSet<String> buf = new HashSet<>(s);
shownApps.addAll(buf);
if (persistent && !s.isEmpty()) {
buf.addAll(getShownScreenFilterApps());
@@ -168,7 +197,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
private Set<String> getInstalledScreenFilterApps() {
HashSet<String> screenFilterApps = new HashSet<>();
List<PackageInfo> packageInfos =
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
pm.getInstalledPackages(GET_PERMISSIONS);
for (PackageInfo packageInfo : packageInfos) {
if (isOverlayApp(packageInfo)) {
String name = pkgToString(packageInfo);
@@ -180,25 +209,22 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
return screenFilterApps;
}
// Checks if pkg uses the SYSTEM_ALERT_WINDOW permission and if so
// Checks if a package uses the SYSTEM_ALERT_WINDOW permission and if so
// returns the app name.
@Nullable
private String isOverlayApp(String pkg) {
try {
PackageInfo pkgInfo =
pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS);
PackageInfo pkgInfo = pm.getPackageInfo(pkg, GET_PERMISSIONS);
if (isOverlayApp(pkgInfo)) {
return pkgToString(pkgInfo);
}
} catch (PackageManager.NameNotFoundException ignored) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.warning("Package name not found: " + pkg);
}
} catch (NameNotFoundException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
return null;
}
// Fetch the application name for a given package.
// Fetches the application name for a given package.
@Nullable
private String pkgToString(PackageInfo pkgInfo) {
CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo);
@@ -208,25 +234,49 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
return null;
}
// Checks if an installed pkg is a user app using the permission.
// Checks if an installed package is a user app using the permission.
private boolean isOverlayApp(PackageInfo packageInfo) {
int mask = ApplicationInfo.FLAG_SYSTEM |
ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP;
// Ignore system apps
if ((packageInfo.applicationInfo.flags & mask) != 0) {
return false;
}
//Get Permissions
String[] requestedPermissions =
packageInfo.requestedPermissions;
// Ignore Play Services, it's effectively a system app
if (isPlayServices(packageInfo.packageName)) {
return false;
}
// Get permissions
String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions != null) {
for (String requestedPermission : requestedPermissions) {
if (requestedPermission
.equals(SYSTEM_ALERT_WINDOW)) {
if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) {
return true;
}
}
}
return false;
}
private boolean isPlayServices(String pkg) {
if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false;
try {
PackageInfo sigs = pm.getPackageInfo(pkg, GET_SIGNATURES);
// The genuine Play Services app should have a single signature
Signature[] signatures = sigs.signatures;
if (signatures == null || signatures.length != 1) return false;
// Extract the public key from the signature
CertificateFactory certFactory =
CertificateFactory.getInstance("X509");
byte[] signatureBytes = signatures[0].toByteArray();
InputStream in = new ByteArrayInputStream(signatureBytes);
X509Certificate cert =
(X509Certificate) certFactory.generateCertificate(in);
byte[] publicKeyBytes = cert.getPublicKey().getEncoded();
String publicKey = StringUtils.toHexString(publicKeyBytes);
return PLAY_SERVICES_PUBLIC_KEY.equals(publicKey);
} catch (NameNotFoundException | CertificateException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return false;
}
}
}

View File

@@ -43,7 +43,6 @@ import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
@@ -149,14 +148,14 @@ public class BlogFragment extends BaseFragment
return true;
case R.id.action_blog_share:
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_BLOG);
return true;
case R.id.action_blog_sharing_status:
Intent i3 = new Intent(getActivity(),
BlogSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;

View File

@@ -48,6 +48,10 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
return body;
}
public boolean isRssFeed() {
return header.isRssFeed();
}
public boolean isRead() {
return read;
}

View File

@@ -108,7 +108,8 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
author.setAuthor(a);
author.setAuthorStatus(post.getAuthorStatus());
author.setDate(post.getTimestamp());
author.setPersona(AuthorView.NORMAL);
author.setPersona(
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
// TODO make author clickable more often #624
if (item.getHeader().getType() == POST) {
author.setBlogLink(post.getGroupId());
@@ -168,7 +169,9 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
reblogger.setVisibility(VISIBLE);
reblogger.setPersona(AuthorView.REBLOGGER);
author.setPersona(AuthorView.COMMENTER);
author.setPersona(item.getHeader().getRootPost().isRssFeed() ?
AuthorView.RSS_FEED_REBLOGGED :
AuthorView.COMMENTER);
// comments
for (BlogCommentHeader c : item.getComments()) {

View File

@@ -179,7 +179,6 @@ public class FeedFragment extends BaseFragment implements
case R.id.action_rss_feeds_import:
Intent i2 =
new Intent(getActivity(), RssFeedImportActivity.class);
i2.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i2);
return true;
case R.id.action_rss_feeds_manage:

View File

@@ -6,7 +6,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ImageButton;
import android.widget.TextView;
import org.briarproject.briar.R;
@@ -39,12 +39,7 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
if (item == null) return;
// Feed Title
if (item.getTitle() != null) {
ui.title.setText(item.getTitle());
ui.title.setVisibility(VISIBLE);
} else {
ui.title.setVisibility(GONE);
}
ui.title.setText(item.getTitle());
// Delete Button
ui.delete.setOnClickListener(new OnClickListener() {
@@ -75,6 +70,14 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
} else {
ui.description.setVisibility(GONE);
}
// Open feed's blog when clicked
ui.layout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.onFeedClick(item);
}
});
}
@Override
@@ -99,8 +102,9 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
}
static class FeedViewHolder extends RecyclerView.ViewHolder {
private final View layout;
private final TextView title;
private final ImageView delete;
private final ImageButton delete;
private final TextView imported;
private final TextView updated;
private final TextView author;
@@ -110,8 +114,9 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
private FeedViewHolder(View v) {
super(v);
layout = v;
title = (TextView) v.findViewById(R.id.titleView);
delete = (ImageView) v.findViewById(R.id.deleteButton);
delete = (ImageButton) v.findViewById(R.id.deleteButton);
imported = (TextView) v.findViewById(R.id.importedView);
updated = (TextView) v.findViewById(R.id.updatedView);
author = (TextView) v.findViewById(R.id.authorView);
@@ -121,6 +126,7 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
}
interface RssFeedListener {
void onFeedClick(Feed feed);
void onDeleteClick(Feed feed);
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
@@ -15,7 +14,6 @@ import android.widget.ProgressBar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
@@ -44,9 +42,6 @@ public class RssFeedImportActivity extends BriarActivity {
@IoExecutor
Executor ioExecutor;
// Fields that are accessed from background threads must be volatile
private volatile GroupId groupId = null;
@Inject
@SuppressWarnings("WeakerAccess")
volatile FeedManager feedManager;
@@ -55,12 +50,6 @@ public class RssFeedImportActivity extends BriarActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// GroupId from Intent
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group in intent.");
groupId = new GroupId(b);
setContentView(R.layout.activity_rss_feed_import);
urlInput = (EditText) findViewById(R.id.urlInput);
@@ -128,7 +117,7 @@ public class RssFeedImportActivity extends BriarActivity {
@Override
public void run() {
try {
feedManager.addFeed(url, groupId);
feedManager.addFeed(url);
feedImported();
} catch (DbException | IOException e) {
if (LOG.isLoggable(WARNING))

View File

@@ -1,15 +1,16 @@
package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
@@ -23,6 +24,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
@@ -34,7 +36,6 @@ public class RssFeedManageActivity extends BriarActivity
private BriarRecyclerView list;
private RssFeedAdapter adapter;
private GroupId groupId;
@Inject
@SuppressWarnings("WeakerAccess")
@@ -44,12 +45,6 @@ public class RssFeedManageActivity extends BriarActivity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// GroupId from Intent
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group in intent.");
groupId = new GroupId(b);
setContentView(R.layout.activity_rss_feed_manage);
adapter = new RssFeedAdapter(this, this);
@@ -87,7 +82,6 @@ public class RssFeedManageActivity extends BriarActivity
return true;
case R.id.action_rss_feeds_import:
Intent i = new Intent(this, RssFeedImportActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
default:
@@ -100,21 +94,32 @@ public class RssFeedManageActivity extends BriarActivity
component.inject(this);
}
@Override
public void onFeedClick(Feed feed) {
Intent i = new Intent(this, BlogActivity.class);
i.putExtra(GROUP_ID, feed.getBlogId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
@Override
public void onDeleteClick(final Feed feed) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
feedManager.removeFeed(feed.getUrl());
onFeedDeleted(feed);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
onDeleteError();
}
}
});
DialogInterface.OnClickListener okListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteFeed(feed);
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this,
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_rss_remove_feed));
builder.setMessage(
getString(R.string.blogs_rss_remove_feed_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok,
okListener);
builder.show();
}
private void loadFeeds() {
@@ -149,6 +154,22 @@ public class RssFeedManageActivity extends BriarActivity
});
}
private void deleteFeed(final Feed feed) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
feedManager.removeFeed(feed);
onFeedDeleted(feed);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
onDeleteError();
}
}
});
}
private void onLoadError() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override

View File

@@ -946,6 +946,7 @@ public class ConversationActivity extends BriarActivity
@Override
public void respondToRequest(final ConversationRequestItem item,
final boolean accept) {
item.setAnswered(true);
int position = adapter.findItemPosition(item);
if (position != INVALID_POSITION) {
adapter.notifyItemChanged(position, item);

View File

@@ -21,7 +21,8 @@ class ConversationRequestItem extends ConversationNoticeInItem {
private final GroupId requestedGroupId;
private final RequestType requestType;
private final SessionId sessionId;
private final boolean answered, canBeOpened;
private final boolean canBeOpened;
private boolean answered;
ConversationRequestItem(MessageId id, GroupId groupId,
RequestType requestType, SessionId sessionId, String text,
@@ -53,6 +54,10 @@ class ConversationRequestItem extends ConversationNoticeInItem {
return answered;
}
void setAnswered(boolean answered) {
this.answered = answered;
}
public boolean canBeOpened() {
return canBeOpened;
}

View File

@@ -51,6 +51,8 @@ class ConversationRequestViewHolder extends ConversationNoticeInViewHolder {
acceptButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
acceptButton.setEnabled(false);
declineButton.setEnabled(false);
listener.respondToRequest(item, true);
}
});
@@ -58,6 +60,8 @@ class ConversationRequestViewHolder extends ConversationNoticeInViewHolder {
declineButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
acceptButton.setEnabled(false);
declineButton.setEnabled(false);
listener.respondToRequest(item, false);
}
});

View File

@@ -34,7 +34,6 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
@@ -117,18 +116,15 @@ public class ForumActivity extends
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case R.id.action_forum_compose_post:
showTextInput(null);
return true;
case R.id.action_forum_share:
Intent i2 = new Intent(this, ShareForumActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_FORUM);
return true;
case R.id.action_forum_sharing_status:
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;

View File

@@ -37,6 +37,7 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_GROUP_INVITE;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
@@ -50,8 +51,8 @@ public class GroupActivity extends
GroupController controller;
private boolean isCreator, isDissolved = false;
private MenuItem writeMenuItem, revealMenuItem, inviteMenuItem,
leaveMenuItem, dissolveMenuItem;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem;
@Override
public void injectActivity(ActivityComponent component) {
@@ -139,7 +140,6 @@ public class GroupActivity extends
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.group_actions, menu);
writeMenuItem = menu.findItem(R.id.action_group_compose_message);
revealMenuItem = menu.findItem(R.id.action_group_reveal);
inviteMenuItem = menu.findItem(R.id.action_group_invite);
leaveMenuItem = menu.findItem(R.id.action_group_leave);
@@ -152,9 +152,6 @@ public class GroupActivity extends
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_group_compose_message:
showTextInput(null);
return true;
case R.id.action_group_member_list:
Intent i1 = new Intent(this, GroupMemberListActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes());
@@ -205,7 +202,6 @@ public class GroupActivity extends
private void setGroupEnabled(boolean enabled) {
isDissolved = !enabled;
if (writeMenuItem != null) writeMenuItem.setVisible(enabled);
textInput.setSendButtonEnabled(enabled);
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
@@ -213,6 +209,8 @@ public class GroupActivity extends
textInput.setVisibility(GONE);
if (textInput.isKeyboardOpen()) textInput.hideSoftKeyboard();
if (textInput.isEmojiDrawerOpen()) textInput.hideEmojiDrawer();
} else {
textInput.setVisibility(VISIBLE);
}
}
@@ -229,7 +227,6 @@ public class GroupActivity extends
leaveMenuItem.setVisible(true);
dissolveMenuItem.setVisible(false);
}
writeMenuItem.setVisible(!isDissolved);
}
private void showLeaveGroupDialog() {

View File

@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.view.MenuItem;
import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -40,7 +41,7 @@ public class GroupMemberListActivity extends BriarActivity {
public void onCreate(@Nullable final Bundle state) {
super.onCreate(state);
setContentView(R.layout.list);
setContentView(R.layout.activity_sharing_status);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
@@ -52,6 +53,9 @@ public class GroupMemberListActivity extends BriarActivity {
list.setLayoutManager(linearLayoutManager);
adapter = new MemberListAdapter(this);
list.setAdapter(adapter);
TextView info = (TextView) findViewById(R.id.info);
info.setText(R.string.sharing_status_groups);
}
@Override

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -25,6 +26,11 @@ public class BlogSharingStatusActivity extends SharingStatusActivity {
component.inject(this);
}
@Override
int getInfoText() {
return R.string.sharing_status_blog;
}
@Override
@DatabaseExecutor
protected Collection<Contact> getSharedWith() throws DbException {

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.api.forum.ForumSharingManager;
@@ -25,6 +26,11 @@ public class ForumSharingStatusActivity extends SharingStatusActivity {
component.inject(this);
}
@Override
int getInfoText() {
return R.string.sharing_status_forum;
}
@Override
@DatabaseExecutor
protected Collection<Contact> getSharedWith() throws DbException {

View File

@@ -3,8 +3,10 @@ package org.briarproject.briar.android.sharing;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.widget.LinearLayoutManager;
import android.view.MenuItem;
import android.widget.TextView;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.db.DatabaseExecutor;
@@ -45,7 +47,7 @@ abstract class SharingStatusActivity extends BriarActivity {
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list);
setContentView(R.layout.activity_sharing_status);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
@@ -57,6 +59,9 @@ abstract class SharingStatusActivity extends BriarActivity {
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
list.setEmptyText(getString(R.string.nobody));
TextView info = (TextView) findViewById(R.id.info);
info.setText(getInfoText());
}
@Override
@@ -84,6 +89,9 @@ abstract class SharingStatusActivity extends BriarActivity {
}
}
@StringRes
abstract int getInfoText();
@DatabaseExecutor
abstract protected Collection<Contact> getSharedWith() throws DbException;

View File

@@ -43,8 +43,6 @@ import javax.inject.Inject;
import static android.support.design.widget.Snackbar.make;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.INFO;
import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCount;
@@ -55,7 +53,6 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
implements ThreadListListener<H>, TextInputListener, SharingListener,
ThreadItemListener<I> {
protected static final String KEY_INPUT_VISIBILITY = "inputVisibility";
protected static final String KEY_REPLY_ID = "replyId";
private static final Logger LOG =
@@ -89,7 +86,6 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
getController().setGroupId(groupId);
textInput = (TextInputView) findViewById(R.id.text_input_container);
textInput.setVisibility(GONE);
textInput.setListener(this);
list = (BriarRecyclerView) findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this);
@@ -181,8 +177,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
} else {
adapter.setItems(items);
list.showData();
if (replyId != null)
adapter.setHighlightedItem(replyId);
updateTextInput(replyId);
}
} else {
LOG.info("Concurrent update, reloading");
@@ -231,18 +226,9 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
list.stopPeriodicUpdate();
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
boolean visible = savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY);
textInput.setVisibility(visible ? VISIBLE : GONE);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
boolean visible = textInput.getVisibility() == VISIBLE;
outState.putBoolean(KEY_INPUT_VISIBILITY, visible);
ThreadItem replyItem = adapter.getHighlightedItem();
if (replyItem != null) {
outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes());
@@ -262,9 +248,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onBackPressed() {
if (textInput.getVisibility() == VISIBLE) {
textInput.setVisibility(GONE);
adapter.setHighlightedItem(null);
if (adapter.getHighlightedItem() != null) {
updateTextInput(null);
} else {
super.onBackPressed();
}
@@ -280,7 +265,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onReplyClick(final I item) {
showTextInput(item);
updateTextInput(item.getId());
if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item);
} else {
@@ -330,20 +315,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
snackbar.show();
}
protected void showTextInput(@Nullable I replyItem) {
// An animation here would be an overkill because of the keyboard
// popping up.
// only clear the text when the input container was not visible
if (textInput.getVisibility() != VISIBLE) {
textInput.setVisibility(VISIBLE);
textInput.setText("");
private void updateTextInput(@Nullable MessageId replyItemId) {
if (replyItemId != null) {
textInput.setHint(R.string.forum_message_reply_hint);
textInput.requestFocus();
textInput.showSoftKeyboard();
} else {
textInput.setHint(R.string.forum_new_message_hint);
}
textInput.requestFocus();
textInput.showSoftKeyboard();
textInput.setHint(replyItem == null ? R.string.forum_new_message_hint :
R.string.forum_message_reply_hint);
adapter.setHighlightedItem(
replyItem == null ? null : replyItem.getId());
adapter.setHighlightedItem(replyItemId);
}
@Override
@@ -369,9 +349,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
};
getController().createAndStoreMessage(text, replyItem, handler);
textInput.hideSoftKeyboard();
textInput.setVisibility(GONE);
textInput.setText("");
adapter.setHighlightedItem(null);
updateTextInput(null);
}
protected abstract int getMaxBodyLength();

View File

@@ -30,6 +30,7 @@ import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.graphics.Typeface.BOLD;
import static android.util.TypedValue.COMPLEX_UNIT_PX;
import static org.briarproject.bramble.api.identity.Author.Status.NONE;
import static org.briarproject.bramble.api.identity.Author.Status.OURSELVES;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@@ -40,6 +41,8 @@ public class AuthorView extends RelativeLayout {
public static final int REBLOGGER = 1;
public static final int COMMENTER = 2;
public static final int LIST = 3;
public static final int RSS_FEED = 4;
public static final int RSS_FEED_REBLOGGED = 5;
private final CircleImageView avatar;
private final ImageView avatarIcon;
@@ -83,7 +86,13 @@ public class AuthorView extends RelativeLayout {
}
public void setAuthorStatus(Status status) {
trustIndicator.setTrustLevel(status);
if (status != NONE) {
trustIndicator.setTrustLevel(status);
trustIndicator.setVisibility(VISIBLE);
} else {
trustIndicator.setVisibility(GONE);
}
if (status == OURSELVES) {
authorName.setTypeface(authorNameTypeface, BOLD);
} else {
@@ -124,10 +133,17 @@ public class AuthorView extends RelativeLayout {
setOnClickListener(null);
}
/**
* Styles this view for a different persona.
*
* Attention: RSS_FEED and RSS_FEED_REBLOGGED change the avatar
* and override the one set by
* {@link AuthorView#setAuthor(Author)}.
*/
public void setPersona(int persona) {
switch (persona) {
case NORMAL:
avatarIcon.setVisibility(VISIBLE);
avatarIcon.setVisibility(INVISIBLE);
date.setVisibility(VISIBLE);
setAvatarSize(R.dimen.blogs_avatar_normal_size);
setTextSize(authorName, R.dimen.text_size_small);
@@ -158,6 +174,24 @@ public class AuthorView extends RelativeLayout {
setCenterVertical(authorName, true);
setCenterVertical(trustIndicator, true);
break;
case RSS_FEED:
avatarIcon.setVisibility(INVISIBLE);
date.setVisibility(VISIBLE);
avatar.setImageResource(R.drawable.ic_rss_feed);
setAvatarSize(R.dimen.blogs_avatar_normal_size);
setTextSize(authorName, R.dimen.text_size_small);
setCenterVertical(authorName, false);
setCenterVertical(trustIndicator, false);
break;
case RSS_FEED_REBLOGGED:
avatarIcon.setVisibility(INVISIBLE);
date.setVisibility(VISIBLE);
avatar.setImageResource(R.drawable.ic_rss_feed);
setAvatarSize(R.dimen.blogs_avatar_comment_size);
setTextSize(authorName, R.dimen.text_size_tiny);
setCenterVertical(authorName, false);
setCenterVertical(trustIndicator, false);
break;
}
}

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="48.0"
android:viewportWidth="48.0">
<path
android:fillColor="#06b9ff"
android:pathData="M9.1,19.3l14.9,11.8l14.9,-11.8l-1.9,-2.4l-13,10.4l-13,-10.4z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="48.0"
android:viewportWidth="48.0">
<path
android:fillColor="#06b9ff"
android:pathData="M38.9,28.7l-14.9,-11.8l-14.9,11.8l1.9,2.4l13,-10.4l13,10.4z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.56"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#000000"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportHeight="30"
android:viewportWidth="30">
<path
android:fillColor="#ffa500"
android:pathData="M0,8.88178e-16 L30,8.88178e-16 L30,30 L0,30 L0,8.88178e-16 Z"/>
<path
android:fillColor="#ffffff"
android:pathData="M8.9322,18.0339 C10.6078,18.0339,11.9661,19.3922,11.9661,21.0678
C11.9661,22.7434,10.6078,24.1017,8.9322,24.1017
C7.25663,24.1017,5.8983,22.7434,5.8983,21.0678
C5.8983,19.3922,7.25663,18.0339,8.9322,18.0339 Z"/>
<path
android:fillColor="#ffffff"
android:pathData="M5.8983,15 A9.1016949,9.1016949,0,0,1,15,24.1017 L18.0339,24.1017
A12.135593,12.135593,0,0,0,5.8983,11.9661 Z"/>
<path
android:fillColor="#ffffff"
android:pathData="M5.8983,8.9322 A15.169492,15.169492,0,0,1,21.0678,24.1017 L24.1017,24.1017
A18.20339,18.20339,0,0,0,5.8983,5.8983 Z"/>
</vector>

View File

@@ -5,5 +5,5 @@
<stroke
android:width="2dp"
android:color="@color/forum_discussion_nested_line"/>
android:color="@color/thread_indicator"/>
</shape>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/chevron48dp_down" android:state_selected="true"/>
<item android:drawable="@drawable/chevron48dp_up"/>
</selector>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableLeft="@drawable/ic_info"
android:drawablePadding="@dimen/margin_medium"
android:padding="@dimen/margin_medium"
android:textColor="?android:textColorSecondary"
android:textSize="@dimen/text_size_tiny"
tools:text="@string/sharing_status_forum"/>
<View style="@style/Divider.Horizontal"/>
<org.briarproject.briar.android.view.BriarRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:scrollToEnd="false"
tools:listitem="@layout/list_item_contact"/>
</LinearLayout>

View File

@@ -19,7 +19,7 @@
android:textSize="@dimen/text_size_medium"
tools:text="This is a name of a RSS Feed"/>
<ImageView
<ImageButton
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -3,12 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_forum_compose_post"
android:icon="@drawable/forum_item_create_white"
android:title="@string/forum_compose_post"
app:showAsAction="always"/>
<item
android:id="@+id/action_forum_share"
android:icon="@drawable/social_share_white"

View File

@@ -3,12 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_group_compose_message"
android:icon="@drawable/forum_item_create_white"
android:title="@string/groups_compose_message"
app:showAsAction="always"/>
<item
android:id="@+id/action_group_member_list"
android:icon="@drawable/ic_group_white"

View File

@@ -12,6 +12,8 @@
<enum name="reblogger" value="1"/>
<enum name="commenter" value="2"/>
<enum name="list" value="3"/>
<enum name="rss_feed" value="4"/>
<enum name="rss_feed_reblogged" value="5"/>
</attr>
</declare-styleable>

View File

@@ -33,11 +33,11 @@
<!-- this is needed as preference_category_material layout uses this color as the text color -->
<color name="preference_fallback_accent_color">@color/briar_accent</color>
<color name="thread_indicator">#9e9e9e</color>
<color name="divider">#c1c1c1</color>
<color name="default_separator_inverted">#ffffff</color>
<color name="menu_background">#FFFFFF</color>
<color name="spinner_border">#61000000</color> <!-- 38% Black -->
<color name="forum_discussion_nested_line">#cfd2d4</color>
<color name="forum_cell_highlight">#ffffff</color>
</resources>

View File

@@ -162,7 +162,6 @@
<string name="groups_create_group_invitation_button">Send Invitation</string>
<string name="groups_create_group_hint">Add a name for your private group</string>
<string name="groups_invitation_sent">Group invitation has been sent</string>
<string name="groups_compose_message">Compose Message</string>
<string name="groups_message_sent">Message sent</string>
<string name="groups_member_list">Member List</string>
<string name="groups_invite_members">Invite Members</string>
@@ -194,6 +193,7 @@
<string name="groups_invitations_response_declined_sent">You declined the group invitation from %s.</string>
<string name="groups_invitations_response_accepted_received">%s accepted the group invitation.</string>
<string name="groups_invitations_response_declined_received">%s declined the group invitation.</string>
<string name="sharing_status_groups">Only the creator can invite new members to the group. Below are all current members of the group.</string>
<!-- Private Groups Revealing Contacts -->
<string name="groups_reveal_contacts">Reveal Contacts</string>
@@ -215,15 +215,10 @@
<item quantity="one">%d post</item>
<item quantity="other">%d posts</item>
</plurals>
<string name="forum_compose_post">New Forum Post</string>
<string name="forum_new_entry_posted">Forum entry posted</string>
<string name="forum_new_message_hint">New Entry</string>
<string name="forum_message_reply_hint">New Reply</string>
<string name="btn_reply">Reply</string>
<plurals name="message_replies">
<item quantity="one">%1$d reply</item>
<item quantity="other">%1$d replies</item>
</plurals>
<string name="forum_leave">Leave Forum</string>
<string name="dialog_title_leave_forum">Confirm Leaving Forum</string>
<string name="dialog_message_leave_forum">Are you sure that you want to leave this forum? Contacts you have shared this forum with might get cut off from receiving updates for this forum.</string>
@@ -252,6 +247,7 @@
<string name="forum_invitation_response_declined_received">%s declined the forum invitation.</string>
<string name="sharing_status">Sharing Status</string>
<string name="sharing_status_forum">Any member of a forum can share it with their contacts. You are sharing this forum with the following contacts. There may also be other members who you can\'t see.</string>
<string name="shared_with">Shared with %1$d (%2$d online)</string>
<plurals name="forums_shared">
<item quantity="one">%d forum shared by contacts</item>
@@ -291,6 +287,7 @@
<string name="blogs_sharing_invitations_title">Blog Invitations</string>
<string name="blogs_sharing_joined_toast">Subscribed to Blog</string>
<string name="blogs_sharing_declined_toast">Blog Invitation Declined</string>
<string name="sharing_status_blog">Anyone who subscribes to a blog can share it with their contacts. You are sharing this blog with the following contacts. There may also be other subscribers who you can\'t see.</string>
<!-- RSS Feeds -->
<string name="blogs_rss_feeds_import">Import RSS Feed</string>
@@ -301,6 +298,9 @@
<string name="blogs_rss_feeds_manage_imported">Imported:</string>
<string name="blogs_rss_feeds_manage_author">Author:</string>
<string name="blogs_rss_feeds_manage_updated">Last Updated:</string>
<string name="blogs_rss_remove_feed">Remove Feed</string>
<string name="blogs_rss_remove_feed_dialog_message">Are you sure you want to remove this feed and all its posts?\nAny posts you have shared will not be removed from other people\'s devices.</string>
<string name="blogs_rss_remove_feed_ok">Remove Feed</string>
<string name="blogs_rss_feeds_manage_delete_error">The feed could not be deleted!</string>
<string name="blogs_rss_feeds_manage_empty_state">You haven\'t imported any RSS feeds.\n\nWhy don\'t you click the plus in the top right screen corner to add your first?</string>
<string name="blogs_rss_feeds_manage_error">There was a problem loading your feeds. Please try again later.</string>

View File

@@ -102,7 +102,7 @@
<style name="DiscussionLevelIndicator">
<item name="android:layout_marginLeft">4dp</item>
<item name="android:background">@color/divider</item>
<item name="android:background">@color/thread_indicator</item>
</style>
<style name="BriarTabLayout" parent="Widget.Design.TabLayout">

View File

@@ -13,16 +13,22 @@ import javax.annotation.concurrent.Immutable;
public class Blog extends BaseGroup implements Shareable {
private final Author author;
private final boolean rssFeed;
public Blog(Group group, Author author) {
public Blog(Group group, Author author, boolean rssFeed) {
super(group);
this.author = author;
this.rssFeed = rssFeed;
}
public Author getAuthor() {
return author;
}
public boolean isRssFeed() {
return rssFeed;
}
@Override
public boolean equals(Object o) {
return o instanceof Blog && super.equals(o);

View File

@@ -26,7 +26,7 @@ public class BlogCommentHeader extends BlogPostHeader {
Status authorStatus, boolean read) {
super(type, groupId, id, parent.getId(), timestamp,
timeReceived, author, authorStatus, read);
timeReceived, author, authorStatus, false, read);
if (type != COMMENT && type != WRAPPED_COMMENT)
throw new IllegalArgumentException("Incompatible Message Type");
@@ -43,4 +43,11 @@ public class BlogCommentHeader extends BlogPostHeader {
public BlogPostHeader getParent() {
return parent;
}
public BlogPostHeader getRootPost() {
if (parent instanceof BlogCommentHeader)
return ((BlogCommentHeader) parent).getRootPost();
return parent;
}
}

View File

@@ -28,6 +28,7 @@ public interface BlogConstants {
String KEY_AUTHOR_NAME = "name";
String KEY_PUBLIC_KEY = "publicKey";
String KEY_AUTHOR = "author";
String KEY_RSS_FEED = "rssFeed";
String KEY_READ = "read";
String KEY_COMMENT = "comment";
String KEY_ORIGINAL_MSG_ID = "originalMessageId";

View File

@@ -13,6 +13,11 @@ public interface BlogFactory {
*/
Blog createBlog(Author author);
/**
* Creates a RSS feed blog for a given author.
*/
Blog createFeedBlog(Author author);
/**
* Parses a blog with the given Group
*/

View File

@@ -41,6 +41,11 @@ public interface BlogManager {
*/
void removeBlog(Blog b) throws DbException;
/**
* Removes and deletes a blog with the given {@link Transaction}.
*/
void removeBlog(Transaction txn, Blog b) throws DbException;
/**
* Stores a local blog post.
*/

View File

@@ -17,21 +17,23 @@ public class BlogPostHeader extends PostHeader {
private final MessageType type;
private final GroupId groupId;
private final long timeReceived;
private final boolean rssFeed;
public BlogPostHeader(MessageType type, GroupId groupId, MessageId id,
@Nullable MessageId parentId, long timestamp, long timeReceived,
Author author, Status authorStatus, boolean read) {
Author author, Status authorStatus, boolean rssFeed, boolean read) {
super(id, parentId, timestamp, author, authorStatus, read);
this.type = type;
this.groupId = groupId;
this.timeReceived = timeReceived;
this.rssFeed = rssFeed;
}
public BlogPostHeader(MessageType type, GroupId groupId, MessageId id,
long timestamp, long timeReceived, Author author,
Status authorStatus, boolean read) {
Status authorStatus, boolean rssFeed, boolean read) {
this(type, groupId, id, null, timestamp, timeReceived, author,
authorStatus, read);
authorStatus, rssFeed, read);
}
public MessageType getType() {
@@ -45,4 +47,9 @@ public class BlogPostHeader extends PostHeader {
public long getTimeReceived() {
return timeReceived;
}
public boolean isRssFeed() {
return rssFeed;
}
}

View File

@@ -13,18 +13,18 @@ public abstract class BaseMessageHeader {
private final MessageId id;
private final GroupId groupId;
private final long timestamp;
private final boolean local, read, sent, seen;
private final boolean local, sent, seen, read;
public BaseMessageHeader(MessageId id, GroupId groupId, long timestamp,
boolean local, boolean read, boolean sent, boolean seen) {
boolean local, boolean sent, boolean seen, boolean read) {
this.id = id;
this.groupId = groupId;
this.timestamp = timestamp;
this.local = local;
this.read = read;
this.sent = sent;
this.seen = seen;
this.read = read;
}
public MessageId getId() {
@@ -43,10 +43,6 @@ public abstract class BaseMessageHeader {
return local;
}
public boolean isRead() {
return read;
}
public boolean isSent() {
return sent;
}
@@ -55,4 +51,8 @@ public abstract class BaseMessageHeader {
return seen;
}
public boolean isRead() {
return read;
}
}

View File

@@ -1,40 +1,31 @@
package org.briarproject.briar.api.feed;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.blog.Blog;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_BLOG_GROUP_ID;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_ADDED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_DESC;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_LAST_ENTRY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_TITLE;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_UPDATED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_URL;
@Immutable
@NotNullByDefault
public class Feed {
private final String url;
private final GroupId blogId;
private final Blog blog;
private final LocalAuthor localAuthor;
@Nullable
private final String title, description, author;
private final String description, author;
private final long added, updated, lastEntryTime;
public Feed(String url, GroupId blogId, @Nullable String title,
@Nullable String description, @Nullable String author,
long added, long updated, long lastEntryTime) {
public Feed(String url, Blog blog, LocalAuthor localAuthor,
@Nullable String description, @Nullable String author, long added,
long updated, long lastEntryTime) {
this.url = url;
this.blogId = blogId;
this.title = title;
this.blog = blog;
this.localAuthor = localAuthor;
this.description = description;
this.author = author;
this.added = added;
@@ -42,13 +33,13 @@ public class Feed {
this.lastEntryTime = lastEntryTime;
}
public Feed(String url, GroupId blogId, @Nullable String title,
public Feed(String url, Blog blog, LocalAuthor localAuthor,
@Nullable String description, @Nullable String author, long added) {
this(url, blogId, title, description, author, added, 0L, 0L);
this(url, blog, localAuthor, description, author, added, 0L, 0L);
}
public Feed(String url, GroupId blogId, long added) {
this(url, blogId, null, null, null, added, 0L, 0L);
public Feed(String url, Blog blog, LocalAuthor localAuthor, long added) {
this(url, blog, localAuthor, null, null, added, 0L, 0L);
}
public String getUrl() {
@@ -56,39 +47,19 @@ public class Feed {
}
public GroupId getBlogId() {
return blogId;
return blog.getId();
}
public BdfDictionary toBdfDictionary() {
BdfDictionary d = BdfDictionary.of(
new BdfEntry(KEY_FEED_URL, url),
new BdfEntry(KEY_BLOG_GROUP_ID, blogId.getBytes()),
new BdfEntry(KEY_FEED_ADDED, added),
new BdfEntry(KEY_FEED_UPDATED, updated),
new BdfEntry(KEY_FEED_LAST_ENTRY, lastEntryTime)
);
if (title != null) d.put(KEY_FEED_TITLE, title);
if (description != null) d.put(KEY_FEED_DESC, description);
if (author != null) d.put(KEY_FEED_AUTHOR, author);
return d;
public Blog getBlog() {
return blog;
}
public static Feed from(BdfDictionary d) throws FormatException {
String url = d.getString(KEY_FEED_URL);
GroupId blogId = new GroupId(d.getRaw(KEY_BLOG_GROUP_ID));
String title = d.getOptionalString(KEY_FEED_TITLE);
String desc = d.getOptionalString(KEY_FEED_DESC);
String author = d.getOptionalString(KEY_FEED_AUTHOR);
long added = d.getLong(KEY_FEED_ADDED, 0L);
long updated = d.getLong(KEY_FEED_UPDATED, 0L);
long lastEntryTime = d.getLong(KEY_FEED_LAST_ENTRY, 0L);
return new Feed(url, blogId, title, desc, author, added, updated,
lastEntryTime);
public LocalAuthor getLocalAuthor() {
return localAuthor;
}
@Nullable
public String getTitle() {
return title;
return blog.getName();
}
@Nullable
@@ -118,20 +89,9 @@ public class Feed {
if (this == o) return true;
if (o instanceof Feed) {
Feed f = (Feed) o;
return url.equals(f.url) && blogId.equals(f.getBlogId()) &&
equalsWithNull(title, f.getTitle()) &&
equalsWithNull(description, f.getDescription()) &&
equalsWithNull(author, f.getAuthor()) &&
added == f.getAdded() &&
updated == f.getUpdated() &&
lastEntryTime == f.getLastEntryTime();
return blog.equals(f.blog);
}
return false;
}
private boolean equalsWithNull(@Nullable Object a, @Nullable Object b) {
if (a == b) return true;
if (a == null || b == null) return false;
return a.equals(b);
}
}

View File

@@ -18,8 +18,9 @@ public interface FeedConstants {
// group metadata keys
String KEY_FEEDS = "feeds";
String KEY_FEED_URL = "feedURL";
String KEY_BLOG_GROUP_ID = "blogGroupId";
String KEY_FEED_TITLE = "feedTitle";
String KEY_BLOG_TITLE = "blogTitle";
String KEY_PUBLIC_KEY = "publicKey";
String KEY_PRIVATE_KEY = "privateKey";
String KEY_FEED_DESC = "feedDesc";
String KEY_FEED_AUTHOR = "feedAuthor";
String KEY_FEED_ADDED = "feedAdded";

View File

@@ -3,7 +3,6 @@ package org.briarproject.briar.api.feed;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.GroupId;
import java.io.IOException;
import java.util.List;
@@ -17,14 +16,14 @@ public interface FeedManager {
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.feed");
/**
* Adds an RSS feed.
* Adds an RSS feed as a new dedicated blog.
*/
void addFeed(String url, GroupId g) throws DbException, IOException;
void addFeed(String url) throws DbException, IOException;
/**
* Removes an RSS feed.
*/
void removeFeed(String url) throws DbException;
void removeFeed(Feed feed) throws DbException;
/**
* Returns a list of all added RSS feeds

View File

@@ -22,7 +22,7 @@ public class IntroductionMessage extends BaseMessageHeader {
GroupId groupId, int role, long time, boolean local, boolean sent,
boolean seen, boolean read) {
super(messageId, groupId, time, local, read, sent, seen);
super(messageId, groupId, time, local, sent, seen, read);
this.sessionId = sessionId;
this.messageId = messageId;
this.role = role;

View File

@@ -14,7 +14,7 @@ public class PrivateMessageHeader extends BaseMessageHeader {
public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp,
boolean local, boolean read, boolean sent, boolean seen) {
super(id, groupId, timestamp, local, read, sent, seen);
super(id, groupId, timestamp, local, sent, seen, read);
}
}

View File

@@ -20,7 +20,7 @@ public class InvitationMessage extends BaseMessageHeader {
boolean local, boolean sent, boolean seen, boolean read,
SessionId sessionId, ContactId contactId) {
super(id, groupId, time, local, read, sent, seen);
super(id, groupId, time, local, sent, seen, read);
this.sessionId = sessionId;
this.contactId = contactId;
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar;
import org.briarproject.briar.blog.BlogModule;
import org.briarproject.briar.client.BriarClientModule;
import org.briarproject.briar.feed.DnsModule;
import org.briarproject.briar.feed.FeedModule;
import org.briarproject.briar.forum.ForumModule;
import org.briarproject.briar.introduction.IntroductionModule;
@@ -16,6 +17,7 @@ import dagger.Module;
BlogModule.class,
BriarClientModule.class,
FeedModule.class,
DnsModule.class,
ForumModule.class,
GroupInvitationModule.class,
IntroductionModule.class,

View File

@@ -14,6 +14,9 @@ import org.briarproject.briar.api.blog.BlogFactory;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
@Immutable
@NotNullByDefault
class BlogFactoryImpl implements BlogFactory {
@@ -33,28 +36,45 @@ class BlogFactoryImpl implements BlogFactory {
@Override
public Blog createBlog(Author a) {
return createBlog(a, false);
}
@Override
public Blog createFeedBlog(Author a) {
return createBlog(a, true);
}
private Blog createBlog(Author a, boolean rssFeed) {
try {
BdfList blog = BdfList.of(
a.getName(),
a.getPublicKey()
a.getPublicKey(),
rssFeed
);
byte[] descriptor = clientHelper.toByteArray(blog);
Group g = groupFactory
.createGroup(BlogManagerImpl.CLIENT_ID, descriptor);
return new Blog(g, a);
return new Blog(g, a, rssFeed);
} catch (FormatException e) {
throw new RuntimeException(e);
}
}
@Override
public Blog parseBlog(Group g) throws FormatException {
byte[] descriptor = g.getDescriptor();
// Author Name, Public Key
public Blog parseBlog(Group group) throws FormatException {
byte[] descriptor = group.getDescriptor();
// Author name, public key, RSS feed
BdfList blog = clientHelper.toList(descriptor);
Author a =
authorFactory.createAuthor(blog.getString(0), blog.getRaw(1));
return new Blog(g, a);
String name = blog.getString(0);
if (name.length() > MAX_AUTHOR_NAME_LENGTH)
throw new IllegalArgumentException();
byte[] publicKey = blog.getRaw(1);
if (publicKey.length > MAX_PUBLIC_KEY_LENGTH)
throw new IllegalArgumentException();
Author author = authorFactory.createAuthor(name, publicKey);
boolean rssFeed = blog.getBoolean(2);
return new Blog(group, author, rssFeed);
}
}

View File

@@ -61,6 +61,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_ORIGINAL_PARENT_
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PARENT_MSG_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_READ;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_RSS_FEED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIMESTAMP;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE;
@@ -224,6 +225,11 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
}
}
@Override
public void removeBlog(Transaction txn, Blog b) throws DbException {
removeBlog(txn, b, false);
}
private void removeBlog(Transaction txn, Blog b, boolean forced)
throws DbException {
if (!forced && !canBeRemoved(txn, b.getId()))
@@ -248,15 +254,18 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
@Override
public void addLocalPost(Transaction txn, BlogPost p) throws DbException {
try {
GroupId groupId = p.getMessage().getGroupId();
Blog b = getBlog(txn, groupId);
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_TYPE, POST.getInt());
meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp());
meta.put(KEY_AUTHOR, authorToBdfDictionary(p.getAuthor()));
meta.put(KEY_READ, true);
meta.put(KEY_RSS_FEED, b.isRssFeed());
clientHelper.addLocalMessage(txn, p.getMessage(), meta, true);
// broadcast event about new post
GroupId groupId = p.getMessage().getGroupId();
MessageId postId = p.getMessage().getId();
BlogPostHeader h =
getPostHeaderFromMetadata(txn, groupId, postId, meta);
@@ -345,6 +354,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
wMessage = blogPostFactory
.wrapPost(groupId, wDescriptor, wTimestamp, body);
meta.put(KEY_TYPE, WRAPPED_POST.getInt());
meta.put(KEY_RSS_FEED, pOriginalHeader.isRssFeed());
} else if (type == COMMENT) {
Group wGroup = db.getGroup(txn, pOriginalHeader.getGroupId());
byte[] wDescriptor = wGroup.getDescriptor();
@@ -363,6 +373,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
// Re-wrap wrapped post without adding another wrapping layer
wMessage = blogPostFactory.rewrapWrappedPost(groupId, body);
meta.put(KEY_TYPE, WRAPPED_POST.getInt());
meta.put(KEY_RSS_FEED, pOriginalHeader.isRssFeed());
} else if (type == WRAPPED_COMMENT) {
BlogCommentHeader wComment = (BlogCommentHeader) pOriginalHeader;
MessageId wrappedId =
@@ -593,8 +604,11 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
String name = d.getString(KEY_AUTHOR_NAME);
byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY);
Author author = new Author(authorId, name, publicKey);
boolean isFeedPost = meta.getBoolean(KEY_RSS_FEED, false);
Status authorStatus;
if (authorStatuses.containsKey(authorId)) {
if (isFeedPost) {
authorStatus = Status.NONE;
} else if (authorStatuses.containsKey(authorId)) {
authorStatus = authorStatuses.get(authorId);
} else {
authorStatus = identityManager.getAuthorStatus(txn, authorId);
@@ -611,7 +625,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
timestamp, timeReceived, author, authorStatus, read);
} else {
return new BlogPostHeader(type, groupId, id, timestamp,
timeReceived, author, authorStatus, read);
timeReceived, author, authorStatus, isFeedPost, read);
}
}

View File

@@ -39,6 +39,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_ORIGINAL_PARENT_
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PARENT_MSG_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_READ;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_RSS_FEED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIMESTAMP;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE;
@@ -123,6 +124,7 @@ class BlogPostValidator extends BdfMessageValidator {
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_ORIGINAL_MSG_ID, m.getId());
meta.put(KEY_AUTHOR, authorToBdfDictionary(a));
meta.put(KEY_RSS_FEED, b.isRssFeed());
return new BdfMessageContext(meta);
}
@@ -199,6 +201,7 @@ class BlogPostValidator extends BdfMessageValidator {
// Get and Validate the Wrapped Message
Group wGroup = groupFactory
.createGroup(BlogManagerImpl.CLIENT_ID, descriptor);
Blog wBlog = blogFactory.parseBlog(wGroup);
BdfList wBodyList = BdfList.of(POST.getInt(), content, signature);
byte[] wBody = clientHelper.toByteArray(wBodyList);
Message wMessage =
@@ -211,6 +214,7 @@ class BlogPostValidator extends BdfMessageValidator {
meta.put(KEY_ORIGINAL_MSG_ID, wMessage.getId());
meta.put(KEY_TIMESTAMP, wTimestamp);
meta.put(KEY_AUTHOR, c.getDictionary().getDictionary(KEY_AUTHOR));
meta.put(KEY_RSS_FEED, wBlog.isRssFeed());
return new BdfMessageContext(meta);
}

View File

@@ -14,6 +14,7 @@ import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.util.ByteUtils.INT_64_BYTES;
import static org.briarproject.briar.api.client.QueueMessage.MAX_QUEUE_MESSAGE_BODY_LENGTH;
import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH;
@@ -39,11 +40,14 @@ class QueueMessageFactoryImpl implements QueueMessageFactory {
ByteUtils.writeUint64(queuePosition, raw, MESSAGE_HEADER_LENGTH);
System.arraycopy(body, 0, raw, QUEUE_MESSAGE_HEADER_LENGTH,
body.length);
byte[] timeBytes = new byte[ByteUtils.INT_64_BYTES];
byte[] timeBytes = new byte[INT_64_BYTES];
ByteUtils.writeUint64(timestamp, timeBytes, 0);
byte[] bodyBytes = new byte[body.length + INT_64_BYTES];
System.arraycopy(raw, MESSAGE_HEADER_LENGTH, bodyBytes, 0,
body.length + INT_64_BYTES);
MessageId id = new MessageId(
crypto.hash(MessageId.LABEL, groupId.getBytes(), timeBytes,
body));
bodyBytes));
return new QueueMessage(id, groupId, timestamp, queuePosition, raw);
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.briar.feed;
import dagger.Module;
import dagger.Provides;
import okhttp3.Dns;
/**
* This is a dedicated module, so it can be replaced for testing.
*/
@Module
public class DnsModule {
@Provides
Dns provideDns(NoDns noDns) {
return noDns;
}
}

View File

@@ -0,0 +1,34 @@
package org.briarproject.briar.feed;
import com.rometools.rome.feed.synd.SyndFeed;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.briar.api.feed.Feed;
interface FeedFactory {
/**
* Create a new feed based on the feed url
* and the metadata of an existing {@link SyndFeed}.
*/
Feed createFeed(String url, SyndFeed feed);
/**
* Creates a new updated feed, based on the given existing feed,
* new metadata from the given {@link SyndFeed}
* and the time of the last feed entry.
*/
Feed createFeed(Feed feed, SyndFeed f, long lastEntryTime);
/**
* De-serializes a {@link BdfDictionary} into a {@link Feed}.
*/
Feed createFeed(BdfDictionary d) throws FormatException;
/**
* Serializes a {@link Feed} into a {@link BdfDictionary}.
*/
BdfDictionary feedToBdfDictionary(Feed feed);
}

View File

@@ -0,0 +1,112 @@
package org.briarproject.briar.feed;
import com.rometools.rome.feed.synd.SyndFeed;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogFactory;
import org.briarproject.briar.api.feed.Feed;
import javax.inject.Inject;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_BLOG_TITLE;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_ADDED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_DESC;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_LAST_ENTRY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_UPDATED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_URL;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_PRIVATE_KEY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_PUBLIC_KEY;
class FeedFactoryImpl implements FeedFactory {
private final CryptoComponent cryptoComponent;
private final AuthorFactory authorFactory;
private final BlogFactory blogFactory;
private final Clock clock;
@Inject
FeedFactoryImpl(CryptoComponent cryptoComponent,
AuthorFactory authorFactory, BlogFactory blogFactory, Clock clock) {
this.cryptoComponent = cryptoComponent;
this.authorFactory = authorFactory;
this.blogFactory = blogFactory;
this.clock = clock;
}
@Override
public Feed createFeed(String url, SyndFeed syndFeed) {
String title = syndFeed.getTitle();
if (title == null) title = "RSS";
title = StringUtils.truncateUtf8(title, MAX_AUTHOR_NAME_LENGTH);
KeyPair keyPair = cryptoComponent.generateSignatureKeyPair();
LocalAuthor localAuthor = authorFactory
.createLocalAuthor(title,
keyPair.getPublic().getEncoded(),
keyPair.getPrivate().getEncoded());
Blog blog = blogFactory.createFeedBlog(localAuthor);
long added = clock.currentTimeMillis();
return new Feed(url, blog, localAuthor, added);
}
@Override
public Feed createFeed(Feed feed, SyndFeed f, long lastEntryTime) {
long updated = clock.currentTimeMillis();
return new Feed(feed.getUrl(), feed.getBlog(), feed.getLocalAuthor(),
f.getDescription(), f.getAuthor(), feed.getAdded(), updated,
lastEntryTime);
}
@Override
public Feed createFeed(BdfDictionary d) throws FormatException {
String url = d.getString(KEY_FEED_URL);
String blogTitle = d.getString(KEY_BLOG_TITLE);
byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY);
byte[] privateKey = d.getRaw(KEY_PRIVATE_KEY);
LocalAuthor localAuthor = authorFactory
.createLocalAuthor(blogTitle, publicKey, privateKey);
Blog blog = blogFactory.createFeedBlog(localAuthor);
String desc = d.getOptionalString(KEY_FEED_DESC);
String author = d.getOptionalString(KEY_FEED_AUTHOR);
long added = d.getLong(KEY_FEED_ADDED, 0L);
long updated = d.getLong(KEY_FEED_UPDATED, 0L);
long lastEntryTime = d.getLong(KEY_FEED_LAST_ENTRY, 0L);
return new Feed(url, blog, localAuthor, desc, author, added,
updated, lastEntryTime);
}
@Override
public BdfDictionary feedToBdfDictionary(Feed feed) {
BdfDictionary d = BdfDictionary.of(
new BdfEntry(KEY_FEED_URL, feed.getUrl()),
new BdfEntry(KEY_BLOG_TITLE, feed.getLocalAuthor().getName()),
new BdfEntry(KEY_PUBLIC_KEY,
feed.getLocalAuthor().getPublicKey()),
new BdfEntry(KEY_PRIVATE_KEY,
feed.getLocalAuthor().getPrivateKey()),
new BdfEntry(KEY_FEED_ADDED, feed.getAdded()),
new BdfEntry(KEY_FEED_UPDATED, feed.getUpdated()),
new BdfEntry(KEY_FEED_LAST_ENTRY, feed.getLastEntryTime())
);
if (feed.getDescription() != null)
d.put(KEY_FEED_DESC, feed.getDescription());
if (feed.getAuthor() != null) d.put(KEY_FEED_AUTHOR, feed.getAuthor());
return d;
}
}

View File

@@ -18,7 +18,6 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -31,6 +30,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
@@ -39,8 +39,6 @@ import org.briarproject.briar.api.feed.FeedManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
@@ -75,12 +73,12 @@ import static org.briarproject.briar.util.HtmlUtils.clean;
@ThreadSafe
@NotNullByDefault
class FeedManagerImpl implements FeedManager, Client, EventListener {
class FeedManagerImpl implements FeedManager, Client, EventListener,
BlogManager.RemoveBlogHook {
private static final Logger LOG =
Logger.getLogger(FeedManagerImpl.class.getName());
private static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
private static final int CONNECT_TIMEOUT = 60 * 1000; // Milliseconds
private final ScheduledExecutorService scheduler;
@@ -88,31 +86,33 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
private final DatabaseComponent db;
private final ContactGroupFactory contactGroupFactory;
private final ClientHelper clientHelper;
private final IdentityManager identityManager;
private final BlogManager blogManager;
private final BlogPostFactory blogPostFactory;
private final FeedFactory feedFactory;
private final SocketFactory torSocketFactory;
private final Clock clock;
private final Dns noDnsLookups;
private final AtomicBoolean fetcherStarted = new AtomicBoolean(false);
@Inject
FeedManagerImpl(@Scheduler ScheduledExecutorService scheduler,
@IoExecutor Executor ioExecutor, DatabaseComponent db,
ContactGroupFactory contactGroupFactory, ClientHelper clientHelper,
IdentityManager identityManager, BlogManager blogManager,
BlogPostFactory blogPostFactory, SocketFactory torSocketFactory,
Clock clock) {
BlogManager blogManager, BlogPostFactory blogPostFactory,
FeedFactory feedFactory, SocketFactory torSocketFactory,
Clock clock, Dns noDnsLookups) {
this.scheduler = scheduler;
this.ioExecutor = ioExecutor;
this.db = db;
this.contactGroupFactory = contactGroupFactory;
this.clientHelper = clientHelper;
this.identityManager = identityManager;
this.blogManager = blogManager;
this.blogPostFactory = blogPostFactory;
this.feedFactory = feedFactory;
this.torSocketFactory = torSocketFactory;
this.clock = clock;
this.noDnsLookups = noDnsLookups;
}
@Override
@@ -158,21 +158,21 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
}
@Override
public void addFeed(String url, GroupId g) throws DbException, IOException {
LOG.info("Adding new RSS feed...");
// TODO check for existing feed?
// fetch feed to get its metadata
Feed feed = new Feed(url, g, clock.currentTimeMillis());
public void addFeed(String url) throws DbException, IOException {
// fetch syndication feed to get its metadata
SyndFeed f;
try {
feed = fetchFeed(feed, false);
f = fetchSyndFeed(url);
} catch (FeedException e) {
throw new IOException(e);
}
// store feed
Feed feed = feedFactory.createFeed(url, f);
// store feed and new blog
Transaction txn = db.startTransaction(false);
try {
blogManager.addBlog(txn, feed.getBlog());
List<Feed> feeds = getFeeds(txn);
feeds.add(feed);
storeFeeds(txn, feeds);
@@ -181,10 +181,10 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
db.endTransaction(txn);
}
// fetch feed again, post entries this time
// fetch feed again and post entries
Feed updatedFeed;
try {
updatedFeed = fetchFeed(feed, true);
updatedFeed = fetchFeed(feed);
} catch (FeedException e) {
throw new IOException(e);
}
@@ -203,27 +203,35 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
}
@Override
public void removeFeed(String url) throws DbException {
public void removeFeed(Feed feed) throws DbException {
LOG.info("Removing RSS feed...");
Transaction txn = db.startTransaction(false);
try {
List<Feed> feeds = getFeeds(txn);
boolean found = false;
for (Feed feed : feeds) {
if (feed.getUrl().equals(url)) {
found = true;
feeds.remove(feed);
break;
}
}
if (!found) throw new DbException();
storeFeeds(txn, feeds);
// this will call removingBlog() where the feed itself gets removed
blogManager.removeBlog(txn, feed.getBlog());
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
}
@Override
public void removingBlog(Transaction txn, Blog b) throws DbException {
if (!b.isRssFeed()) return;
// delete blog's RSS feed if we have it
boolean found = false;
List<Feed> feeds = getFeeds(txn);
for (Feed f : feeds) {
if (f.getBlogId().equals(b.getId())) {
found = true;
feeds.remove(f);
break;
}
}
if (found) storeFeeds(txn, feeds);
}
@Override
public List<Feed> getFeeds() throws DbException {
List<Feed> feeds;
@@ -246,7 +254,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
for (Object object : d.getList(KEY_FEEDS)) {
if (!(object instanceof BdfDictionary))
throw new FormatException();
feeds.add(Feed.from((BdfDictionary) object));
feeds.add(feedFactory.createFeed((BdfDictionary) object));
}
} catch (FormatException e) {
throw new DbException(e);
@@ -259,7 +267,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
BdfList feedList = new BdfList();
for (Feed feed : feeds) {
feedList.add(feed.toBdfDictionary());
feedList.add(feedFactory.feedToBdfDictionary(feed));
}
BdfDictionary gm = BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
try {
@@ -300,7 +308,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
List<Feed> newFeeds = new ArrayList<Feed>(feeds.size());
for (Feed feed : feeds) {
try {
newFeeds.add(fetchFeed(feed, true));
newFeeds.add(fetchFeed(feed));
} catch (FeedException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
@@ -323,49 +331,52 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
LOG.info("Done updating RSS feeds");
}
private Feed fetchFeed(Feed feed, boolean post)
throws FeedException, IOException, DbException {
String title, description, author;
long updated = clock.currentTimeMillis();
long lastEntryTime = feed.getLastEntryTime();
SyndFeed f = getSyndFeed(getFeedInputStream(feed.getUrl()));
title = StringUtils.isNullOrEmpty(f.getTitle()) ? null : f.getTitle();
if (title != null) title = clean(title, STRIP_ALL);
description = StringUtils.isNullOrEmpty(f.getDescription()) ? null :
f.getDescription();
if (description != null) description = clean(description, STRIP_ALL);
author =
StringUtils.isNullOrEmpty(f.getAuthor()) ? null : f.getAuthor();
if (author != null) author = clean(author, STRIP_ALL);
private SyndFeed fetchSyndFeed(String url)
throws FeedException, IOException {
// fetch feed
SyndFeed f = getSyndFeed(getFeedInputStream(url));
if (f.getEntries().size() == 0)
throw new FeedException("Feed has no entries");
// clean title
String title =
StringUtils.isNullOrEmpty(f.getTitle()) ? null : f.getTitle();
if (title != null) title = clean(title, STRIP_ALL);
f.setTitle(title);
// clean description
String description =
StringUtils.isNullOrEmpty(f.getDescription()) ? null :
f.getDescription();
if (description != null) description = clean(description, STRIP_ALL);
f.setDescription(description);
// clean author
String author =
StringUtils.isNullOrEmpty(f.getAuthor()) ? null : f.getAuthor();
if (author != null) author = clean(author, STRIP_ALL);
f.setAuthor(author);
return f;
}
private Feed fetchFeed(Feed feed)
throws FeedException, IOException, DbException {
// fetch and clean feed
SyndFeed f = fetchSyndFeed(feed.getUrl());
// sort and add new entries
if (post) {
lastEntryTime = postFeedEntries(feed, f.getEntries());
}
return new Feed(feed.getUrl(), feed.getBlogId(), title, description,
author, feed.getAdded(), updated, lastEntryTime);
long lastEntryTime = postFeedEntries(feed, f.getEntries());
return feedFactory.createFeed(feed, f, lastEntryTime);
}
private InputStream getFeedInputStream(String url) throws IOException {
// Don't make local DNS lookups
Dns noLookups = new Dns() {
@Override
public List<InetAddress> lookup(String hostname)
throws UnknownHostException {
InetAddress unspecified =
InetAddress.getByAddress(hostname, UNSPECIFIED_ADDRESS);
return Collections.singletonList(unspecified);
}
};
// Build HTTP Client
OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(torSocketFactory)
.dns(noLookups)
.dns(noDnsLookups) // Don't make local DNS lookups
.connectTimeout(CONNECT_TIMEOUT, MILLISECONDS)
.build();
@@ -422,9 +433,8 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
// build post body
StringBuilder b = new StringBuilder();
if (feed.getTitle() != null) {
b.append("<h3>").append(feed.getTitle()).append("</h3>");
}
b.append("<h3>").append(feed.getTitle()).append("</h3>");
if (!StringUtils.isNullOrEmpty(entry.getTitle())) {
b.append("<h1>").append(entry.getTitle()).append("</h1>");
}
@@ -457,13 +467,17 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
// get other information for post
GroupId groupId = feed.getBlogId();
long time = clock.currentTimeMillis();
long time, now = clock.currentTimeMillis();
Date date = entry.getUpdatedDate();
if (date == null) date = entry.getPublishedDate();
if (date == null) time = now;
else time = Math.max(0, Math.min(date.getTime(), now));
String body = getPostBody(b.toString());
try {
// create and store post
LocalAuthor author = identityManager.getLocalAuthor(txn);
LocalAuthor localAuthor = feed.getLocalAuthor();
BlogPost post = blogPostFactory
.createBlogPost(groupId, time, null, author, body);
.createBlogPost(groupId, time, null, localAuthor, body);
blogManager.addLocalPost(txn, post);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.feed;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.feed.FeedManager;
import javax.inject.Inject;
@@ -21,11 +22,18 @@ public class FeedModule {
@Provides
@Singleton
FeedManager provideFeedManager(FeedManagerImpl feedManager,
LifecycleManager lifecycleManager, EventBus eventBus) {
LifecycleManager lifecycleManager, EventBus eventBus,
BlogManager blogManager) {
lifecycleManager.registerClient(feedManager);
eventBus.addListener(feedManager);
blogManager.registerRemoveBlogHook(feedManager);
return feedManager;
}
@Provides
FeedFactory provideFeedFactory(FeedFactoryImpl feedFactory) {
return feedFactory;
}
}

View File

@@ -0,0 +1,28 @@
package org.briarproject.briar.feed;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import okhttp3.Dns;
class NoDns implements Dns {
private static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
@Inject
public NoDns() {
}
@Override
public List<InetAddress> lookup(String hostname)
throws UnknownHostException {
InetAddress unspecified =
InetAddress.getByAddress(hostname, UNSPECIFIED_ADDRESS);
return Collections.singletonList(unspecified);
}
}

View File

@@ -39,14 +39,20 @@ class BlogSharingValidator extends SharingValidator {
@Override
protected GroupId validateDescriptor(BdfList descriptor)
throws FormatException {
checkSize(descriptor, 2);
checkSize(descriptor, 3);
String name = descriptor.getString(0);
checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
byte[] publicKey = descriptor.getRaw(1);
checkLength(publicKey, 1, MAX_PUBLIC_KEY_LENGTH);
boolean rssFeed = descriptor.getBoolean(2);
Author author = authorFactory.createAuthor(name, publicKey);
Blog blog = blogFactory.createBlog(author);
Blog blog;
if (rssFeed) {
blog = blogFactory.createFeedBlog(author);
} else {
blog = blogFactory.createBlog(author);
}
return blog.getId();
}

View File

@@ -44,6 +44,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_AUTHOR_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_AUTHOR_NAME;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_READ;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_RSS_FEED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIMESTAMP;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE;
@@ -198,12 +199,17 @@ public class BlogManagerImplTest extends BriarTestCase {
new BdfEntry(KEY_TYPE, POST.getInt()),
new BdfEntry(KEY_TIMESTAMP, message.getTimestamp()),
new BdfEntry(KEY_AUTHOR, authorMeta),
new BdfEntry(KEY_READ, true)
new BdfEntry(KEY_READ, true),
new BdfEntry(KEY_RSS_FEED, false)
);
context.checking(new Expectations() {{
oneOf(db).startTransaction(false);
will(returnValue(txn));
oneOf(db).getGroup(txn, blog1.getId());
will(returnValue(blog1.getGroup()));
oneOf(blogFactory).parseBlog(blog1.getGroup());
will(returnValue(blog1));
oneOf(clientHelper)
.addLocalMessage(txn, message, meta, true);
oneOf(identityManager)
@@ -297,7 +303,7 @@ public class BlogManagerImplTest extends BriarTestCase {
final LocalAuthor localAuthor =
new LocalAuthor(authorId, "Author", publicKey, privateKey,
created);
return new Blog(group, localAuthor);
return new Blog(group, localAuthor, false);
}
private BdfDictionary authorToBdfDictionary(Author a) {

View File

@@ -1,5 +1,8 @@
package org.briarproject.briar.blog;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.TestDatabaseModule;
import org.briarproject.briar.api.blog.Blog;
@@ -20,6 +23,9 @@ import java.util.Iterator;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomString;
import static org.briarproject.briar.api.blog.MessageType.COMMENT;
import static org.briarproject.briar.api.blog.MessageType.POST;
@@ -32,7 +38,8 @@ public class BlogManagerIntegrationTest
extends BriarIntegrationTest<BriarIntegrationTestComponent> {
private BlogManager blogManager0, blogManager1;
private Blog blog0, blog1;
private Blog blog0, blog1, rssBlog;
private LocalAuthor rssAuthor;
@Rule
public ExpectedException thrown = ExpectedException.none();
@@ -44,12 +51,22 @@ public class BlogManagerIntegrationTest
author0 = identityManager0.getLocalAuthor();
author1 = identityManager1.getLocalAuthor();
rssAuthor = c0.getAuthorFactory().createLocalAuthor(
getRandomString(MAX_AUTHOR_NAME_LENGTH),
getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
getRandomBytes(123));
blogManager0 = c0.getBlogManager();
blogManager1 = c1.getBlogManager();
blog0 = blogFactory.createBlog(author0);
blog1 = blogFactory.createBlog(author1);
rssBlog = blogFactory.createFeedBlog(rssAuthor);
Transaction txn = db0.startTransaction(false);
blogManager0.addBlog(txn, rssBlog);
db0.commitTransaction(txn);
db0.endTransaction(txn);
}
@Override
@@ -74,11 +91,12 @@ public class BlogManagerIntegrationTest
@Test
public void testPersonalBlogInitialisation() throws Exception {
Collection<Blog> blogs0 = blogManager0.getBlogs();
assertEquals(3, blogs0.size());
assertEquals(4, blogs0.size());
Iterator<Blog> i0 = blogs0.iterator();
assertEquals(author0, i0.next().getAuthor());
assertEquals(author1, i0.next().getAuthor());
assertEquals(author2, i0.next().getAuthor());
assertEquals(rssAuthor, i0.next().getAuthor());
Collection<Blog> blogs1 = blogManager1.getBlogs();
assertEquals(2, blogs1.size());
@@ -95,11 +113,14 @@ public class BlogManagerIntegrationTest
assertEquals(blog0, blogManager1.getBlog(blog0.getId()));
assertEquals(blog1, blogManager0.getBlog(blog1.getId()));
assertEquals(blog1, blogManager1.getBlog(blog1.getId()));
assertEquals(rssBlog, blogManager0.getBlog(rssBlog.getId()));
assertEquals(1, blogManager0.getBlogs(author0).size());
assertEquals(1, blogManager1.getBlogs(author0).size());
assertEquals(1, blogManager0.getBlogs(author1).size());
assertEquals(1, blogManager1.getBlogs(author1).size());
assertEquals(1, blogManager0.getBlogs(rssAuthor).size());
assertEquals(0, blogManager1.getBlogs(rssAuthor).size());
}
@Test
@@ -279,9 +300,8 @@ public class BlogManagerIntegrationTest
assertEquals(POST, headers1.iterator().next().getType());
// 1 reblogs that blog post
blogManager1
.addLocalComment(author1, blog1.getId(), null,
headers1.iterator().next());
blogManager1.addLocalComment(author1, blog1.getId(), null,
headers1.iterator().next());
// sync comment over
sync1To0(2, true);
@@ -304,8 +324,7 @@ public class BlogManagerIntegrationTest
sync0To1(3, true);
// check that comment arrived
headers1 =
blogManager1.getPostHeaders(blog0.getId());
headers1 = blogManager1.getPostHeaders(blog0.getId());
assertEquals(2, headers1.size());
// get header of comment
@@ -393,4 +412,63 @@ public class BlogManagerIntegrationTest
assertEquals(2, headers0.size());
}
@Test
public void testFeedPost() throws Exception {
assertTrue(rssBlog.isRssFeed());
// add a feed post to rssBlog
final String body = getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(rssBlog.getId(), clock.currentTimeMillis(),
null, author0, body);
blogManager0.addLocalPost(p);
// make sure it got saved as an RSS feed post
Collection<BlogPostHeader> headers =
blogManager0.getPostHeaders(rssBlog.getId());
assertEquals(1, headers.size());
BlogPostHeader header = headers.iterator().next();
assertEquals(POST, header.getType());
assertEquals(Author.Status.NONE, header.getAuthorStatus());
assertTrue(header.isRssFeed());
}
@Test
public void testFeedReblog() throws Exception {
// add a feed post to rssBlog
final String body = getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(rssBlog.getId(), clock.currentTimeMillis(),
null, author0, body);
blogManager0.addLocalPost(p);
// reblog feed post to own blog
Collection<BlogPostHeader> headers =
blogManager0.getPostHeaders(rssBlog.getId());
assertEquals(1, headers.size());
BlogPostHeader header = headers.iterator().next();
blogManager0.addLocalComment(author0, blog0.getId(), null, header);
// make sure it got saved as an RSS feed post
headers = blogManager0.getPostHeaders(blog0.getId());
assertEquals(1, headers.size());
BlogCommentHeader commentHeader =
(BlogCommentHeader) headers.iterator().next();
assertEquals(COMMENT, commentHeader.getType());
assertTrue(commentHeader.getParent().isRssFeed());
// reblog reblogged post again to own blog
blogManager0
.addLocalComment(author0, blog0.getId(), null, commentHeader);
// make sure it got saved as an RSS feed post
headers = blogManager0.getPostHeaders(blog0.getId());
assertEquals(2, headers.size());
for (BlogPostHeader h : headers) {
assertTrue(h instanceof BlogCommentHeader);
assertEquals(COMMENT, h.getType());
assertTrue(((BlogCommentHeader) h).getRootPost().isRssFeed());
}
}
}

View File

@@ -38,6 +38,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.KEY_ORIGINAL_PARENT_
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PARENT_MSG_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_READ;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_RSS_FEED;
import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_COMMENT;
import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_POST;
import static org.briarproject.briar.api.blog.MessageType.COMMENT;
@@ -50,7 +51,7 @@ import static org.junit.Assert.assertFalse;
public class BlogPostValidatorTest extends BriarTestCase {
private final Mockery context = new Mockery();
private final Blog blog;
private final Blog blog, rssBlog;
private final BdfDictionary authorDict;
private final ClientId clientId;
private final byte[] descriptor;
@@ -79,7 +80,8 @@ public class BlogPostValidatorTest extends BriarTestCase {
new BdfEntry(KEY_AUTHOR_NAME, author.getName()),
new BdfEntry(KEY_PUBLIC_KEY, author.getPublicKey())
);
blog = new Blog(group, author);
blog = new Blog(group, author, false);
rssBlog = new Blog(group, author, true);
MessageId messageId = new MessageId(TestUtils.getRandomId());
long timestamp = System.currentTimeMillis();
@@ -97,17 +99,28 @@ public class BlogPostValidatorTest extends BriarTestCase {
@Test
public void testValidateProperBlogPost()
throws IOException, GeneralSecurityException {
testValidateProperBlogPost(blog, false);
}
@Test
public void testValidateProperRssBlogPost()
throws IOException, GeneralSecurityException {
testValidateProperBlogPost(rssBlog, true);
}
private void testValidateProperBlogPost(Blog b, boolean rssFeed)
throws IOException, GeneralSecurityException {
final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m = BdfList.of(POST.getInt(), body, sigBytes);
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), body);
expectCrypto(SIGNING_LABEL_POST, signed, sigBytes);
BdfList signed = BdfList.of(b.getId(), message.getTimestamp(), body);
expectCrypto(b, SIGNING_LABEL_POST, signed, sigBytes);
final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary();
assertEquals(authorDict, result.getDictionary(KEY_AUTHOR));
assertFalse(result.getBoolean(KEY_READ));
assertEquals(rssFeed, result.getBoolean(KEY_RSS_FEED));
context.assertIsSatisfied();
}
@@ -137,14 +150,12 @@ public class BlogPostValidatorTest extends BriarTestCase {
MessageId pOriginalId = new MessageId(TestUtils.getRandomId());
MessageId currentId = new MessageId(TestUtils.getRandomId());
final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m =
BdfList.of(COMMENT.getInt(), comment, pOriginalId, currentId,
sigBytes);
BdfList m = BdfList.of(COMMENT.getInt(), comment, pOriginalId,
currentId, sigBytes);
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), comment,
pOriginalId, currentId);
expectCrypto(SIGNING_LABEL_COMMENT, signed, sigBytes);
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
comment, pOriginalId, currentId);
expectCrypto(blog, SIGNING_LABEL_COMMENT, signed, sigBytes);
final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary();
@@ -164,14 +175,12 @@ public class BlogPostValidatorTest extends BriarTestCase {
MessageId originalId = new MessageId(TestUtils.getRandomId());
MessageId currentId = new MessageId(TestUtils.getRandomId());
final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m =
BdfList.of(COMMENT.getInt(), null, originalId, currentId,
sigBytes);
BdfList m = BdfList.of(COMMENT.getInt(), null, originalId, currentId,
sigBytes);
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), null,
originalId, currentId);
expectCrypto(SIGNING_LABEL_COMMENT, signed, sigBytes);
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(), null,
originalId, currentId);
expectCrypto(blog, SIGNING_LABEL_COMMENT, signed, sigBytes);
final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary();
@@ -182,22 +191,33 @@ public class BlogPostValidatorTest extends BriarTestCase {
@Test
public void testValidateProperWrappedPost()
throws IOException, GeneralSecurityException {
testValidateProperWrappedPost(blog, false);
}
@Test
public void testValidateProperWrappedRssPost()
throws IOException, GeneralSecurityException {
testValidateProperWrappedPost(rssBlog, true);
}
private void testValidateProperWrappedPost(final Blog b, boolean rssFeed)
throws IOException, GeneralSecurityException {
// group descriptor, timestamp, content, signature
final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m =
BdfList.of(WRAPPED_POST.getInt(), descriptor,
message.getTimestamp(), body, sigBytes);
BdfList m = BdfList.of(WRAPPED_POST.getInt(), descriptor,
message.getTimestamp(), body, sigBytes);
BdfList signed =
BdfList.of(blog.getId(), message.getTimestamp(), body);
expectCrypto(SIGNING_LABEL_POST, signed, sigBytes);
BdfList signed = BdfList.of(b.getId(), message.getTimestamp(), body);
expectCrypto(b, SIGNING_LABEL_POST, signed, sigBytes);
final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
final byte[] originalBody = TestUtils.getRandomBytes(42);
context.checking(new Expectations() {{
oneOf(groupFactory).createGroup(clientId, descriptor);
will(returnValue(blog.getGroup()));
will(returnValue(b.getGroup()));
oneOf(blogFactory).parseBlog(b.getGroup());
will(returnValue(b));
oneOf(clientHelper).toByteArray(originalList);
will(returnValue(originalBody));
oneOf(messageFactory)
@@ -210,6 +230,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
validator.validateMessage(message, group, m).getDictionary();
assertEquals(authorDict, result.getDictionary(KEY_AUTHOR));
assertEquals(rssFeed, result.getBoolean(KEY_RSS_FEED));
context.assertIsSatisfied();
}
@@ -229,7 +250,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
comment, originalId, oldId);
expectCrypto(SIGNING_LABEL_COMMENT, signed, sigBytes);
expectCrypto(blog, SIGNING_LABEL_COMMENT, signed, sigBytes);
final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
originalId, oldId, sigBytes);
@@ -257,11 +278,12 @@ public class BlogPostValidatorTest extends BriarTestCase {
context.assertIsSatisfied();
}
private void expectCrypto(final String label, final BdfList signed,
final byte[] sig) throws IOException, GeneralSecurityException {
private void expectCrypto(final Blog b, final String label,
final BdfList signed, final byte[] sig)
throws IOException, GeneralSecurityException {
context.checking(new Expectations() {{
oneOf(blogFactory).parseBlog(group);
will(returnValue(blog));
will(returnValue(b));
oneOf(clientHelper)
.verifySignature(label, sig, author.getPublicKey(), signed);
}});

View File

@@ -0,0 +1,127 @@
package org.briarproject.briar.feed;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoModule;
import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.sync.SyncModule;
import org.briarproject.bramble.system.SystemModule;
import org.briarproject.bramble.test.TestDatabaseModule;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import org.briarproject.briar.blog.BlogModule;
import org.briarproject.briar.test.BriarTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class FeedManagerIntegrationTest extends BriarTestCase {
private LifecycleManager lifecycleManager;
private FeedManager feedManager;
private BlogManager blogManager;
private final File testDir = TestUtils.getTestDirectory();
private final File testFile = new File(testDir, "feedTest");
@Before
public void setUp() throws Exception {
assertTrue(testDir.mkdirs());
FeedManagerIntegrationTestComponent component =
DaggerFeedManagerIntegrationTestComponent.builder()
.testDatabaseModule(new TestDatabaseModule(testFile))
.build();
component.inject(this);
injectEagerSingletons(component);
lifecycleManager = component.getLifecycleManager();
lifecycleManager.startServices("feedTest");
lifecycleManager.waitForStartup();
feedManager = component.getFeedManager();
blogManager = component.getBlogManager();
}
@Test
public void testFeedImportAndRemoval() throws Exception {
// initially, there's only the one personal blog
Collection<Blog> blogs = blogManager.getBlogs();
assertEquals(1, blogs.size());
Blog personalBlog = blogs.iterator().next();
// add feed into a dedicated blog
String url = "https://www.schneier.com/blog/atom.xml";
feedManager.addFeed(url);
// then there's the feed's blog now
blogs = blogManager.getBlogs();
assertEquals(2, blogs.size());
Blog feedBlog = null;
for (Blog blog : blogs) {
if (!blog.equals(personalBlog)) feedBlog = blog;
}
assertNotNull(feedBlog);
// check the feed got saved as expected
Collection<Feed> feeds = feedManager.getFeeds();
assertEquals(1, feeds.size());
Feed feed = feeds.iterator().next();
assertTrue(feed.getLastEntryTime() > 0);
assertTrue(feed.getAdded() > 0);
assertTrue(feed.getUpdated() > 0);
assertEquals(url, feed.getUrl());
assertEquals(feedBlog, feed.getBlog());
assertEquals("Schneier on Security", feed.getTitle());
assertEquals("A blog covering security and security technology.",
feed.getDescription());
assertEquals(feed.getTitle(), feed.getBlog().getName());
assertEquals(feed.getTitle(), feed.getLocalAuthor().getName());
// check the feed entries have been added to the blog as expected
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(feedBlog.getId());
for (BlogPostHeader header : headers) {
assertTrue(header.isRssFeed());
}
// now let's remove the feed's blog again
blogManager.removeBlog(feedBlog);
blogs = blogManager.getBlogs();
assertEquals(1, blogs.size());
assertEquals(personalBlog, blogs.iterator().next());
assertEquals(0, feedManager.getFeeds().size());
}
@After
public void tearDown() throws Exception {
lifecycleManager.stopServices();
lifecycleManager.waitForShutdown();
TestUtils.deleteTestDirectory(testDir);
}
protected void injectEagerSingletons(
FeedManagerIntegrationTestComponent component) {
component.inject(new FeedModule.EagerSingletons());
component.inject(new BlogModule.EagerSingletons());
component.inject(new ContactModule.EagerSingletons());
component.inject(new CryptoModule.EagerSingletons());
component.inject(new IdentityModule.EagerSingletons());
component.inject(new LifecycleModule.EagerSingletons());
component.inject(new SyncModule.EagerSingletons());
component.inject(new SystemModule.EagerSingletons());
component.inject(new TransportModule.EagerSingletons());
}
}

View File

@@ -0,0 +1,79 @@
package org.briarproject.briar.feed;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.client.ClientModule;
import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoModule;
import org.briarproject.bramble.data.DataModule;
import org.briarproject.bramble.db.DatabaseModule;
import org.briarproject.bramble.event.EventModule;
import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.sync.SyncModule;
import org.briarproject.bramble.system.SystemModule;
import org.briarproject.bramble.test.TestDatabaseModule;
import org.briarproject.bramble.test.TestPluginConfigModule;
import org.briarproject.bramble.test.TestSeedProviderModule;
import org.briarproject.bramble.test.TestSocksModule;
import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.feed.FeedManager;
import org.briarproject.briar.blog.BlogModule;
import org.briarproject.briar.client.BriarClientModule;
import org.briarproject.briar.test.TestDnsModule;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
TestDatabaseModule.class,
TestPluginConfigModule.class,
TestSeedProviderModule.class,
TestSocksModule.class,
TestDnsModule.class,
LifecycleModule.class,
BriarClientModule.class,
ClientModule.class,
ContactModule.class,
CryptoModule.class,
BlogModule.class,
FeedModule.class,
DataModule.class,
DatabaseModule.class,
EventModule.class,
IdentityModule.class,
SyncModule.class,
SystemModule.class,
TransportModule.class
})
interface FeedManagerIntegrationTestComponent {
void inject(FeedManagerIntegrationTest testCase);
void inject(FeedModule.EagerSingletons init);
void inject(BlogModule.EagerSingletons init);
void inject(ContactModule.EagerSingletons init);
void inject(CryptoModule.EagerSingletons init);
void inject(IdentityModule.EagerSingletons init);
void inject(LifecycleModule.EagerSingletons init);
void inject(SyncModule.EagerSingletons init);
void inject(SystemModule.EagerSingletons init);
void inject(TransportModule.EagerSingletons init);
LifecycleManager getLifecycleManager();
FeedManager getFeedManager();
BlogManager getBlogManager();
}

View File

@@ -19,6 +19,7 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.Group;
@@ -360,6 +361,14 @@ public class IntroductionIntegrationTest
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response1Received);
// sync fake transport properties back to 1, so Message ACK can arrive
// and the assertDefaultUiMessages() check at the end will not fail
TransportProperties tp = new TransportProperties(
Collections.singletonMap("key", "value"));
c0.getTransportPropertyManager()
.mergeLocalProperties(new TransportId("fake"), tp);
sync0To1(1, true);
// sync second response
sync2To0(1, true);
eventWaiter.await(TIMEOUT, 1);
@@ -836,14 +845,32 @@ public class IntroductionIntegrationTest
}
private void assertDefaultUiMessages() throws DbException {
assertEquals(2, introductionManager0.getIntroductionMessages(
contactId1From0).size());
assertEquals(2, introductionManager0.getIntroductionMessages(
contactId2From0).size());
assertEquals(2, introductionManager1.getIntroductionMessages(
contactId0From1).size());
assertEquals(2, introductionManager2.getIntroductionMessages(
contactId0From2).size());
Collection<IntroductionMessage> messages =
introductionManager0.getIntroductionMessages(contactId1From0);
assertEquals(2, messages.size());
assertMessagesAreAcked(messages);
messages = introductionManager0.getIntroductionMessages(
contactId2From0);
assertEquals(2, messages.size());
assertMessagesAreAcked(messages);
messages = introductionManager1.getIntroductionMessages(
contactId0From1);
assertEquals(2, messages.size());
assertMessagesAreAcked(messages);
messages = introductionManager2.getIntroductionMessages(
contactId0From2);
assertEquals(2, messages.size());
assertMessagesAreAcked(messages);
}
private void assertMessagesAreAcked(
Collection<IntroductionMessage> messages) {
for (IntroductionMessage msg : messages) {
if (msg.isLocal()) assertTrue(msg.isSeen());
}
}
private void addListeners(boolean accept1, boolean accept2) {

View File

@@ -23,8 +23,8 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
private final byte[] publicKey =
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
private final Author author = new Author(authorId, authorName, publicKey);
private final Blog blog = new Blog(group, author);
private final BdfList descriptor = BdfList.of(authorName, publicKey);
private final Blog blog = new Blog(group, author, false);
private final BdfList descriptor = BdfList.of(authorName, publicKey, false);
private final String content =
TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH);
@@ -64,7 +64,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class)
public void testRejectsNullBlogName() throws Exception {
BdfList invalidDescriptor = BdfList.of(null, publicKey);
BdfList invalidDescriptor = BdfList.of(null, publicKey, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -72,7 +72,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class)
public void testRejectsNonStringBlogName() throws Exception {
BdfList invalidDescriptor = BdfList.of(123, publicKey);
BdfList invalidDescriptor = BdfList.of(123, publicKey, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -80,7 +80,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class)
public void testRejectsTooShortBlogName() throws Exception {
BdfList invalidDescriptor = BdfList.of("", publicKey);
BdfList invalidDescriptor = BdfList.of("", publicKey, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -89,7 +89,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test
public void testAcceptsMinLengthBlogName() throws Exception {
String shortBlogName = TestUtils.getRandomString(1);
BdfList validDescriptor = BdfList.of(shortBlogName, publicKey);
BdfList validDescriptor = BdfList.of(shortBlogName, publicKey, false);
expectCreateBlog(shortBlogName, publicKey);
expectEncodeMetadata(INVITE);
BdfMessageContext messageContext = v.validateMessage(message, group,
@@ -102,7 +102,8 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
public void testRejectsTooLongBlogName() throws Exception {
String invalidBlogName =
TestUtils.getRandomString(MAX_BLOG_NAME_LENGTH + 1);
BdfList invalidDescriptor = BdfList.of(invalidBlogName, publicKey);
BdfList invalidDescriptor =
BdfList.of(invalidBlogName, publicKey, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -110,7 +111,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class)
public void testRejectsNullPublicKey() throws Exception {
BdfList invalidDescriptor = BdfList.of(authorName, null);
BdfList invalidDescriptor = BdfList.of(authorName, null, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -118,7 +119,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class)
public void testRejectsNonRawPublicKey() throws Exception {
BdfList invalidDescriptor = BdfList.of(authorName, 123);
BdfList invalidDescriptor = BdfList.of(authorName, 123, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -127,7 +128,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class)
public void testRejectsTooLongPublicKey() throws Exception {
byte[] invalidKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1);
BdfList invalidDescriptor = BdfList.of(authorName, invalidKey);
BdfList invalidDescriptor = BdfList.of(authorName, invalidKey, false);
v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null));
@@ -136,7 +137,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test
public void testAcceptsMinLengthPublicKey() throws Exception {
byte[] key = TestUtils.getRandomBytes(1);
BdfList validDescriptor = BdfList.of(authorName, key);
BdfList validDescriptor = BdfList.of(authorName, key, false);
expectCreateBlog(authorName, key);
expectEncodeMetadata(INVITE);

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
@@ -136,4 +137,5 @@ public interface BriarIntegrationTestComponent {
TransportPropertyManager getTransportPropertyManager();
AuthorFactory getAuthorFactory();
}

View File

@@ -0,0 +1,15 @@
package org.briarproject.briar.test;
import dagger.Module;
import dagger.Provides;
import okhttp3.Dns;
@Module
public class TestDnsModule {
@Provides
Dns provideDns() {
return Dns.SYSTEM;
}
}