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.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginException; import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.AbstractBluetoothPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
@@ -75,7 +74,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class DroidtoothPlugin implements DuplexPlugin { class DroidtoothPlugin<C, S>
extends AbstractBluetoothPlugin<C, S>{
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(DroidtoothPlugin.class.getName()); Logger.getLogger(DroidtoothPlugin.class.getName());
@@ -84,16 +84,10 @@ class DroidtoothPlugin implements DuplexPlugin {
private static final String DISCOVERY_FINISHED = private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED"; "android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor; private final AndroidExecutor androidExecutor;
private final Context appContext; 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 final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false;
private volatile boolean wasEnabledByUs = false; private volatile boolean wasEnabledByUs = false;
private volatile BluetoothStateReceiver receiver = null; private volatile BluetoothStateReceiver receiver = null;
private volatile BluetoothServerSocket socket = null; private volatile BluetoothServerSocket socket = null;
@@ -104,29 +98,15 @@ class DroidtoothPlugin implements DuplexPlugin {
DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor, DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Backoff backoff, Context appContext, SecureRandom secureRandom, Backoff backoff,
DuplexPluginCallback callback, int maxLatency) { DuplexPluginCallback callback, int maxLatency) {
this.ioExecutor = ioExecutor;
super(ioExecutor, secureRandom, backoff, maxLatency, callback);
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.appContext = appContext; this.appContext = appContext;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
} }
@Override @Override
public TransportId getId() { protected void close(S ss) throws IOException {
return ID; ((BluetoothServerSocket)ss).close();
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
} }
@Override @Override
@@ -194,14 +174,14 @@ class DroidtoothPlugin implements DuplexPlugin {
BluetoothServerSocket ss; BluetoothServerSocket ss;
try { try {
ss = adapter.listenUsingInsecureRfcommWithServiceRecord( ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
"RFCOMM", getUuid()); "RFCOMM", UUID.fromString(getUuid()));
} catch (IOException e) { } catch (IOException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
return; return;
} }
if (!isRunning()) { if (!isRunning()) {
tryToClose(ss); tryToClose((S)ss);
return; return;
} }
LOG.info("Socket bound"); 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() { private void acceptContactConnections() {
while (isRunning()) { while (isRunning()) {
BluetoothSocket s; BluetoothSocket s;
@@ -261,9 +218,9 @@ class DroidtoothPlugin implements DuplexPlugin {
@Override @Override
public void stop() { public void stop() {
running = false; this.running = false;
if (receiver != null) appContext.unregisterReceiver(receiver); if (receiver != null) appContext.unregisterReceiver(receiver);
tryToClose(socket); tryToClose((S)socket);
// Disable Bluetooth if we enabled it and it's still enabled // Disable Bluetooth if we enabled it and it's still enabled
if (wasEnabledByUs && adapter.isEnabled()) { if (wasEnabledByUs && adapter.isEnabled()) {
if (adapter.disable()) LOG.info("Disabling Bluetooth"); if (adapter.disable()) LOG.info("Disabling Bluetooth");
@@ -276,42 +233,20 @@ class DroidtoothPlugin implements DuplexPlugin {
return running && adapter != null && adapter.isEnabled(); return running && adapter != null && adapter.isEnabled();
} }
@Override protected Runnable returnPollRunnable(final String address, final String uuid,
public boolean shouldPoll() { final ContactId c) {
return true; return new Runnable() {
} @Override
public void run() {
@Override if (!running) return;
public int getPollingInterval() { BluetoothSocket s = connect(address, uuid);
return backoff.getPollingInterval(); if (s != null) {
} backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s));
@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));
}
} }
}); }
}
};
} }
@Nullable @Nullable
@@ -347,37 +282,17 @@ class DroidtoothPlugin implements DuplexPlugin {
LOG.info("Failed to connect to " + scrubMacAddress(address) LOG.info("Failed to connect to " + scrubMacAddress(address)
+ ": " + e); + ": " + e);
} }
tryToClose(s); tryToClose((S)s);
return null; 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 connectToAddress(String address, String uuid) {
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;
BluetoothSocket s = connect(address, uuid); BluetoothSocket s = connect(address, uuid);
if (s == null) return null; return s == null ? null : wrapSocket(s);
return new DroidtoothTransportConnection(this, s);
} }
@Override
public boolean supportsInvitations() {
return true;
}
@Override @Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
@@ -426,7 +341,7 @@ class DroidtoothPlugin implements DuplexPlugin {
return null; return null;
} finally { } finally {
// Closing the socket will terminate the listener task // Closing the socket will terminate the listener task
tryToClose(ss); tryToClose((S)ss);
closeSockets(futures, chosen); closeSockets(futures, chosen);
} }
} }
@@ -458,11 +373,6 @@ class DroidtoothPlugin implements DuplexPlugin {
}); });
} }
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override @Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) { public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!isRunning()) return null; if (!isRunning()) return null;
@@ -487,31 +397,6 @@ class DroidtoothPlugin implements DuplexPlugin {
return new BluetoothKeyAgreementListener(descriptor, ss); 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 { private class BluetoothStateReceiver extends BroadcastReceiver {
@@ -523,7 +408,7 @@ class DroidtoothPlugin implements DuplexPlugin {
bind(); bind();
} else if (state == STATE_OFF) { } else if (state == STATE_OFF) {
LOG.info("Bluetooth disabled"); LOG.info("Bluetooth disabled");
tryToClose(socket); tryToClose((S)socket);
} }
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0); int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
if (scanMode == SCAN_MODE_NONE) { if (scanMode == SCAN_MODE_NONE) {

View File

@@ -13,7 +13,9 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault @NotNullByDefault
public class Author { 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 AuthorId id;
private final String name; private final String name;

View File

@@ -8,6 +8,7 @@ public interface TorConstants {
int CONTROL_PORT = 59051; int CONTROL_PORT = 59051;
int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds int CONNECT_TO_PROXY_TIMEOUT = 5000; // Milliseconds
int EXTRA_SOCKET_TIMEOUT = 30000; // Milliseconds
String PREF_TOR_NETWORK = "network"; String PREF_TOR_NETWORK = "network";
String PREF_TOR_PORT = "port"; 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); acked.add(m);
} }
} }
transaction.attach(new MessagesAckedEvent(c, acked)); if (acked.size() > 0) {
transaction.attach(new MessagesAckedEvent(c, acked));
}
} }
@Override @Override

View File

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

View File

@@ -8,6 +8,7 @@ import dagger.Module;
import dagger.Provides; import dagger.Provides;
import static org.briarproject.bramble.api.plugin.TorConstants.CONNECT_TO_PROXY_TIMEOUT; 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; import static org.briarproject.bramble.api.plugin.TorConstants.SOCKS_PORT;
@Module @Module
@@ -17,6 +18,7 @@ public class SocksModule {
SocketFactory provideTorSocketFactory() { SocketFactory provideTorSocketFactory() {
InetSocketAddress proxy = new InetSocketAddress("127.0.0.1", InetSocketAddress proxy = new InetSocketAddress("127.0.0.1",
SOCKS_PORT); 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 static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
private final SocketAddress proxy; 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.proxy = proxy;
this.connectToProxyTimeout = connectToProxyTimeout; this.connectToProxyTimeout = connectToProxyTimeout;
this.extraSocketTimeout = extraSocketTimeout;
} }
@Override @Override
@@ -47,7 +49,7 @@ class SocksSocket extends Socket {
InetAddress address = inet.getAddress(); InetAddress address = inet.getAddress();
if (address != null if (address != null
&& !Arrays.equals(address.getAddress(), UNSPECIFIED_ADDRESS)) { && !Arrays.equals(address.getAddress(), UNSPECIFIED_ADDRESS)) {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
String host = inet.getHostName(); String host = inet.getHostName();
if (host.length() > 255) throw new IllegalArgumentException(); if (host.length() > 255) throw new IllegalArgumentException();
@@ -62,16 +64,16 @@ class SocksSocket extends Socket {
sendMethodRequest(out); sendMethodRequest(out);
receiveMethodResponse(in); receiveMethodResponse(in);
// Use the supplied timeout temporarily // Use the supplied timeout temporarily, plus any configured extra
int oldTimeout = getSoTimeout(); int oldTimeout = getSoTimeout();
setSoTimeout(timeout); setSoTimeout(timeout + extraSocketTimeout);
// Connect to the endpoint via the proxy // Connect to the endpoint via the proxy
sendConnectRequest(out, host, port); sendConnectRequest(out, host, port);
receiveConnectResponse(in); receiveConnectResponse(in);
// Restore the old timeout // Restore the old timeout, plus any configured extra
setSoTimeout(oldTimeout); setSoTimeout(oldTimeout + extraSocketTimeout);
} }
private void sendMethodRequest(OutputStream out) throws IOException { private void sendMethodRequest(OutputStream out) throws IOException {

View File

@@ -11,16 +11,18 @@ import javax.net.SocketFactory;
class SocksSocketFactory extends SocketFactory { class SocksSocketFactory extends SocketFactory {
private final SocketAddress proxy; 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.proxy = proxy;
this.connectToProxyTimeout = connectToProxyTimeout; this.connectToProxyTimeout = connectToProxyTimeout;
this.extraSocketTimeout = extraSocketTimeout;
} }
@Override @Override
public Socket createSocket() { public Socket createSocket() {
return new SocksSocket(proxy, connectToProxyTimeout); return new SocksSocket(proxy, connectToProxyTimeout, extraSocketTimeout);
} }
@Override @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.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginException; import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.AbstractBluetoothPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.util.OsUtils; import org.briarproject.bramble.util.OsUtils;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import java.io.IOError;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -57,46 +57,22 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class BluetoothPlugin implements DuplexPlugin { class BluetoothPlugin<C, S> extends AbstractBluetoothPlugin<C, S> {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(BluetoothPlugin.class.getName()); 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 Semaphore discoverySemaphore = new Semaphore(1);
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false;
private volatile StreamConnectionNotifier socket = null; private volatile StreamConnectionNotifier socket = null;
private volatile LocalDevice localDevice = null; private volatile LocalDevice localDevice = null;
BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom, BluetoothPlugin(Executor ioExecutor,
Backoff backoff, DuplexPluginCallback callback, int maxLatency) { SecureRandom secureRandom,
this.ioExecutor = ioExecutor; Backoff backoff, int maxLatency,
this.secureRandom = secureRandom; DuplexPluginCallback callback) {
this.backoff = backoff; super(ioExecutor, secureRandom, backoff, maxLatency, callback);
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;
} }
@Override @Override
@@ -139,7 +115,7 @@ class BluetoothPlugin implements DuplexPlugin {
return; return;
} }
if (!running) { if (!running) {
tryToClose(ss); tryToClose((S)ss);
return; return;
} }
socket = ss; socket = ss;
@@ -153,29 +129,16 @@ class BluetoothPlugin implements DuplexPlugin {
private String makeUrl(String address, String uuid) { private String makeUrl(String address, String uuid) {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM"; return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
} }
//
private String getUuid() { // private void tryToClose(@Nullable StreamConnectionNotifier ss) {
String uuid = callback.getLocalProperties().get(PROP_UUID); // try {
if (uuid == null) { // if (ss != null) ss.close();
byte[] random = new byte[UUID_BYTES]; // } catch (IOException e) {
secureRandom.nextBytes(random); // if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
uuid = UUID.nameUUIDFromBytes(random).toString(); // } finally {
TransportProperties p = new TransportProperties(); // callback.transportDisabled();
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 acceptContactConnections(StreamConnectionNotifier ss) { private void acceptContactConnections(StreamConnectionNotifier ss) {
while (true) { while (true) {
@@ -196,54 +159,33 @@ class BluetoothPlugin implements DuplexPlugin {
private DuplexTransportConnection wrapSocket(StreamConnection s) { private DuplexTransportConnection wrapSocket(StreamConnection s) {
return new BluetoothTransportConnection(this, s); return new BluetoothTransportConnection(this, s);
} }
//
// @Override
// public void stop() {
// running = false;
// tryToClose(socket);
// }
@Override @Override
public void stop() { protected void close(S ss) throws IOException {
running = false; ((StreamConnection)ss).close();
tryToClose(socket);
} }
@Override @Override
public boolean isRunning() { public Runnable returnPollRunnable(final String address, final String uuid,
return running; final ContactId c) {
}
@Override return new Runnable() {
public boolean shouldPoll() { @Override
return true; public void run() {
} if (!running) return;
StreamConnection s = connect(makeUrl(address, uuid));
@Override if (s != null) {
public int getPollingInterval() { backoff.reset();
return backoff.getPollingInterval(); callback.outgoingConnectionCreated(c, wrapSocket(s));
}
@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));
}
} }
}); }
} };
} }
private StreamConnection connect(String url) { private StreamConnection connect(String url) {
@@ -259,25 +201,13 @@ class BluetoothPlugin implements DuplexPlugin {
} }
@Override @Override
public DuplexTransportConnection createConnection(ContactId c) { protected DuplexTransportConnection connectToAddress(String address, String uuid) {
if (!running) return null; String url = makeUrl(address, uuid);
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);
StreamConnection s = connect(url); StreamConnection s = connect(url);
if (s == null) return null; if (s == null) return null;
return new BluetoothTransportConnection(this, s); return new BluetoothTransportConnection(this, s);
} }
@Override
public boolean supportsInvitations() {
return true;
}
@Override @Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) { long timeout, boolean alice) {
@@ -297,7 +227,7 @@ class BluetoothPlugin implements DuplexPlugin {
return null; return null;
} }
if (!running) { if (!running) {
tryToClose(ss); tryToClose((S)ss);
return null; return null;
} }
// Create the background tasks // Create the background tasks
@@ -330,7 +260,7 @@ class BluetoothPlugin implements DuplexPlugin {
return null; return null;
} finally { } finally {
// Closing the socket will terminate the listener task // Closing the socket will terminate the listener task
tryToClose(ss); tryToClose((S)ss);
closeSockets(futures, chosen); closeSockets(futures, chosen);
} }
} }
@@ -362,11 +292,6 @@ class BluetoothPlugin implements DuplexPlugin {
}); });
} }
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override @Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) { public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!running) return null; if (!running) return null;
@@ -385,7 +310,7 @@ class BluetoothPlugin implements DuplexPlugin {
return null; return null;
} }
if (!running) { if (!running) {
tryToClose(ss); tryToClose((S)ss);
return null; return null;
} }
BdfList descriptor = new BdfList(); BdfList descriptor = new BdfList();
@@ -395,33 +320,6 @@ class BluetoothPlugin implements DuplexPlugin {
return new BluetoothKeyAgreementListener(descriptor, ss); 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() { private void makeDeviceDiscoverable() {
// Try to make the device discoverable (requires root on Linux) // Try to make the device discoverable (requires root on Linux)
try { try {

View File

@@ -59,7 +59,6 @@ import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE; import static android.app.Notification.DEFAULT_VIBRATE;
import static android.content.Context.NOTIFICATION_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; 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_MESSAGE;
import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL; import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET; import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
@@ -310,7 +309,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
ContactId c = contactCounts.keySet().iterator().next(); ContactId c = contactCounts.keySet().iterator().next();
i.putExtra(CONTACT_ID, c.getInt()); i.putExtra(CONTACT_ID, c.getInt());
i.setData(Uri.parse(CONTACT_URI + "/" + 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); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(ConversationActivity.class); t.addParentStack(ConversationActivity.class);
t.addNextIntent(i); t.addNextIntent(i);
@@ -319,7 +318,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the contact list // Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class); Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_CONTACTS, true); 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)); i.setData(Uri.parse(CONTACT_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class); t.addParentStack(NavDrawerActivity.class);
@@ -415,7 +414,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
i.putExtra(GROUP_ID, g.getBytes()); i.putExtra(GROUP_ID, g.getBytes());
String idHex = StringUtils.toHexString(g.getBytes()); String idHex = StringUtils.toHexString(g.getBytes());
i.setData(Uri.parse(GROUP_URI + "/" + idHex)); 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); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(GroupActivity.class); t.addParentStack(GroupActivity.class);
t.addNextIntent(i); t.addNextIntent(i);
@@ -424,7 +423,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the group list // Touching the notification shows the group list
Intent i = new Intent(appContext, NavDrawerActivity.class); Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_GROUPS, true); 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)); i.setData(Uri.parse(GROUP_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class); t.addParentStack(NavDrawerActivity.class);
@@ -507,7 +506,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
i.putExtra(GROUP_ID, g.getBytes()); i.putExtra(GROUP_ID, g.getBytes());
String idHex = StringUtils.toHexString(g.getBytes()); String idHex = StringUtils.toHexString(g.getBytes());
i.setData(Uri.parse(FORUM_URI + "/" + idHex)); 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); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(ForumActivity.class); t.addParentStack(ForumActivity.class);
t.addNextIntent(i); t.addNextIntent(i);
@@ -516,7 +515,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the forum list // Touching the notification shows the forum list
Intent i = new Intent(appContext, NavDrawerActivity.class); Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_FORUMS, true); 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)); i.setData(Uri.parse(FORUM_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class); t.addParentStack(NavDrawerActivity.class);
@@ -595,7 +594,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the combined blog feed // Touching the notification shows the combined blog feed
Intent i = new Intent(appContext, NavDrawerActivity.class); Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_BLOGS, true); 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)); i.setData(Uri.parse(BLOG_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class); t.addParentStack(NavDrawerActivity.class);
@@ -651,7 +650,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
// Touching the notification shows the contact list // Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class); Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_CONTACTS, true); 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)); i.setData(Uri.parse(CONTACT_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext); TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class); 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.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; 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_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.CATEGORY_SERVICE;
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN; import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET; 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.setWhen(0); // Don't show the time
b.setOngoing(true); b.setOngoing(true);
Intent i = new Intent(this, NavDrawerActivity.class); Intent i = new Intent(this, NavDrawerActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP | i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
FLAG_ACTIVITY_SINGLE_TOP);
b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0)); b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0));
if (Build.VERSION.SDK_INT >= 21) { if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_SERVICE); b.setCategory(CATEGORY_SERVICE);

View File

@@ -6,10 +6,10 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; 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.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v7.preference.PreferenceManager; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.android.ScreenFilterMonitor; 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.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
@@ -31,32 +36,57 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static android.Manifest.permission.SYSTEM_ALERT_WINDOW; import static android.Manifest.permission.SYSTEM_ALERT_WINDOW;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.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 @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ScreenFilterMonitorImpl extends BroadcastReceiver public class ScreenFilterMonitorImpl extends BroadcastReceiver
implements Service, implements Service, ScreenFilterMonitor {
ScreenFilterMonitor {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ScreenFilterMonitorImpl.class.getName()); Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
private static final String PREF_SCREEN_FILTER_APPS = private static final String PREF_SCREEN_FILTER_APPS =
"shownScreenFilterApps"; "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 Context appContext;
private final AndroidExecutor androidExecutor; private final AndroidExecutor androidExecutor;
private final LinkedList<String> appNames = new LinkedList<>();
private final PackageManager pm; private final PackageManager pm;
private final SharedPreferences prefs; private final SharedPreferences prefs;
private final AtomicBoolean used = new AtomicBoolean(false); 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> apps = new HashSet<>();
private final Set<String> shownApps; private final Set<String> shownApps;
// Used solely for the UiThread
private boolean serviceStarted = false; private boolean serviceStarted = false;
@Inject @Inject
@@ -75,7 +105,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
@Override @Override
public Void call() { public Void call() {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); intentFilter.addAction(ACTION_PACKAGE_ADDED);
intentFilter.addDataScheme("package"); intentFilter.addDataScheme("package");
appContext.registerReceiver(ScreenFilterMonitorImpl.this, appContext.registerReceiver(ScreenFilterMonitorImpl.this,
intentFilter); intentFilter);
@@ -109,9 +139,8 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
} }
private Set<String> getShownScreenFilterApps() { private Set<String> getShownScreenFilterApps() {
// res must not be modified // Result must not be modified
Set<String> s = Set<String> s = prefs.getStringSet(PREF_SCREEN_FILTER_APPS, null);
prefs.getStringSet(PREF_SCREEN_FILTER_APPS, null);
HashSet<String> result = new HashSet<>(); HashSet<String> result = new HashSet<>();
if (s != null) { if (s != null) {
result.addAll(s); result.addAll(s);
@@ -121,7 +150,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { if (!intent.getBooleanExtra(EXTRA_REPLACING, false)) {
final String packageName = final String packageName =
intent.getData().getEncodedSchemeSpecificPart(); intent.getData().getEncodedSchemeSpecificPart();
androidExecutor.runOnUiThread(new Runnable() { androidExecutor.runOnUiThread(new Runnable() {
@@ -155,7 +184,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
@Override @Override
@UiThread @UiThread
public void storeAppsAsShown(Collection<String> s, boolean persistent) { public void storeAppsAsShown(Collection<String> s, boolean persistent) {
HashSet<String> buf = new HashSet(s); HashSet<String> buf = new HashSet<>(s);
shownApps.addAll(buf); shownApps.addAll(buf);
if (persistent && !s.isEmpty()) { if (persistent && !s.isEmpty()) {
buf.addAll(getShownScreenFilterApps()); buf.addAll(getShownScreenFilterApps());
@@ -168,7 +197,7 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
private Set<String> getInstalledScreenFilterApps() { private Set<String> getInstalledScreenFilterApps() {
HashSet<String> screenFilterApps = new HashSet<>(); HashSet<String> screenFilterApps = new HashSet<>();
List<PackageInfo> packageInfos = List<PackageInfo> packageInfos =
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS); pm.getInstalledPackages(GET_PERMISSIONS);
for (PackageInfo packageInfo : packageInfos) { for (PackageInfo packageInfo : packageInfos) {
if (isOverlayApp(packageInfo)) { if (isOverlayApp(packageInfo)) {
String name = pkgToString(packageInfo); String name = pkgToString(packageInfo);
@@ -180,25 +209,22 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
return screenFilterApps; 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. // returns the app name.
@Nullable @Nullable
private String isOverlayApp(String pkg) { private String isOverlayApp(String pkg) {
try { try {
PackageInfo pkgInfo = PackageInfo pkgInfo = pm.getPackageInfo(pkg, GET_PERMISSIONS);
pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS);
if (isOverlayApp(pkgInfo)) { if (isOverlayApp(pkgInfo)) {
return pkgToString(pkgInfo); return pkgToString(pkgInfo);
} }
} catch (PackageManager.NameNotFoundException ignored) { } catch (NameNotFoundException e) {
if (LOG.isLoggable(Level.WARNING)) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
LOG.warning("Package name not found: " + pkg);
}
} }
return null; return null;
} }
// Fetch the application name for a given package. // Fetches the application name for a given package.
@Nullable @Nullable
private String pkgToString(PackageInfo pkgInfo) { private String pkgToString(PackageInfo pkgInfo) {
CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo); CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo);
@@ -208,25 +234,49 @@ public class ScreenFilterMonitorImpl extends BroadcastReceiver
return null; 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) { private boolean isOverlayApp(PackageInfo packageInfo) {
int mask = ApplicationInfo.FLAG_SYSTEM | int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP;
ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
// Ignore system apps // Ignore system apps
if ((packageInfo.applicationInfo.flags & mask) != 0) { if ((packageInfo.applicationInfo.flags & mask) != 0) {
return false; return false;
} }
//Get Permissions // Ignore Play Services, it's effectively a system app
String[] requestedPermissions = if (isPlayServices(packageInfo.packageName)) {
packageInfo.requestedPermissions; return false;
}
// Get permissions
String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions != null) { if (requestedPermissions != null) {
for (String requestedPermission : requestedPermissions) { for (String requestedPermission : requestedPermissions) {
if (requestedPermission if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) {
.equals(SYSTEM_ALERT_WINDOW)) {
return true; return true;
} }
} }
} }
return false; 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.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; 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 android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
@@ -149,14 +148,14 @@ public class BlogFragment extends BaseFragment
return true; return true;
case R.id.action_blog_share: case R.id.action_blog_share:
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class); 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()); i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_BLOG); startActivityForResult(i2, REQUEST_SHARE_BLOG);
return true; return true;
case R.id.action_blog_sharing_status: case R.id.action_blog_sharing_status:
Intent i3 = new Intent(getActivity(), Intent i3 = new Intent(getActivity(),
BlogSharingStatusActivity.class); BlogSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes()); i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3); startActivity(i3);
return true; return true;

View File

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

View File

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

View File

@@ -179,7 +179,6 @@ public class FeedFragment extends BaseFragment implements
case R.id.action_rss_feeds_import: case R.id.action_rss_feeds_import:
Intent i2 = Intent i2 =
new Intent(getActivity(), RssFeedImportActivity.class); new Intent(getActivity(), RssFeedImportActivity.class);
i2.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i2); startActivity(i2);
return true; return true;
case R.id.action_rss_feeds_manage: 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;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.briar.R; import org.briarproject.briar.R;
@@ -39,12 +39,7 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
if (item == null) return; if (item == null) return;
// Feed Title // Feed Title
if (item.getTitle() != null) { ui.title.setText(item.getTitle());
ui.title.setText(item.getTitle());
ui.title.setVisibility(VISIBLE);
} else {
ui.title.setVisibility(GONE);
}
// Delete Button // Delete Button
ui.delete.setOnClickListener(new OnClickListener() { ui.delete.setOnClickListener(new OnClickListener() {
@@ -75,6 +70,14 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
} else { } else {
ui.description.setVisibility(GONE); 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 @Override
@@ -99,8 +102,9 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
} }
static class FeedViewHolder extends RecyclerView.ViewHolder { static class FeedViewHolder extends RecyclerView.ViewHolder {
private final View layout;
private final TextView title; private final TextView title;
private final ImageView delete; private final ImageButton delete;
private final TextView imported; private final TextView imported;
private final TextView updated; private final TextView updated;
private final TextView author; private final TextView author;
@@ -110,8 +114,9 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
private FeedViewHolder(View v) { private FeedViewHolder(View v) {
super(v); super(v);
layout = v;
title = (TextView) v.findViewById(R.id.titleView); 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); imported = (TextView) v.findViewById(R.id.importedView);
updated = (TextView) v.findViewById(R.id.updatedView); updated = (TextView) v.findViewById(R.id.updatedView);
author = (TextView) v.findViewById(R.id.authorView); author = (TextView) v.findViewById(R.id.authorView);
@@ -121,6 +126,7 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
} }
interface RssFeedListener { interface RssFeedListener {
void onFeedClick(Feed feed);
void onDeleteClick(Feed feed); void onDeleteClick(Feed feed);
} }

View File

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

View File

@@ -1,15 +1,16 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
@@ -23,6 +24,7 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.support.design.widget.Snackbar.LENGTH_LONG; import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -34,7 +36,6 @@ public class RssFeedManageActivity extends BriarActivity
private BriarRecyclerView list; private BriarRecyclerView list;
private RssFeedAdapter adapter; private RssFeedAdapter adapter;
private GroupId groupId;
@Inject @Inject
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@@ -44,12 +45,6 @@ public class RssFeedManageActivity extends BriarActivity
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(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); setContentView(R.layout.activity_rss_feed_manage);
adapter = new RssFeedAdapter(this, this); adapter = new RssFeedAdapter(this, this);
@@ -87,7 +82,6 @@ public class RssFeedManageActivity extends BriarActivity
return true; return true;
case R.id.action_rss_feeds_import: case R.id.action_rss_feeds_import:
Intent i = new Intent(this, RssFeedImportActivity.class); Intent i = new Intent(this, RssFeedImportActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i); startActivity(i);
return true; return true;
default: default:
@@ -100,21 +94,32 @@ public class RssFeedManageActivity extends BriarActivity
component.inject(this); 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 @Override
public void onDeleteClick(final Feed feed) { public void onDeleteClick(final Feed feed) {
runOnDbThread(new Runnable() { DialogInterface.OnClickListener okListener =
@Override new DialogInterface.OnClickListener() {
public void run() { @Override
try { public void onClick(DialogInterface dialog, int which) {
feedManager.removeFeed(feed.getUrl()); deleteFeed(feed);
onFeedDeleted(feed); }
} catch (DbException e) { };
if (LOG.isLoggable(WARNING)) AlertDialog.Builder builder = new AlertDialog.Builder(this,
LOG.log(WARNING, e.toString(), e); R.style.BriarDialogTheme);
onDeleteError(); 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() { 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() { private void onLoadError() {
runOnUiThreadUnlessDestroyed(new Runnable() { runOnUiThreadUnlessDestroyed(new Runnable() {
@Override @Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.TextView;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -40,7 +41,7 @@ public class GroupMemberListActivity extends BriarActivity {
public void onCreate(@Nullable final Bundle state) { public void onCreate(@Nullable final Bundle state) {
super.onCreate(state); super.onCreate(state);
setContentView(R.layout.list); setContentView(R.layout.activity_sharing_status);
Intent i = getIntent(); Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID); byte[] b = i.getByteArrayExtra(GROUP_ID);
@@ -52,6 +53,9 @@ public class GroupMemberListActivity extends BriarActivity {
list.setLayoutManager(linearLayoutManager); list.setLayoutManager(linearLayoutManager);
adapter = new MemberListAdapter(this); adapter = new MemberListAdapter(this);
list.setAdapter(adapter); list.setAdapter(adapter);
TextView info = (TextView) findViewById(R.id.info);
info.setText(R.string.sharing_status_groups);
} }
@Override @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.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -25,6 +26,11 @@ public class BlogSharingStatusActivity extends SharingStatusActivity {
component.inject(this); component.inject(this);
} }
@Override
int getInfoText() {
return R.string.sharing_status_blog;
}
@Override @Override
@DatabaseExecutor @DatabaseExecutor
protected Collection<Contact> getSharedWith() throws DbException { 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.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.forum.ForumSharingManager;
@@ -25,6 +26,11 @@ public class ForumSharingStatusActivity extends SharingStatusActivity {
component.inject(this); component.inject(this);
} }
@Override
int getInfoText() {
return R.string.sharing_status_forum;
}
@Override @Override
@DatabaseExecutor @DatabaseExecutor
protected Collection<Contact> getSharedWith() throws DbException { protected Collection<Contact> getSharedWith() throws DbException {

View File

@@ -3,8 +3,10 @@ package org.briarproject.briar.android.sharing;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.TextView;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
@@ -45,7 +47,7 @@ abstract class SharingStatusActivity extends BriarActivity {
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.list); setContentView(R.layout.activity_sharing_status);
Intent i = getIntent(); Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID); byte[] b = i.getByteArrayExtra(GROUP_ID);
@@ -57,6 +59,9 @@ abstract class SharingStatusActivity extends BriarActivity {
list.setLayoutManager(new LinearLayoutManager(this)); list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter); list.setAdapter(adapter);
list.setEmptyText(getString(R.string.nobody)); list.setEmptyText(getString(R.string.nobody));
TextView info = (TextView) findViewById(R.id.info);
info.setText(getInfoText());
} }
@Override @Override
@@ -84,6 +89,9 @@ abstract class SharingStatusActivity extends BriarActivity {
} }
} }
@StringRes
abstract int getInfoText();
@DatabaseExecutor @DatabaseExecutor
abstract protected Collection<Contact> getSharedWith() throws DbException; 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.design.widget.Snackbar.make;
import static android.support.v7.widget.RecyclerView.NO_POSITION; import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; 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 java.util.logging.Level.INFO;
import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCount; 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, implements ThreadListListener<H>, TextInputListener, SharingListener,
ThreadItemListener<I> { ThreadItemListener<I> {
protected static final String KEY_INPUT_VISIBILITY = "inputVisibility";
protected static final String KEY_REPLY_ID = "replyId"; protected static final String KEY_REPLY_ID = "replyId";
private static final Logger LOG = private static final Logger LOG =
@@ -89,7 +86,6 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
getController().setGroupId(groupId); getController().setGroupId(groupId);
textInput = (TextInputView) findViewById(R.id.text_input_container); textInput = (TextInputView) findViewById(R.id.text_input_container);
textInput.setVisibility(GONE);
textInput.setListener(this); textInput.setListener(this);
list = (BriarRecyclerView) findViewById(R.id.list); list = (BriarRecyclerView) findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this); layoutManager = new LinearLayoutManager(this);
@@ -181,8 +177,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
} else { } else {
adapter.setItems(items); adapter.setItems(items);
list.showData(); list.showData();
if (replyId != null) updateTextInput(replyId);
adapter.setHighlightedItem(replyId);
} }
} else { } else {
LOG.info("Concurrent update, reloading"); LOG.info("Concurrent update, reloading");
@@ -231,18 +226,9 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
} }
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
boolean visible = savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY);
textInput.setVisibility(visible ? VISIBLE : GONE);
}
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
boolean visible = textInput.getVisibility() == VISIBLE;
outState.putBoolean(KEY_INPUT_VISIBILITY, visible);
ThreadItem replyItem = adapter.getHighlightedItem(); ThreadItem replyItem = adapter.getHighlightedItem();
if (replyItem != null) { if (replyItem != null) {
outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes()); outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes());
@@ -262,9 +248,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (textInput.getVisibility() == VISIBLE) { if (adapter.getHighlightedItem() != null) {
textInput.setVisibility(GONE); updateTextInput(null);
adapter.setHighlightedItem(null);
} else { } else {
super.onBackPressed(); super.onBackPressed();
} }
@@ -280,7 +265,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override @Override
public void onReplyClick(final I item) { public void onReplyClick(final I item) {
showTextInput(item); updateTextInput(item.getId());
if (textInput.isKeyboardOpen()) { if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item); scrollToItemAtTop(item);
} else { } else {
@@ -330,20 +315,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
snackbar.show(); snackbar.show();
} }
protected void showTextInput(@Nullable I replyItem) { private void updateTextInput(@Nullable MessageId replyItemId) {
// An animation here would be an overkill because of the keyboard if (replyItemId != null) {
// popping up. textInput.setHint(R.string.forum_message_reply_hint);
// only clear the text when the input container was not visible textInput.requestFocus();
if (textInput.getVisibility() != VISIBLE) { textInput.showSoftKeyboard();
textInput.setVisibility(VISIBLE); } else {
textInput.setText(""); textInput.setHint(R.string.forum_new_message_hint);
} }
textInput.requestFocus(); adapter.setHighlightedItem(replyItemId);
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());
} }
@Override @Override
@@ -369,9 +349,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
}; };
getController().createAndStoreMessage(text, replyItem, handler); getController().createAndStoreMessage(text, replyItem, handler);
textInput.hideSoftKeyboard(); textInput.hideSoftKeyboard();
textInput.setVisibility(GONE);
textInput.setText(""); textInput.setText("");
adapter.setHighlightedItem(null); updateTextInput(null);
} }
protected abstract int getMaxBodyLength(); 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.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.graphics.Typeface.BOLD; import static android.graphics.Typeface.BOLD;
import static android.util.TypedValue.COMPLEX_UNIT_PX; 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.bramble.api.identity.Author.Status.OURSELVES;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; 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 REBLOGGER = 1;
public static final int COMMENTER = 2; public static final int COMMENTER = 2;
public static final int LIST = 3; 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 CircleImageView avatar;
private final ImageView avatarIcon; private final ImageView avatarIcon;
@@ -83,7 +86,13 @@ public class AuthorView extends RelativeLayout {
} }
public void setAuthorStatus(Status status) { public void setAuthorStatus(Status status) {
trustIndicator.setTrustLevel(status); if (status != NONE) {
trustIndicator.setTrustLevel(status);
trustIndicator.setVisibility(VISIBLE);
} else {
trustIndicator.setVisibility(GONE);
}
if (status == OURSELVES) { if (status == OURSELVES) {
authorName.setTypeface(authorNameTypeface, BOLD); authorName.setTypeface(authorNameTypeface, BOLD);
} else { } else {
@@ -124,10 +133,17 @@ public class AuthorView extends RelativeLayout {
setOnClickListener(null); 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) { public void setPersona(int persona) {
switch (persona) { switch (persona) {
case NORMAL: case NORMAL:
avatarIcon.setVisibility(VISIBLE); avatarIcon.setVisibility(INVISIBLE);
date.setVisibility(VISIBLE); date.setVisibility(VISIBLE);
setAvatarSize(R.dimen.blogs_avatar_normal_size); setAvatarSize(R.dimen.blogs_avatar_normal_size);
setTextSize(authorName, R.dimen.text_size_small); setTextSize(authorName, R.dimen.text_size_small);
@@ -158,6 +174,24 @@ public class AuthorView extends RelativeLayout {
setCenterVertical(authorName, true); setCenterVertical(authorName, true);
setCenterVertical(trustIndicator, true); setCenterVertical(trustIndicator, true);
break; 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 <stroke
android:width="2dp" android:width="2dp"
android:color="@color/forum_discussion_nested_line"/> android:color="@color/thread_indicator"/>
</shape> </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" android:textSize="@dimen/text_size_medium"
tools:text="This is a name of a RSS Feed"/> tools:text="This is a name of a RSS Feed"/>
<ImageView <ImageButton
android:id="@+id/deleteButton" android:id="@+id/deleteButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -3,12 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns: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 <item
android:id="@+id/action_forum_share" android:id="@+id/action_forum_share"
android:icon="@drawable/social_share_white" android:icon="@drawable/social_share_white"

View File

@@ -3,12 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns: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 <item
android:id="@+id/action_group_member_list" android:id="@+id/action_group_member_list"
android:icon="@drawable/ic_group_white" android:icon="@drawable/ic_group_white"

View File

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

View File

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

View File

@@ -162,7 +162,6 @@
<string name="groups_create_group_invitation_button">Send Invitation</string> <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_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_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_message_sent">Message sent</string>
<string name="groups_member_list">Member List</string> <string name="groups_member_list">Member List</string>
<string name="groups_invite_members">Invite Members</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_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_accepted_received">%s accepted the group invitation.</string>
<string name="groups_invitations_response_declined_received">%s declined 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 --> <!-- Private Groups Revealing Contacts -->
<string name="groups_reveal_contacts">Reveal Contacts</string> <string name="groups_reveal_contacts">Reveal Contacts</string>
@@ -215,15 +215,10 @@
<item quantity="one">%d post</item> <item quantity="one">%d post</item>
<item quantity="other">%d posts</item> <item quantity="other">%d posts</item>
</plurals> </plurals>
<string name="forum_compose_post">New Forum Post</string>
<string name="forum_new_entry_posted">Forum entry posted</string> <string name="forum_new_entry_posted">Forum entry posted</string>
<string name="forum_new_message_hint">New Entry</string> <string name="forum_new_message_hint">New Entry</string>
<string name="forum_message_reply_hint">New Reply</string> <string name="forum_message_reply_hint">New Reply</string>
<string name="btn_reply">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="forum_leave">Leave Forum</string>
<string name="dialog_title_leave_forum">Confirm Leaving 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> <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="forum_invitation_response_declined_received">%s declined the forum invitation.</string>
<string name="sharing_status">Sharing Status</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> <string name="shared_with">Shared with %1$d (%2$d online)</string>
<plurals name="forums_shared"> <plurals name="forums_shared">
<item quantity="one">%d forum shared by contacts</item> <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_invitations_title">Blog Invitations</string>
<string name="blogs_sharing_joined_toast">Subscribed to Blog</string> <string name="blogs_sharing_joined_toast">Subscribed to Blog</string>
<string name="blogs_sharing_declined_toast">Blog Invitation Declined</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 --> <!-- RSS Feeds -->
<string name="blogs_rss_feeds_import">Import RSS Feed</string> <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_imported">Imported:</string>
<string name="blogs_rss_feeds_manage_author">Author:</string> <string name="blogs_rss_feeds_manage_author">Author:</string>
<string name="blogs_rss_feeds_manage_updated">Last Updated:</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_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_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> <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"> <style name="DiscussionLevelIndicator">
<item name="android:layout_marginLeft">4dp</item> <item name="android:layout_marginLeft">4dp</item>
<item name="android:background">@color/divider</item> <item name="android:background">@color/thread_indicator</item>
</style> </style>
<style name="BriarTabLayout" parent="Widget.Design.TabLayout"> <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 { public class Blog extends BaseGroup implements Shareable {
private final Author author; private final Author author;
private final boolean rssFeed;
public Blog(Group group, Author author) { public Blog(Group group, Author author, boolean rssFeed) {
super(group); super(group);
this.author = author; this.author = author;
this.rssFeed = rssFeed;
} }
public Author getAuthor() { public Author getAuthor() {
return author; return author;
} }
public boolean isRssFeed() {
return rssFeed;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
return o instanceof Blog && super.equals(o); return o instanceof Blog && super.equals(o);

View File

@@ -26,7 +26,7 @@ public class BlogCommentHeader extends BlogPostHeader {
Status authorStatus, boolean read) { Status authorStatus, boolean read) {
super(type, groupId, id, parent.getId(), timestamp, super(type, groupId, id, parent.getId(), timestamp,
timeReceived, author, authorStatus, read); timeReceived, author, authorStatus, false, read);
if (type != COMMENT && type != WRAPPED_COMMENT) if (type != COMMENT && type != WRAPPED_COMMENT)
throw new IllegalArgumentException("Incompatible Message Type"); throw new IllegalArgumentException("Incompatible Message Type");
@@ -43,4 +43,11 @@ public class BlogCommentHeader extends BlogPostHeader {
public BlogPostHeader getParent() { public BlogPostHeader getParent() {
return parent; 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_AUTHOR_NAME = "name";
String KEY_PUBLIC_KEY = "publicKey"; String KEY_PUBLIC_KEY = "publicKey";
String KEY_AUTHOR = "author"; String KEY_AUTHOR = "author";
String KEY_RSS_FEED = "rssFeed";
String KEY_READ = "read"; String KEY_READ = "read";
String KEY_COMMENT = "comment"; String KEY_COMMENT = "comment";
String KEY_ORIGINAL_MSG_ID = "originalMessageId"; String KEY_ORIGINAL_MSG_ID = "originalMessageId";

View File

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

View File

@@ -41,6 +41,11 @@ public interface BlogManager {
*/ */
void removeBlog(Blog b) throws DbException; 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. * Stores a local blog post.
*/ */

View File

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

View File

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

View File

@@ -1,40 +1,31 @@
package org.briarproject.briar.api.feed; package org.briarproject.briar.api.feed;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.blog.Blog;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; 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 @Immutable
@NotNullByDefault @NotNullByDefault
public class Feed { public class Feed {
private final String url; private final String url;
private final GroupId blogId; private final Blog blog;
private final LocalAuthor localAuthor;
@Nullable @Nullable
private final String title, description, author; private final String description, author;
private final long added, updated, lastEntryTime; private final long added, updated, lastEntryTime;
public Feed(String url, GroupId blogId, @Nullable String title, public Feed(String url, Blog blog, LocalAuthor localAuthor,
@Nullable String description, @Nullable String author, @Nullable String description, @Nullable String author, long added,
long added, long updated, long lastEntryTime) { long updated, long lastEntryTime) {
this.url = url; this.url = url;
this.blogId = blogId; this.blog = blog;
this.title = title; this.localAuthor = localAuthor;
this.description = description; this.description = description;
this.author = author; this.author = author;
this.added = added; this.added = added;
@@ -42,13 +33,13 @@ public class Feed {
this.lastEntryTime = lastEntryTime; 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) { @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) { public Feed(String url, Blog blog, LocalAuthor localAuthor, long added) {
this(url, blogId, null, null, null, added, 0L, 0L); this(url, blog, localAuthor, null, null, added, 0L, 0L);
} }
public String getUrl() { public String getUrl() {
@@ -56,39 +47,19 @@ public class Feed {
} }
public GroupId getBlogId() { public GroupId getBlogId() {
return blogId; return blog.getId();
} }
public BdfDictionary toBdfDictionary() { public Blog getBlog() {
BdfDictionary d = BdfDictionary.of( return blog;
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 static Feed from(BdfDictionary d) throws FormatException { public LocalAuthor getLocalAuthor() {
String url = d.getString(KEY_FEED_URL); return localAuthor;
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);
} }
@Nullable
public String getTitle() { public String getTitle() {
return title; return blog.getName();
} }
@Nullable @Nullable
@@ -118,20 +89,9 @@ public class Feed {
if (this == o) return true; if (this == o) return true;
if (o instanceof Feed) { if (o instanceof Feed) {
Feed f = (Feed) o; Feed f = (Feed) o;
return url.equals(f.url) && blogId.equals(f.getBlogId()) && return blog.equals(f.blog);
equalsWithNull(title, f.getTitle()) &&
equalsWithNull(description, f.getDescription()) &&
equalsWithNull(author, f.getAuthor()) &&
added == f.getAdded() &&
updated == f.getUpdated() &&
lastEntryTime == f.getLastEntryTime();
} }
return false; 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 // group metadata keys
String KEY_FEEDS = "feeds"; String KEY_FEEDS = "feeds";
String KEY_FEED_URL = "feedURL"; String KEY_FEED_URL = "feedURL";
String KEY_BLOG_GROUP_ID = "blogGroupId"; String KEY_BLOG_TITLE = "blogTitle";
String KEY_FEED_TITLE = "feedTitle"; String KEY_PUBLIC_KEY = "publicKey";
String KEY_PRIVATE_KEY = "privateKey";
String KEY_FEED_DESC = "feedDesc"; String KEY_FEED_DESC = "feedDesc";
String KEY_FEED_AUTHOR = "feedAuthor"; String KEY_FEED_AUTHOR = "feedAuthor";
String KEY_FEED_ADDED = "feedAdded"; 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.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.ClientId; import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.GroupId;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@@ -17,14 +16,14 @@ public interface FeedManager {
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.feed"); 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. * Removes an RSS feed.
*/ */
void removeFeed(String url) throws DbException; void removeFeed(Feed feed) throws DbException;
/** /**
* Returns a list of all added RSS feeds * 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, GroupId groupId, int role, long time, boolean local, boolean sent,
boolean seen, boolean read) { boolean seen, boolean read) {
super(messageId, groupId, time, local, read, sent, seen); super(messageId, groupId, time, local, sent, seen, read);
this.sessionId = sessionId; this.sessionId = sessionId;
this.messageId = messageId; this.messageId = messageId;
this.role = role; this.role = role;

View File

@@ -14,7 +14,7 @@ public class PrivateMessageHeader extends BaseMessageHeader {
public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp, public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp,
boolean local, boolean read, boolean sent, boolean seen) { 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, boolean local, boolean sent, boolean seen, boolean read,
SessionId sessionId, ContactId contactId) { SessionId sessionId, ContactId contactId) {
super(id, groupId, time, local, read, sent, seen); super(id, groupId, time, local, sent, seen, read);
this.sessionId = sessionId; this.sessionId = sessionId;
this.contactId = contactId; this.contactId = contactId;
} }

View File

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

View File

@@ -14,6 +14,9 @@ import org.briarproject.briar.api.blog.BlogFactory;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; 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 @Immutable
@NotNullByDefault @NotNullByDefault
class BlogFactoryImpl implements BlogFactory { class BlogFactoryImpl implements BlogFactory {
@@ -33,28 +36,45 @@ class BlogFactoryImpl implements BlogFactory {
@Override @Override
public Blog createBlog(Author a) { 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 { try {
BdfList blog = BdfList.of( BdfList blog = BdfList.of(
a.getName(), a.getName(),
a.getPublicKey() a.getPublicKey(),
rssFeed
); );
byte[] descriptor = clientHelper.toByteArray(blog); byte[] descriptor = clientHelper.toByteArray(blog);
Group g = groupFactory Group g = groupFactory
.createGroup(BlogManagerImpl.CLIENT_ID, descriptor); .createGroup(BlogManagerImpl.CLIENT_ID, descriptor);
return new Blog(g, a); return new Blog(g, a, rssFeed);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override @Override
public Blog parseBlog(Group g) throws FormatException { public Blog parseBlog(Group group) throws FormatException {
byte[] descriptor = g.getDescriptor(); byte[] descriptor = group.getDescriptor();
// Author Name, Public Key // Author name, public key, RSS feed
BdfList blog = clientHelper.toList(descriptor); BdfList blog = clientHelper.toList(descriptor);
Author a = String name = blog.getString(0);
authorFactory.createAuthor(blog.getString(0), blog.getRaw(1)); if (name.length() > MAX_AUTHOR_NAME_LENGTH)
return new Blog(g, a); 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_PARENT_MSG_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY; 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_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_TIMESTAMP;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED; import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE; 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) private void removeBlog(Transaction txn, Blog b, boolean forced)
throws DbException { throws DbException {
if (!forced && !canBeRemoved(txn, b.getId())) if (!forced && !canBeRemoved(txn, b.getId()))
@@ -248,15 +254,18 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
@Override @Override
public void addLocalPost(Transaction txn, BlogPost p) throws DbException { public void addLocalPost(Transaction txn, BlogPost p) throws DbException {
try { try {
GroupId groupId = p.getMessage().getGroupId();
Blog b = getBlog(txn, groupId);
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
meta.put(KEY_TYPE, POST.getInt()); meta.put(KEY_TYPE, POST.getInt());
meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp()); meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp());
meta.put(KEY_AUTHOR, authorToBdfDictionary(p.getAuthor())); meta.put(KEY_AUTHOR, authorToBdfDictionary(p.getAuthor()));
meta.put(KEY_READ, true); meta.put(KEY_READ, true);
meta.put(KEY_RSS_FEED, b.isRssFeed());
clientHelper.addLocalMessage(txn, p.getMessage(), meta, true); clientHelper.addLocalMessage(txn, p.getMessage(), meta, true);
// broadcast event about new post // broadcast event about new post
GroupId groupId = p.getMessage().getGroupId();
MessageId postId = p.getMessage().getId(); MessageId postId = p.getMessage().getId();
BlogPostHeader h = BlogPostHeader h =
getPostHeaderFromMetadata(txn, groupId, postId, meta); getPostHeaderFromMetadata(txn, groupId, postId, meta);
@@ -345,6 +354,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
wMessage = blogPostFactory wMessage = blogPostFactory
.wrapPost(groupId, wDescriptor, wTimestamp, body); .wrapPost(groupId, wDescriptor, wTimestamp, body);
meta.put(KEY_TYPE, WRAPPED_POST.getInt()); meta.put(KEY_TYPE, WRAPPED_POST.getInt());
meta.put(KEY_RSS_FEED, pOriginalHeader.isRssFeed());
} else if (type == COMMENT) { } else if (type == COMMENT) {
Group wGroup = db.getGroup(txn, pOriginalHeader.getGroupId()); Group wGroup = db.getGroup(txn, pOriginalHeader.getGroupId());
byte[] wDescriptor = wGroup.getDescriptor(); byte[] wDescriptor = wGroup.getDescriptor();
@@ -363,6 +373,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
// Re-wrap wrapped post without adding another wrapping layer // Re-wrap wrapped post without adding another wrapping layer
wMessage = blogPostFactory.rewrapWrappedPost(groupId, body); wMessage = blogPostFactory.rewrapWrappedPost(groupId, body);
meta.put(KEY_TYPE, WRAPPED_POST.getInt()); meta.put(KEY_TYPE, WRAPPED_POST.getInt());
meta.put(KEY_RSS_FEED, pOriginalHeader.isRssFeed());
} else if (type == WRAPPED_COMMENT) { } else if (type == WRAPPED_COMMENT) {
BlogCommentHeader wComment = (BlogCommentHeader) pOriginalHeader; BlogCommentHeader wComment = (BlogCommentHeader) pOriginalHeader;
MessageId wrappedId = MessageId wrappedId =
@@ -593,8 +604,11 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
String name = d.getString(KEY_AUTHOR_NAME); String name = d.getString(KEY_AUTHOR_NAME);
byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY); byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY);
Author author = new Author(authorId, name, publicKey); Author author = new Author(authorId, name, publicKey);
boolean isFeedPost = meta.getBoolean(KEY_RSS_FEED, false);
Status authorStatus; Status authorStatus;
if (authorStatuses.containsKey(authorId)) { if (isFeedPost) {
authorStatus = Status.NONE;
} else if (authorStatuses.containsKey(authorId)) {
authorStatus = authorStatuses.get(authorId); authorStatus = authorStatuses.get(authorId);
} else { } else {
authorStatus = identityManager.getAuthorStatus(txn, authorId); authorStatus = identityManager.getAuthorStatus(txn, authorId);
@@ -611,7 +625,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
timestamp, timeReceived, author, authorStatus, read); timestamp, timeReceived, author, authorStatus, read);
} else { } else {
return new BlogPostHeader(type, groupId, id, timestamp, 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_PARENT_MSG_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY; 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_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_TIMESTAMP;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED; import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE; import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE;
@@ -123,6 +124,7 @@ class BlogPostValidator extends BdfMessageValidator {
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
meta.put(KEY_ORIGINAL_MSG_ID, m.getId()); meta.put(KEY_ORIGINAL_MSG_ID, m.getId());
meta.put(KEY_AUTHOR, authorToBdfDictionary(a)); meta.put(KEY_AUTHOR, authorToBdfDictionary(a));
meta.put(KEY_RSS_FEED, b.isRssFeed());
return new BdfMessageContext(meta); return new BdfMessageContext(meta);
} }
@@ -199,6 +201,7 @@ class BlogPostValidator extends BdfMessageValidator {
// Get and Validate the Wrapped Message // Get and Validate the Wrapped Message
Group wGroup = groupFactory Group wGroup = groupFactory
.createGroup(BlogManagerImpl.CLIENT_ID, descriptor); .createGroup(BlogManagerImpl.CLIENT_ID, descriptor);
Blog wBlog = blogFactory.parseBlog(wGroup);
BdfList wBodyList = BdfList.of(POST.getInt(), content, signature); BdfList wBodyList = BdfList.of(POST.getInt(), content, signature);
byte[] wBody = clientHelper.toByteArray(wBodyList); byte[] wBody = clientHelper.toByteArray(wBodyList);
Message wMessage = Message wMessage =
@@ -211,6 +214,7 @@ class BlogPostValidator extends BdfMessageValidator {
meta.put(KEY_ORIGINAL_MSG_ID, wMessage.getId()); meta.put(KEY_ORIGINAL_MSG_ID, wMessage.getId());
meta.put(KEY_TIMESTAMP, wTimestamp); meta.put(KEY_TIMESTAMP, wTimestamp);
meta.put(KEY_AUTHOR, c.getDictionary().getDictionary(KEY_AUTHOR)); meta.put(KEY_AUTHOR, c.getDictionary().getDictionary(KEY_AUTHOR));
meta.put(KEY_RSS_FEED, wBlog.isRssFeed());
return new BdfMessageContext(meta); 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.MAX_MESSAGE_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.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.MAX_QUEUE_MESSAGE_BODY_LENGTH;
import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_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); ByteUtils.writeUint64(queuePosition, raw, MESSAGE_HEADER_LENGTH);
System.arraycopy(body, 0, raw, QUEUE_MESSAGE_HEADER_LENGTH, System.arraycopy(body, 0, raw, QUEUE_MESSAGE_HEADER_LENGTH,
body.length); body.length);
byte[] timeBytes = new byte[ByteUtils.INT_64_BYTES]; byte[] timeBytes = new byte[INT_64_BYTES];
ByteUtils.writeUint64(timestamp, timeBytes, 0); 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( MessageId id = new MessageId(
crypto.hash(MessageId.LABEL, groupId.getBytes(), timeBytes, crypto.hash(MessageId.LABEL, groupId.getBytes(), timeBytes,
body)); bodyBytes));
return new QueueMessage(id, groupId, timestamp, queuePosition, raw); 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.db.Transaction;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener; 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.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -31,6 +30,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler; import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.util.StringUtils; 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.BlogManager;
import org.briarproject.briar.api.blog.BlogPost; import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogPostFactory;
@@ -39,8 +39,6 @@ import org.briarproject.briar.api.feed.FeedManager;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -75,12 +73,12 @@ import static org.briarproject.briar.util.HtmlUtils.clean;
@ThreadSafe @ThreadSafe
@NotNullByDefault @NotNullByDefault
class FeedManagerImpl implements FeedManager, Client, EventListener { class FeedManagerImpl implements FeedManager, Client, EventListener,
BlogManager.RemoveBlogHook {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(FeedManagerImpl.class.getName()); Logger.getLogger(FeedManagerImpl.class.getName());
private static final byte[] UNSPECIFIED_ADDRESS = new byte[4];
private static final int CONNECT_TIMEOUT = 60 * 1000; // Milliseconds private static final int CONNECT_TIMEOUT = 60 * 1000; // Milliseconds
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
@@ -88,31 +86,33 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
private final DatabaseComponent db; private final DatabaseComponent db;
private final ContactGroupFactory contactGroupFactory; private final ContactGroupFactory contactGroupFactory;
private final ClientHelper clientHelper; private final ClientHelper clientHelper;
private final IdentityManager identityManager;
private final BlogManager blogManager; private final BlogManager blogManager;
private final BlogPostFactory blogPostFactory; private final BlogPostFactory blogPostFactory;
private final FeedFactory feedFactory;
private final SocketFactory torSocketFactory; private final SocketFactory torSocketFactory;
private final Clock clock; private final Clock clock;
private final Dns noDnsLookups;
private final AtomicBoolean fetcherStarted = new AtomicBoolean(false); private final AtomicBoolean fetcherStarted = new AtomicBoolean(false);
@Inject @Inject
FeedManagerImpl(@Scheduler ScheduledExecutorService scheduler, FeedManagerImpl(@Scheduler ScheduledExecutorService scheduler,
@IoExecutor Executor ioExecutor, DatabaseComponent db, @IoExecutor Executor ioExecutor, DatabaseComponent db,
ContactGroupFactory contactGroupFactory, ClientHelper clientHelper, ContactGroupFactory contactGroupFactory, ClientHelper clientHelper,
IdentityManager identityManager, BlogManager blogManager, BlogManager blogManager, BlogPostFactory blogPostFactory,
BlogPostFactory blogPostFactory, SocketFactory torSocketFactory, FeedFactory feedFactory, SocketFactory torSocketFactory,
Clock clock) { Clock clock, Dns noDnsLookups) {
this.scheduler = scheduler; this.scheduler = scheduler;
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.db = db; this.db = db;
this.contactGroupFactory = contactGroupFactory; this.contactGroupFactory = contactGroupFactory;
this.clientHelper = clientHelper; this.clientHelper = clientHelper;
this.identityManager = identityManager;
this.blogManager = blogManager; this.blogManager = blogManager;
this.blogPostFactory = blogPostFactory; this.blogPostFactory = blogPostFactory;
this.feedFactory = feedFactory;
this.torSocketFactory = torSocketFactory; this.torSocketFactory = torSocketFactory;
this.clock = clock; this.clock = clock;
this.noDnsLookups = noDnsLookups;
} }
@Override @Override
@@ -158,21 +158,21 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
} }
@Override @Override
public void addFeed(String url, GroupId g) throws DbException, IOException { public void addFeed(String url) throws DbException, IOException {
LOG.info("Adding new RSS feed..."); // fetch syndication feed to get its metadata
SyndFeed f;
// TODO check for existing feed?
// fetch feed to get its metadata
Feed feed = new Feed(url, g, clock.currentTimeMillis());
try { try {
feed = fetchFeed(feed, false); f = fetchSyndFeed(url);
} catch (FeedException e) { } catch (FeedException e) {
throw new IOException(e); throw new IOException(e);
} }
// store feed Feed feed = feedFactory.createFeed(url, f);
// store feed and new blog
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
blogManager.addBlog(txn, feed.getBlog());
List<Feed> feeds = getFeeds(txn); List<Feed> feeds = getFeeds(txn);
feeds.add(feed); feeds.add(feed);
storeFeeds(txn, feeds); storeFeeds(txn, feeds);
@@ -181,10 +181,10 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
db.endTransaction(txn); db.endTransaction(txn);
} }
// fetch feed again, post entries this time // fetch feed again and post entries
Feed updatedFeed; Feed updatedFeed;
try { try {
updatedFeed = fetchFeed(feed, true); updatedFeed = fetchFeed(feed);
} catch (FeedException e) { } catch (FeedException e) {
throw new IOException(e); throw new IOException(e);
} }
@@ -203,27 +203,35 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
} }
@Override @Override
public void removeFeed(String url) throws DbException { public void removeFeed(Feed feed) throws DbException {
LOG.info("Removing RSS feed..."); LOG.info("Removing RSS feed...");
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
List<Feed> feeds = getFeeds(txn); // this will call removingBlog() where the feed itself gets removed
boolean found = false; blogManager.removeBlog(txn, feed.getBlog());
for (Feed feed : feeds) {
if (feed.getUrl().equals(url)) {
found = true;
feeds.remove(feed);
break;
}
}
if (!found) throw new DbException();
storeFeeds(txn, feeds);
db.commitTransaction(txn); db.commitTransaction(txn);
} finally { } finally {
db.endTransaction(txn); 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 @Override
public List<Feed> getFeeds() throws DbException { public List<Feed> getFeeds() throws DbException {
List<Feed> feeds; List<Feed> feeds;
@@ -246,7 +254,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
for (Object object : d.getList(KEY_FEEDS)) { for (Object object : d.getList(KEY_FEEDS)) {
if (!(object instanceof BdfDictionary)) if (!(object instanceof BdfDictionary))
throw new FormatException(); throw new FormatException();
feeds.add(Feed.from((BdfDictionary) object)); feeds.add(feedFactory.createFeed((BdfDictionary) object));
} }
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
@@ -259,7 +267,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
BdfList feedList = new BdfList(); BdfList feedList = new BdfList();
for (Feed feed : feeds) { for (Feed feed : feeds) {
feedList.add(feed.toBdfDictionary()); feedList.add(feedFactory.feedToBdfDictionary(feed));
} }
BdfDictionary gm = BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList)); BdfDictionary gm = BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
try { try {
@@ -300,7 +308,7 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
List<Feed> newFeeds = new ArrayList<Feed>(feeds.size()); List<Feed> newFeeds = new ArrayList<Feed>(feeds.size());
for (Feed feed : feeds) { for (Feed feed : feeds) {
try { try {
newFeeds.add(fetchFeed(feed, true)); newFeeds.add(fetchFeed(feed));
} catch (FeedException e) { } catch (FeedException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
@@ -323,49 +331,52 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
LOG.info("Done updating RSS feeds"); LOG.info("Done updating RSS feeds");
} }
private Feed fetchFeed(Feed feed, boolean post) private SyndFeed fetchSyndFeed(String url)
throws FeedException, IOException, DbException { throws FeedException, IOException {
String title, description, author; // fetch feed
long updated = clock.currentTimeMillis(); SyndFeed f = getSyndFeed(getFeedInputStream(url));
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);
if (f.getEntries().size() == 0) if (f.getEntries().size() == 0)
throw new FeedException("Feed has no entries"); 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 // sort and add new entries
if (post) { long lastEntryTime = postFeedEntries(feed, f.getEntries());
lastEntryTime = postFeedEntries(feed, f.getEntries());
} return feedFactory.createFeed(feed, f, lastEntryTime);
return new Feed(feed.getUrl(), feed.getBlogId(), title, description,
author, feed.getAdded(), updated, lastEntryTime);
} }
private InputStream getFeedInputStream(String url) throws IOException { 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 // Build HTTP Client
OkHttpClient client = new OkHttpClient.Builder() OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(torSocketFactory) .socketFactory(torSocketFactory)
.dns(noLookups) .dns(noDnsLookups) // Don't make local DNS lookups
.connectTimeout(CONNECT_TIMEOUT, MILLISECONDS) .connectTimeout(CONNECT_TIMEOUT, MILLISECONDS)
.build(); .build();
@@ -422,9 +433,8 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
// build post body // build post body
StringBuilder b = new StringBuilder(); 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())) { if (!StringUtils.isNullOrEmpty(entry.getTitle())) {
b.append("<h1>").append(entry.getTitle()).append("</h1>"); b.append("<h1>").append(entry.getTitle()).append("</h1>");
} }
@@ -457,13 +467,17 @@ class FeedManagerImpl implements FeedManager, Client, EventListener {
// get other information for post // get other information for post
GroupId groupId = feed.getBlogId(); 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()); String body = getPostBody(b.toString());
try { try {
// create and store post // create and store post
LocalAuthor author = identityManager.getLocalAuthor(txn); LocalAuthor localAuthor = feed.getLocalAuthor();
BlogPost post = blogPostFactory BlogPost post = blogPostFactory
.createBlogPost(groupId, time, null, author, body); .createBlogPost(groupId, time, null, localAuthor, body);
blogManager.addLocalPost(txn, post); blogManager.addLocalPost(txn, post);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) 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.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.feed.FeedManager; import org.briarproject.briar.api.feed.FeedManager;
import javax.inject.Inject; import javax.inject.Inject;
@@ -21,11 +22,18 @@ public class FeedModule {
@Provides @Provides
@Singleton @Singleton
FeedManager provideFeedManager(FeedManagerImpl feedManager, FeedManager provideFeedManager(FeedManagerImpl feedManager,
LifecycleManager lifecycleManager, EventBus eventBus) { LifecycleManager lifecycleManager, EventBus eventBus,
BlogManager blogManager) {
lifecycleManager.registerClient(feedManager); lifecycleManager.registerClient(feedManager);
eventBus.addListener(feedManager); eventBus.addListener(feedManager);
blogManager.registerRemoveBlogHook(feedManager);
return 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 @Override
protected GroupId validateDescriptor(BdfList descriptor) protected GroupId validateDescriptor(BdfList descriptor)
throws FormatException { throws FormatException {
checkSize(descriptor, 2); checkSize(descriptor, 3);
String name = descriptor.getString(0); String name = descriptor.getString(0);
checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH); checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
byte[] publicKey = descriptor.getRaw(1); byte[] publicKey = descriptor.getRaw(1);
checkLength(publicKey, 1, MAX_PUBLIC_KEY_LENGTH); checkLength(publicKey, 1, MAX_PUBLIC_KEY_LENGTH);
boolean rssFeed = descriptor.getBoolean(2);
Author author = authorFactory.createAuthor(name, publicKey); 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(); 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_AUTHOR_NAME;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY; 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_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_TIMESTAMP;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED; import static org.briarproject.briar.api.blog.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_TYPE; 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_TYPE, POST.getInt()),
new BdfEntry(KEY_TIMESTAMP, message.getTimestamp()), new BdfEntry(KEY_TIMESTAMP, message.getTimestamp()),
new BdfEntry(KEY_AUTHOR, authorMeta), 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() {{ context.checking(new Expectations() {{
oneOf(db).startTransaction(false); oneOf(db).startTransaction(false);
will(returnValue(txn)); will(returnValue(txn));
oneOf(db).getGroup(txn, blog1.getId());
will(returnValue(blog1.getGroup()));
oneOf(blogFactory).parseBlog(blog1.getGroup());
will(returnValue(blog1));
oneOf(clientHelper) oneOf(clientHelper)
.addLocalMessage(txn, message, meta, true); .addLocalMessage(txn, message, meta, true);
oneOf(identityManager) oneOf(identityManager)
@@ -297,7 +303,7 @@ public class BlogManagerImplTest extends BriarTestCase {
final LocalAuthor localAuthor = final LocalAuthor localAuthor =
new LocalAuthor(authorId, "Author", publicKey, privateKey, new LocalAuthor(authorId, "Author", publicKey, privateKey,
created); created);
return new Blog(group, localAuthor); return new Blog(group, localAuthor, false);
} }
private BdfDictionary authorToBdfDictionary(Author a) { private BdfDictionary authorToBdfDictionary(Author a) {

View File

@@ -1,5 +1,8 @@
package org.briarproject.briar.blog; 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.api.sync.MessageId;
import org.briarproject.bramble.test.TestDatabaseModule; import org.briarproject.bramble.test.TestDatabaseModule;
import org.briarproject.briar.api.blog.Blog; 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.assertFalse;
import static junit.framework.Assert.assertNotNull; 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.bramble.test.TestUtils.getRandomString;
import static org.briarproject.briar.api.blog.MessageType.COMMENT; import static org.briarproject.briar.api.blog.MessageType.COMMENT;
import static org.briarproject.briar.api.blog.MessageType.POST; import static org.briarproject.briar.api.blog.MessageType.POST;
@@ -32,7 +38,8 @@ public class BlogManagerIntegrationTest
extends BriarIntegrationTest<BriarIntegrationTestComponent> { extends BriarIntegrationTest<BriarIntegrationTestComponent> {
private BlogManager blogManager0, blogManager1; private BlogManager blogManager0, blogManager1;
private Blog blog0, blog1; private Blog blog0, blog1, rssBlog;
private LocalAuthor rssAuthor;
@Rule @Rule
public ExpectedException thrown = ExpectedException.none(); public ExpectedException thrown = ExpectedException.none();
@@ -44,12 +51,22 @@ public class BlogManagerIntegrationTest
author0 = identityManager0.getLocalAuthor(); author0 = identityManager0.getLocalAuthor();
author1 = identityManager1.getLocalAuthor(); author1 = identityManager1.getLocalAuthor();
rssAuthor = c0.getAuthorFactory().createLocalAuthor(
getRandomString(MAX_AUTHOR_NAME_LENGTH),
getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
getRandomBytes(123));
blogManager0 = c0.getBlogManager(); blogManager0 = c0.getBlogManager();
blogManager1 = c1.getBlogManager(); blogManager1 = c1.getBlogManager();
blog0 = blogFactory.createBlog(author0); blog0 = blogFactory.createBlog(author0);
blog1 = blogFactory.createBlog(author1); blog1 = blogFactory.createBlog(author1);
rssBlog = blogFactory.createFeedBlog(rssAuthor);
Transaction txn = db0.startTransaction(false);
blogManager0.addBlog(txn, rssBlog);
db0.commitTransaction(txn);
db0.endTransaction(txn);
} }
@Override @Override
@@ -74,11 +91,12 @@ public class BlogManagerIntegrationTest
@Test @Test
public void testPersonalBlogInitialisation() throws Exception { public void testPersonalBlogInitialisation() throws Exception {
Collection<Blog> blogs0 = blogManager0.getBlogs(); Collection<Blog> blogs0 = blogManager0.getBlogs();
assertEquals(3, blogs0.size()); assertEquals(4, blogs0.size());
Iterator<Blog> i0 = blogs0.iterator(); Iterator<Blog> i0 = blogs0.iterator();
assertEquals(author0, i0.next().getAuthor()); assertEquals(author0, i0.next().getAuthor());
assertEquals(author1, i0.next().getAuthor()); assertEquals(author1, i0.next().getAuthor());
assertEquals(author2, i0.next().getAuthor()); assertEquals(author2, i0.next().getAuthor());
assertEquals(rssAuthor, i0.next().getAuthor());
Collection<Blog> blogs1 = blogManager1.getBlogs(); Collection<Blog> blogs1 = blogManager1.getBlogs();
assertEquals(2, blogs1.size()); assertEquals(2, blogs1.size());
@@ -95,11 +113,14 @@ public class BlogManagerIntegrationTest
assertEquals(blog0, blogManager1.getBlog(blog0.getId())); assertEquals(blog0, blogManager1.getBlog(blog0.getId()));
assertEquals(blog1, blogManager0.getBlog(blog1.getId())); assertEquals(blog1, blogManager0.getBlog(blog1.getId()));
assertEquals(blog1, blogManager1.getBlog(blog1.getId())); assertEquals(blog1, blogManager1.getBlog(blog1.getId()));
assertEquals(rssBlog, blogManager0.getBlog(rssBlog.getId()));
assertEquals(1, blogManager0.getBlogs(author0).size()); assertEquals(1, blogManager0.getBlogs(author0).size());
assertEquals(1, blogManager1.getBlogs(author0).size()); assertEquals(1, blogManager1.getBlogs(author0).size());
assertEquals(1, blogManager0.getBlogs(author1).size()); assertEquals(1, blogManager0.getBlogs(author1).size());
assertEquals(1, blogManager1.getBlogs(author1).size()); assertEquals(1, blogManager1.getBlogs(author1).size());
assertEquals(1, blogManager0.getBlogs(rssAuthor).size());
assertEquals(0, blogManager1.getBlogs(rssAuthor).size());
} }
@Test @Test
@@ -279,9 +300,8 @@ public class BlogManagerIntegrationTest
assertEquals(POST, headers1.iterator().next().getType()); assertEquals(POST, headers1.iterator().next().getType());
// 1 reblogs that blog post // 1 reblogs that blog post
blogManager1 blogManager1.addLocalComment(author1, blog1.getId(), null,
.addLocalComment(author1, blog1.getId(), null, headers1.iterator().next());
headers1.iterator().next());
// sync comment over // sync comment over
sync1To0(2, true); sync1To0(2, true);
@@ -304,8 +324,7 @@ public class BlogManagerIntegrationTest
sync0To1(3, true); sync0To1(3, true);
// check that comment arrived // check that comment arrived
headers1 = headers1 = blogManager1.getPostHeaders(blog0.getId());
blogManager1.getPostHeaders(blog0.getId());
assertEquals(2, headers1.size()); assertEquals(2, headers1.size());
// get header of comment // get header of comment
@@ -393,4 +412,63 @@ public class BlogManagerIntegrationTest
assertEquals(2, headers0.size()); 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_PARENT_MSG_ID;
import static org.briarproject.briar.api.blog.BlogConstants.KEY_PUBLIC_KEY; 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_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_COMMENT;
import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_POST; import static org.briarproject.briar.api.blog.BlogPostFactory.SIGNING_LABEL_POST;
import static org.briarproject.briar.api.blog.MessageType.COMMENT; import static org.briarproject.briar.api.blog.MessageType.COMMENT;
@@ -50,7 +51,7 @@ import static org.junit.Assert.assertFalse;
public class BlogPostValidatorTest extends BriarTestCase { public class BlogPostValidatorTest extends BriarTestCase {
private final Mockery context = new Mockery(); private final Mockery context = new Mockery();
private final Blog blog; private final Blog blog, rssBlog;
private final BdfDictionary authorDict; private final BdfDictionary authorDict;
private final ClientId clientId; private final ClientId clientId;
private final byte[] descriptor; private final byte[] descriptor;
@@ -79,7 +80,8 @@ public class BlogPostValidatorTest extends BriarTestCase {
new BdfEntry(KEY_AUTHOR_NAME, author.getName()), new BdfEntry(KEY_AUTHOR_NAME, author.getName()),
new BdfEntry(KEY_PUBLIC_KEY, author.getPublicKey()) 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()); MessageId messageId = new MessageId(TestUtils.getRandomId());
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
@@ -97,17 +99,28 @@ public class BlogPostValidatorTest extends BriarTestCase {
@Test @Test
public void testValidateProperBlogPost() public void testValidateProperBlogPost()
throws IOException, GeneralSecurityException { 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); final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m = BdfList.of(POST.getInt(), body, sigBytes); BdfList m = BdfList.of(POST.getInt(), body, sigBytes);
BdfList signed = BdfList signed = BdfList.of(b.getId(), message.getTimestamp(), body);
BdfList.of(blog.getId(), message.getTimestamp(), body); expectCrypto(b, SIGNING_LABEL_POST, signed, sigBytes);
expectCrypto(SIGNING_LABEL_POST, signed, sigBytes);
final BdfDictionary result = final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary(); validator.validateMessage(message, group, m).getDictionary();
assertEquals(authorDict, result.getDictionary(KEY_AUTHOR)); assertEquals(authorDict, result.getDictionary(KEY_AUTHOR));
assertFalse(result.getBoolean(KEY_READ)); assertFalse(result.getBoolean(KEY_READ));
assertEquals(rssFeed, result.getBoolean(KEY_RSS_FEED));
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
@@ -137,14 +150,12 @@ public class BlogPostValidatorTest extends BriarTestCase {
MessageId pOriginalId = new MessageId(TestUtils.getRandomId()); MessageId pOriginalId = new MessageId(TestUtils.getRandomId());
MessageId currentId = new MessageId(TestUtils.getRandomId()); MessageId currentId = new MessageId(TestUtils.getRandomId());
final byte[] sigBytes = TestUtils.getRandomBytes(42); final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m = BdfList m = BdfList.of(COMMENT.getInt(), comment, pOriginalId,
BdfList.of(COMMENT.getInt(), comment, pOriginalId, currentId, currentId, sigBytes);
sigBytes);
BdfList signed = BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
BdfList.of(blog.getId(), message.getTimestamp(), comment, comment, pOriginalId, currentId);
pOriginalId, currentId); expectCrypto(blog, SIGNING_LABEL_COMMENT, signed, sigBytes);
expectCrypto(SIGNING_LABEL_COMMENT, signed, sigBytes);
final BdfDictionary result = final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary(); validator.validateMessage(message, group, m).getDictionary();
@@ -164,14 +175,12 @@ public class BlogPostValidatorTest extends BriarTestCase {
MessageId originalId = new MessageId(TestUtils.getRandomId()); MessageId originalId = new MessageId(TestUtils.getRandomId());
MessageId currentId = new MessageId(TestUtils.getRandomId()); MessageId currentId = new MessageId(TestUtils.getRandomId());
final byte[] sigBytes = TestUtils.getRandomBytes(42); final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m = BdfList m = BdfList.of(COMMENT.getInt(), null, originalId, currentId,
BdfList.of(COMMENT.getInt(), null, originalId, currentId, sigBytes);
sigBytes);
BdfList signed = BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(), null,
BdfList.of(blog.getId(), message.getTimestamp(), null, originalId, currentId);
originalId, currentId); expectCrypto(blog, SIGNING_LABEL_COMMENT, signed, sigBytes);
expectCrypto(SIGNING_LABEL_COMMENT, signed, sigBytes);
final BdfDictionary result = final BdfDictionary result =
validator.validateMessage(message, group, m).getDictionary(); validator.validateMessage(message, group, m).getDictionary();
@@ -182,22 +191,33 @@ public class BlogPostValidatorTest extends BriarTestCase {
@Test @Test
public void testValidateProperWrappedPost() public void testValidateProperWrappedPost()
throws IOException, GeneralSecurityException { 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 // group descriptor, timestamp, content, signature
final byte[] sigBytes = TestUtils.getRandomBytes(42); final byte[] sigBytes = TestUtils.getRandomBytes(42);
BdfList m = BdfList m = BdfList.of(WRAPPED_POST.getInt(), descriptor,
BdfList.of(WRAPPED_POST.getInt(), descriptor, message.getTimestamp(), body, sigBytes);
message.getTimestamp(), body, sigBytes);
BdfList signed = BdfList signed = BdfList.of(b.getId(), message.getTimestamp(), body);
BdfList.of(blog.getId(), message.getTimestamp(), body); expectCrypto(b, SIGNING_LABEL_POST, signed, sigBytes);
expectCrypto(SIGNING_LABEL_POST, signed, sigBytes);
final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes); final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
final byte[] originalBody = TestUtils.getRandomBytes(42); final byte[] originalBody = TestUtils.getRandomBytes(42);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(groupFactory).createGroup(clientId, descriptor); 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); oneOf(clientHelper).toByteArray(originalList);
will(returnValue(originalBody)); will(returnValue(originalBody));
oneOf(messageFactory) oneOf(messageFactory)
@@ -210,6 +230,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
validator.validateMessage(message, group, m).getDictionary(); validator.validateMessage(message, group, m).getDictionary();
assertEquals(authorDict, result.getDictionary(KEY_AUTHOR)); assertEquals(authorDict, result.getDictionary(KEY_AUTHOR));
assertEquals(rssFeed, result.getBoolean(KEY_RSS_FEED));
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
@@ -229,7 +250,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(), BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
comment, originalId, oldId); comment, originalId, oldId);
expectCrypto(SIGNING_LABEL_COMMENT, signed, sigBytes); expectCrypto(blog, SIGNING_LABEL_COMMENT, signed, sigBytes);
final BdfList originalList = BdfList.of(COMMENT.getInt(), comment, final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
originalId, oldId, sigBytes); originalId, oldId, sigBytes);
@@ -257,11 +278,12 @@ public class BlogPostValidatorTest extends BriarTestCase {
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
private void expectCrypto(final String label, final BdfList signed, private void expectCrypto(final Blog b, final String label,
final byte[] sig) throws IOException, GeneralSecurityException { final BdfList signed, final byte[] sig)
throws IOException, GeneralSecurityException {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(blogFactory).parseBlog(group); oneOf(blogFactory).parseBlog(group);
will(returnValue(blog)); will(returnValue(b));
oneOf(clientHelper) oneOf(clientHelper)
.verifySignature(label, sig, author.getPublicKey(), signed); .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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; 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.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager; import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
@@ -360,6 +361,14 @@ public class IntroductionIntegrationTest
eventWaiter.await(TIMEOUT, 1); eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response1Received); 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 // sync second response
sync2To0(1, true); sync2To0(1, true);
eventWaiter.await(TIMEOUT, 1); eventWaiter.await(TIMEOUT, 1);
@@ -836,14 +845,32 @@ public class IntroductionIntegrationTest
} }
private void assertDefaultUiMessages() throws DbException { private void assertDefaultUiMessages() throws DbException {
assertEquals(2, introductionManager0.getIntroductionMessages( Collection<IntroductionMessage> messages =
contactId1From0).size()); introductionManager0.getIntroductionMessages(contactId1From0);
assertEquals(2, introductionManager0.getIntroductionMessages( assertEquals(2, messages.size());
contactId2From0).size()); assertMessagesAreAcked(messages);
assertEquals(2, introductionManager1.getIntroductionMessages(
contactId0From1).size()); messages = introductionManager0.getIntroductionMessages(
assertEquals(2, introductionManager2.getIntroductionMessages( contactId2From0);
contactId0From2).size()); 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) { private void addListeners(boolean accept1, boolean accept2) {

View File

@@ -23,8 +23,8 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
private final byte[] publicKey = private final byte[] publicKey =
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH); TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
private final Author author = new Author(authorId, authorName, publicKey); private final Author author = new Author(authorId, authorName, publicKey);
private final Blog blog = new Blog(group, author); private final Blog blog = new Blog(group, author, false);
private final BdfList descriptor = BdfList.of(authorName, publicKey); private final BdfList descriptor = BdfList.of(authorName, publicKey, false);
private final String content = private final String content =
TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH); TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH);
@@ -64,7 +64,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testRejectsNullBlogName() throws Exception { public void testRejectsNullBlogName() throws Exception {
BdfList invalidDescriptor = BdfList.of(null, publicKey); BdfList invalidDescriptor = BdfList.of(null, publicKey, false);
v.validateMessage(message, group, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -72,7 +72,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testRejectsNonStringBlogName() throws Exception { public void testRejectsNonStringBlogName() throws Exception {
BdfList invalidDescriptor = BdfList.of(123, publicKey); BdfList invalidDescriptor = BdfList.of(123, publicKey, false);
v.validateMessage(message, group, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -80,7 +80,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testRejectsTooShortBlogName() throws Exception { public void testRejectsTooShortBlogName() throws Exception {
BdfList invalidDescriptor = BdfList.of("", publicKey); BdfList invalidDescriptor = BdfList.of("", publicKey, false);
v.validateMessage(message, group, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -89,7 +89,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test @Test
public void testAcceptsMinLengthBlogName() throws Exception { public void testAcceptsMinLengthBlogName() throws Exception {
String shortBlogName = TestUtils.getRandomString(1); String shortBlogName = TestUtils.getRandomString(1);
BdfList validDescriptor = BdfList.of(shortBlogName, publicKey); BdfList validDescriptor = BdfList.of(shortBlogName, publicKey, false);
expectCreateBlog(shortBlogName, publicKey); expectCreateBlog(shortBlogName, publicKey);
expectEncodeMetadata(INVITE); expectEncodeMetadata(INVITE);
BdfMessageContext messageContext = v.validateMessage(message, group, BdfMessageContext messageContext = v.validateMessage(message, group,
@@ -102,7 +102,8 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
public void testRejectsTooLongBlogName() throws Exception { public void testRejectsTooLongBlogName() throws Exception {
String invalidBlogName = String invalidBlogName =
TestUtils.getRandomString(MAX_BLOG_NAME_LENGTH + 1); TestUtils.getRandomString(MAX_BLOG_NAME_LENGTH + 1);
BdfList invalidDescriptor = BdfList.of(invalidBlogName, publicKey); BdfList invalidDescriptor =
BdfList.of(invalidBlogName, publicKey, false);
v.validateMessage(message, group, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -110,7 +111,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testRejectsNullPublicKey() throws Exception { public void testRejectsNullPublicKey() throws Exception {
BdfList invalidDescriptor = BdfList.of(authorName, null); BdfList invalidDescriptor = BdfList.of(authorName, null, false);
v.validateMessage(message, group, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -118,7 +119,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testRejectsNonRawPublicKey() throws Exception { public void testRejectsNonRawPublicKey() throws Exception {
BdfList invalidDescriptor = BdfList.of(authorName, 123); BdfList invalidDescriptor = BdfList.of(authorName, 123, false);
v.validateMessage(message, group, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -127,7 +128,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testRejectsTooLongPublicKey() throws Exception { public void testRejectsTooLongPublicKey() throws Exception {
byte[] invalidKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1); 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, v.validateMessage(message, group,
BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor, BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
null)); null));
@@ -136,7 +137,7 @@ public class BlogSharingValidatorTest extends SharingValidatorTest {
@Test @Test
public void testAcceptsMinLengthPublicKey() throws Exception { public void testAcceptsMinLengthPublicKey() throws Exception {
byte[] key = TestUtils.getRandomBytes(1); byte[] key = TestUtils.getRandomBytes(1);
BdfList validDescriptor = BdfList.of(authorName, key); BdfList validDescriptor = BdfList.of(authorName, key, false);
expectCreateBlog(authorName, key); expectCreateBlog(authorName, key);
expectEncodeMetadata(INVITE); 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.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.event.EventBus; 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.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.properties.TransportPropertyManager; import org.briarproject.bramble.api.properties.TransportPropertyManager;
@@ -136,4 +137,5 @@ public interface BriarIntegrationTestComponent {
TransportPropertyManager getTransportPropertyManager(); 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;
}
}