mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Compare commits
94 Commits
beta-0.16.
...
beta-0.16.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f50bbdaf | ||
|
|
3eed7df1a4 | ||
|
|
f7af0dc3b0 | ||
|
|
fbaf446570 | ||
|
|
fb6d962131 | ||
|
|
d007de48ac | ||
|
|
95a08eed5c | ||
|
|
040894b205 | ||
|
|
41d3bd4f19 | ||
|
|
347868684c | ||
|
|
1038a3532b | ||
|
|
4e6d514a0d | ||
|
|
f178ce807f | ||
|
|
a2c827ef24 | ||
|
|
9496148182 | ||
|
|
bb27ca186a | ||
|
|
be38431e03 | ||
|
|
e314b39661 | ||
|
|
4aa8d0b6c0 | ||
|
|
6220a8c00e | ||
|
|
dcd9b0a637 | ||
|
|
94b17caf0f | ||
|
|
fce8d9fa9f | ||
|
|
f4c798a2da | ||
|
|
accef2e51b | ||
|
|
34b4c35f44 | ||
|
|
9b253fc965 | ||
|
|
4d97cad842 | ||
|
|
ba99f58559 | ||
|
|
edbb0a3c13 | ||
|
|
fdbcc0736c | ||
|
|
f4722b2a67 | ||
|
|
d316e126a9 | ||
|
|
20bd72844c | ||
|
|
02c88eb907 | ||
|
|
1afc0d4fda | ||
|
|
5a7f39df4d | ||
|
|
e30b190209 | ||
|
|
31d35a7dd8 | ||
|
|
53f85d4b71 | ||
|
|
54b0bb6084 | ||
|
|
f2cfca1460 | ||
|
|
0cbdc47649 | ||
|
|
536853343e | ||
|
|
93de06ed0c | ||
|
|
d7f5da305a | ||
|
|
7c48bc5a00 | ||
|
|
9493e242cc | ||
|
|
3e28323ab1 | ||
|
|
c7e496230b | ||
|
|
7f8e96a654 | ||
|
|
84e040605b | ||
|
|
b0aa1517e5 | ||
|
|
2ac9f567dc | ||
|
|
792cfd7d6f | ||
|
|
2112d4fa7d | ||
|
|
31ca04e070 | ||
|
|
9693a5cb93 | ||
|
|
82266345ae | ||
|
|
0942fe6053 | ||
|
|
4a1f58705d | ||
|
|
cfe0d9a656 | ||
|
|
3cf61e7b3d | ||
|
|
7bb7f8ad5b | ||
|
|
fc50bb1c6c | ||
|
|
19be4d6edf | ||
|
|
b2e4de91a4 | ||
|
|
9b184fe1d9 | ||
|
|
f4ddc01641 | ||
|
|
08b63201d9 | ||
|
|
1c41181f1c | ||
|
|
246b330b36 | ||
|
|
fd3e74cefc | ||
|
|
ef12191ec8 | ||
|
|
a9fc310762 | ||
|
|
0a70c2d44d | ||
|
|
af1fc6f095 | ||
|
|
21956f2627 | ||
|
|
55db6e524a | ||
|
|
dac3de24e7 | ||
|
|
f93f41893e | ||
|
|
7dacb43e01 | ||
|
|
6a962bad24 | ||
|
|
489c0154e9 | ||
|
|
85dc99da72 | ||
|
|
ec808fd9f7 | ||
|
|
4c661cd4bb | ||
|
|
6324fb72a5 | ||
|
|
d3aebc4aba | ||
|
|
65c0e110c5 | ||
|
|
67aeb40d34 | ||
|
|
8280b2e3b8 | ||
|
|
4e0b9145c1 | ||
|
|
0ad4f2f39b |
@@ -12,8 +12,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 26
|
||||
versionCode 1618
|
||||
versionName "0.16.18"
|
||||
versionCode 1620
|
||||
versionName "0.16.20"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.briarproject.bramble.api.reporting.DevReporter;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.api.system.LocationUtils;
|
||||
import org.briarproject.bramble.api.system.Scheduler;
|
||||
import org.briarproject.bramble.plugin.droidtooth.DroidtoothPluginFactory;
|
||||
import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory;
|
||||
import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
|
||||
import org.briarproject.bramble.plugin.tor.TorPluginFactory;
|
||||
|
||||
@@ -41,13 +41,14 @@ public class AndroidPluginModule {
|
||||
Application app, LocationUtils locationUtils, DevReporter reporter,
|
||||
EventBus eventBus) {
|
||||
Context appContext = app.getApplicationContext();
|
||||
DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor,
|
||||
androidExecutor, appContext, random, eventBus, backoffFactory);
|
||||
DuplexPluginFactory bluetooth =
|
||||
new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor,
|
||||
appContext, random, eventBus, backoffFactory);
|
||||
DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, scheduler,
|
||||
appContext, locationUtils, reporter, eventBus,
|
||||
torSocketFactory, backoffFactory);
|
||||
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
|
||||
backoffFactory, appContext);
|
||||
scheduler, backoffFactory, appContext);
|
||||
Collection<DuplexPluginFactory> duplex =
|
||||
Arrays.asList(bluetooth, tor, lan);
|
||||
@NotNullByDefault
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.PluginException;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AndroidBluetoothPlugin.class.getName());
|
||||
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
|
||||
private volatile boolean wasEnabledByUs = false;
|
||||
private volatile BluetoothStateReceiver receiver = null;
|
||||
|
||||
// Non-null if the plugin started successfully
|
||||
private volatile BluetoothAdapter adapter = null;
|
||||
|
||||
AndroidBluetoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
|
||||
Context appContext, SecureRandom secureRandom, Backoff backoff,
|
||||
DuplexPluginCallback callback, int maxLatency) {
|
||||
super(ioExecutor, secureRandom, backoff, callback, maxLatency);
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.appContext = appContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws PluginException {
|
||||
super.start();
|
||||
// Listen for changes to the Bluetooth state
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STATE_CHANGED);
|
||||
filter.addAction(ACTION_SCAN_MODE_CHANGED);
|
||||
receiver = new BluetoothStateReceiver();
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (receiver != null) appContext.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
void initialiseAdapter() throws IOException {
|
||||
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
|
||||
// with a message queue, so submit it to the AndroidExecutor
|
||||
try {
|
||||
adapter = androidExecutor.runOnBackgroundThread(
|
||||
BluetoothAdapter::getDefaultAdapter).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
if (adapter == null)
|
||||
throw new IOException("Bluetooth is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isAdapterEnabled() {
|
||||
return adapter != null && adapter.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
void enableAdapter() {
|
||||
if (adapter != null && !adapter.isEnabled()) {
|
||||
if (adapter.enable()) {
|
||||
LOG.info("Enabling Bluetooth");
|
||||
wasEnabledByUs = true;
|
||||
} else {
|
||||
LOG.info("Could not enable Bluetooth");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void disableAdapterIfEnabledByUs() {
|
||||
if (isAdapterEnabled() && wasEnabledByUs) {
|
||||
if (adapter.disable()) LOG.info("Disabling Bluetooth");
|
||||
else LOG.info("Could not disable Bluetooth");
|
||||
wasEnabledByUs = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void setEnabledByUs() {
|
||||
wasEnabledByUs = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
String getBluetoothAddress() {
|
||||
String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
|
||||
return address.isEmpty() ? null : address;
|
||||
}
|
||||
|
||||
@Override
|
||||
BluetoothServerSocket openServerSocket(String uuid) throws IOException {
|
||||
return adapter.listenUsingInsecureRfcommWithServiceRecord(
|
||||
"RFCOMM", UUID.fromString(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
void tryToClose(@Nullable BluetoothServerSocket ss) {
|
||||
try {
|
||||
if (ss != null) ss.close();
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
DuplexTransportConnection acceptConnection(BluetoothServerSocket ss)
|
||||
throws IOException {
|
||||
return wrapSocket(ss.accept());
|
||||
}
|
||||
|
||||
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
|
||||
return new AndroidBluetoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isValidAddress(String address) {
|
||||
return BluetoothAdapter.checkBluetoothAddress(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
DuplexTransportConnection connectTo(String address, String uuid)
|
||||
throws IOException {
|
||||
BluetoothDevice d = adapter.getRemoteDevice(address);
|
||||
UUID u = UUID.fromString(uuid);
|
||||
BluetoothSocket s = null;
|
||||
try {
|
||||
s = d.createInsecureRfcommSocketToServiceRecord(u);
|
||||
s.connect();
|
||||
return wrapSocket(s);
|
||||
} catch (IOException e) {
|
||||
tryToClose(s);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if (state == STATE_ON) onAdapterEnabled();
|
||||
else if (state == STATE_OFF) onAdapterDisabled();
|
||||
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
|
||||
if (scanMode == SCAN_MODE_NONE) {
|
||||
LOG.info("Scan mode: None");
|
||||
} else if (scanMode == SCAN_MODE_CONNECTABLE) {
|
||||
LOG.info("Scan mode: Connectable");
|
||||
} else if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
|
||||
LOG.info("Scan mode: Discoverable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.bramble.plugin.droidtooth;
|
||||
package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -21,7 +21,7 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class DroidtoothPluginFactory implements DuplexPluginFactory {
|
||||
public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
|
||||
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
|
||||
@@ -35,7 +35,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
|
||||
private final EventBus eventBus;
|
||||
private final BackoffFactory backoffFactory;
|
||||
|
||||
public DroidtoothPluginFactory(Executor ioExecutor,
|
||||
public AndroidBluetoothPluginFactory(Executor ioExecutor,
|
||||
AndroidExecutor androidExecutor, Context appContext,
|
||||
SecureRandom secureRandom, EventBus eventBus,
|
||||
BackoffFactory backoffFactory) {
|
||||
@@ -61,7 +61,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
||||
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
||||
DroidtoothPlugin plugin = new DroidtoothPlugin(ioExecutor,
|
||||
AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin(ioExecutor,
|
||||
androidExecutor, appContext, secureRandom, backoff, callback,
|
||||
MAX_LATENCY);
|
||||
eventBus.addListener(plugin);
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.bramble.plugin.droidtooth;
|
||||
package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
|
||||
@@ -11,11 +11,12 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
@NotNullByDefault
|
||||
class DroidtoothTransportConnection extends AbstractDuplexTransportConnection {
|
||||
class AndroidBluetoothTransportConnection
|
||||
extends AbstractDuplexTransportConnection {
|
||||
|
||||
private final BluetoothSocket socket;
|
||||
|
||||
DroidtoothTransportConnection(Plugin plugin, BluetoothSocket socket) {
|
||||
AndroidBluetoothTransportConnection(Plugin plugin, BluetoothSocket socket) {
|
||||
super(plugin);
|
||||
this.socket = socket;
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
package org.briarproject.bramble.plugin.droidtooth;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
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.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.PluginException;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.plugin.event.DisableBluetoothEvent;
|
||||
import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
|
||||
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 static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class DroidtoothPlugin implements DuplexPlugin, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(DroidtoothPlugin.class.getName());
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
private final SecureRandom secureRandom;
|
||||
private final Backoff backoff;
|
||||
private final DuplexPluginCallback callback;
|
||||
private final int maxLatency;
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean wasEnabledByUs = false;
|
||||
private volatile BluetoothStateReceiver receiver = null;
|
||||
private volatile BluetoothServerSocket socket = null;
|
||||
|
||||
// Non-null if the plugin started successfully
|
||||
private volatile BluetoothAdapter adapter = null;
|
||||
|
||||
DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
|
||||
Context appContext, SecureRandom secureRandom, Backoff backoff,
|
||||
DuplexPluginCallback callback, int maxLatency) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.appContext = appContext;
|
||||
this.secureRandom = secureRandom;
|
||||
this.backoff = backoff;
|
||||
this.callback = callback;
|
||||
this.maxLatency = maxLatency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxIdleTime() {
|
||||
// Bluetooth detects dead connections so we don't need keepalives
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws PluginException {
|
||||
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
|
||||
// with a message queue, so submit it to the AndroidExecutor
|
||||
try {
|
||||
adapter = androidExecutor.runOnBackgroundThread(
|
||||
BluetoothAdapter::getDefaultAdapter).get();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.warning("Interrupted while getting BluetoothAdapter");
|
||||
throw new PluginException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
if (adapter == null) {
|
||||
LOG.info("Bluetooth is not supported");
|
||||
throw new PluginException();
|
||||
}
|
||||
running = true;
|
||||
// Listen for changes to the Bluetooth state
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STATE_CHANGED);
|
||||
filter.addAction(ACTION_SCAN_MODE_CHANGED);
|
||||
receiver = new BluetoothStateReceiver();
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
// If Bluetooth is enabled, bind a socket
|
||||
if (adapter.isEnabled()) {
|
||||
bind();
|
||||
} else {
|
||||
// Enable Bluetooth if settings allow
|
||||
if (callback.getSettings().getBoolean(PREF_BT_ENABLE, false)) {
|
||||
enableAdapter();
|
||||
} else {
|
||||
LOG.info("Not enabling Bluetooth");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
ioExecutor.execute(() -> {
|
||||
if (!isRunning()) return;
|
||||
String address = AndroidUtils.getBluetoothAddress(appContext,
|
||||
adapter);
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Local address " + scrubMacAddress(address));
|
||||
if (!StringUtils.isNullOrEmpty(address)) {
|
||||
// Advertise the Bluetooth address to contacts
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put(PROP_ADDRESS, address);
|
||||
callback.mergeLocalProperties(p);
|
||||
}
|
||||
// Bind a server socket to accept connections from contacts
|
||||
BluetoothServerSocket ss;
|
||||
try {
|
||||
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
|
||||
"RFCOMM", getUuid());
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return;
|
||||
}
|
||||
if (!isRunning()) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
LOG.info("Socket bound");
|
||||
socket = ss;
|
||||
backoff.reset();
|
||||
callback.transportEnabled();
|
||||
acceptContactConnections();
|
||||
});
|
||||
}
|
||||
|
||||
private UUID getUuid() {
|
||||
String uuid = callback.getLocalProperties().get(PROP_UUID);
|
||||
if (uuid == null) {
|
||||
byte[] random = new byte[UUID_BYTES];
|
||||
secureRandom.nextBytes(random);
|
||||
uuid = UUID.nameUUIDFromBytes(random).toString();
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put(PROP_UUID, uuid);
|
||||
callback.mergeLocalProperties(p);
|
||||
}
|
||||
return UUID.fromString(uuid);
|
||||
}
|
||||
|
||||
private void tryToClose(@Nullable BluetoothServerSocket ss) {
|
||||
try {
|
||||
if (ss != null) ss.close();
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} finally {
|
||||
callback.transportDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptContactConnections() {
|
||||
while (isRunning()) {
|
||||
BluetoothSocket s;
|
||||
try {
|
||||
s = socket.accept();
|
||||
} catch (IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
return;
|
||||
}
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
String address = s.getRemoteDevice().getAddress();
|
||||
LOG.info("Connection from " + scrubMacAddress(address));
|
||||
}
|
||||
backoff.reset();
|
||||
callback.incomingConnectionCreated(wrapSocket(s));
|
||||
}
|
||||
}
|
||||
|
||||
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
private void enableAdapter() {
|
||||
if (adapter != null && !adapter.isEnabled()) {
|
||||
if (adapter.enable()) {
|
||||
LOG.info("Enabling Bluetooth");
|
||||
wasEnabledByUs = true;
|
||||
} else {
|
||||
LOG.info("Could not enable Bluetooth");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
if (receiver != null) appContext.unregisterReceiver(receiver);
|
||||
tryToClose(socket);
|
||||
disableAdapter();
|
||||
}
|
||||
|
||||
private void disableAdapter() {
|
||||
if (adapter != null && adapter.isEnabled() && wasEnabledByUs) {
|
||||
if (adapter.disable()) LOG.info("Disabling Bluetooth");
|
||||
else LOG.info("Could not disable Bluetooth");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running && adapter != null && adapter.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldPoll() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPollingInterval() {
|
||||
return backoff.getPollingInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void poll(Collection<ContactId> connected) {
|
||||
if (!isRunning()) return;
|
||||
backoff.increment();
|
||||
// Try to connect to known devices in parallel
|
||||
Map<ContactId, TransportProperties> remote =
|
||||
callback.getRemoteProperties();
|
||||
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
if (connected.contains(c)) continue;
|
||||
String address = e.getValue().get(PROP_ADDRESS);
|
||||
if (StringUtils.isNullOrEmpty(address)) continue;
|
||||
String uuid = e.getValue().get(PROP_UUID);
|
||||
if (StringUtils.isNullOrEmpty(uuid)) continue;
|
||||
ioExecutor.execute(() -> {
|
||||
if (!running) return;
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if (s != null) {
|
||||
backoff.reset();
|
||||
callback.outgoingConnectionCreated(c, wrapSocket(s));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BluetoothSocket connect(String address, String uuid) {
|
||||
// Validate the address
|
||||
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
// not scrubbing here to be able to figure out the problem
|
||||
LOG.warning("Invalid address " + address);
|
||||
return null;
|
||||
}
|
||||
// Validate the UUID
|
||||
UUID u;
|
||||
try {
|
||||
u = UUID.fromString(uuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
||||
return null;
|
||||
}
|
||||
// Try to connect
|
||||
BluetoothDevice d = adapter.getRemoteDevice(address);
|
||||
BluetoothSocket s = null;
|
||||
try {
|
||||
s = d.createInsecureRfcommSocketToServiceRecord(u);
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connecting to " + scrubMacAddress(address));
|
||||
s.connect();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connected to " + scrubMacAddress(address));
|
||||
return s;
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Failed to connect to " + scrubMacAddress(address)
|
||||
+ ": " + e);
|
||||
}
|
||||
tryToClose(s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(@Nullable Closeable c) {
|
||||
try {
|
||||
if (c != null) c.close();
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createConnection(ContactId c) {
|
||||
if (!isRunning()) return null;
|
||||
TransportProperties p = callback.getRemoteProperties(c);
|
||||
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);
|
||||
if (s == null) return null;
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsKeyAgreement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
|
||||
if (!isRunning()) return null;
|
||||
// There's no point listening if we can't discover our own address
|
||||
String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
|
||||
if (address.isEmpty()) return null;
|
||||
// No truncation necessary because COMMIT_LENGTH = 16
|
||||
UUID uuid = UUID.nameUUIDFromBytes(commitment);
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
||||
// Bind a server socket for receiving key agreement connections
|
||||
BluetoothServerSocket ss;
|
||||
try {
|
||||
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
|
||||
"RFCOMM", uuid);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
BdfList descriptor = new BdfList();
|
||||
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
||||
descriptor.add(StringUtils.macToBytes(address));
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof EnableBluetoothEvent) {
|
||||
enableAdapterAsync();
|
||||
} else if (e instanceof DisableBluetoothEvent) {
|
||||
disableAdapterAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void enableAdapterAsync() {
|
||||
ioExecutor.execute(this::enableAdapter);
|
||||
}
|
||||
|
||||
private void disableAdapterAsync() {
|
||||
ioExecutor.execute(this::disableAdapter);
|
||||
}
|
||||
|
||||
private class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if (state == STATE_ON) {
|
||||
LOG.info("Bluetooth enabled");
|
||||
bind();
|
||||
} else if (state == STATE_OFF) {
|
||||
LOG.info("Bluetooth disabled");
|
||||
tryToClose(socket);
|
||||
}
|
||||
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
|
||||
if (scanMode == SCAN_MODE_NONE) {
|
||||
LOG.info("Scan mode: None");
|
||||
} else if (scanMode == SCAN_MODE_CONNECTABLE) {
|
||||
LOG.info("Scan mode: Connectable");
|
||||
} else if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
|
||||
LOG.info("Scan mode: Discoverable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
||||
|
||||
private final BluetoothServerSocket ss;
|
||||
|
||||
private BluetoothKeyAgreementListener(BdfList descriptor,
|
||||
BluetoothServerSocket ss) {
|
||||
super(descriptor);
|
||||
this.ss = ss;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Callable<KeyAgreementConnection> listen() {
|
||||
return () -> {
|
||||
BluetoothSocket s = ss.accept();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(ID.getString() + ": Incoming connection");
|
||||
return new KeyAgreementConnection(
|
||||
new DroidtoothTransportConnection(
|
||||
DroidtoothPlugin.this, s), ID);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
ss.close();
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,37 +5,84 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import static android.content.Context.CONNECTIVITY_SERVICE;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
|
||||
import static android.net.ConnectivityManager.TYPE_WIFI;
|
||||
import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
@NotNullByDefault
|
||||
class AndroidLanTcpPlugin extends LanTcpPlugin {
|
||||
|
||||
// See android.net.wifi.WifiManager
|
||||
private static final String WIFI_AP_STATE_CHANGED_ACTION =
|
||||
"android.net.wifi.WIFI_AP_STATE_CHANGED";
|
||||
private static final int WIFI_AP_STATE_ENABLED = 13;
|
||||
|
||||
private static final byte[] WIFI_AP_ADDRESS_BYTES =
|
||||
{(byte) 192, (byte) 168, 43, 1};
|
||||
private static final InetAddress WIFI_AP_ADDRESS;
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AndroidLanTcpPlugin.class.getName());
|
||||
|
||||
static {
|
||||
try {
|
||||
WIFI_AP_ADDRESS = InetAddress.getByAddress(WIFI_AP_ADDRESS_BYTES);
|
||||
} catch (UnknownHostException e) {
|
||||
// Should only be thrown if the address has an illegal length
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final Context appContext;
|
||||
private final ConnectivityManager connectivityManager;
|
||||
@Nullable
|
||||
private final WifiManager wifiManager;
|
||||
|
||||
@Nullable
|
||||
private volatile BroadcastReceiver networkStateReceiver = null;
|
||||
private volatile SocketFactory socketFactory;
|
||||
|
||||
AndroidLanTcpPlugin(Executor ioExecutor, Backoff backoff,
|
||||
Context appContext, DuplexPluginCallback callback, int maxLatency,
|
||||
int maxIdleTime) {
|
||||
AndroidLanTcpPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
|
||||
Backoff backoff, Context appContext, DuplexPluginCallback callback,
|
||||
int maxLatency, int maxIdleTime) {
|
||||
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
|
||||
this.scheduler = scheduler;
|
||||
this.appContext = appContext;
|
||||
ConnectivityManager connectivityManager = (ConnectivityManager)
|
||||
appContext.getSystemService(CONNECTIVITY_SERVICE);
|
||||
if (connectivityManager == null) throw new AssertionError();
|
||||
this.connectivityManager = connectivityManager;
|
||||
wifiManager = (WifiManager) appContext.getApplicationContext()
|
||||
.getSystemService(WIFI_SERVICE);
|
||||
socketFactory = SocketFactory.getDefault();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,7 +91,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
|
||||
running = true;
|
||||
// Register to receive network status events
|
||||
networkStateReceiver = new NetworkStateReceiver();
|
||||
IntentFilter filter = new IntentFilter(CONNECTIVITY_ACTION);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(CONNECTIVITY_ACTION);
|
||||
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
|
||||
appContext.registerReceiver(networkStateReceiver, filter);
|
||||
}
|
||||
|
||||
@@ -56,21 +105,92 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
|
||||
tryToClose(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Socket createSocket() throws IOException {
|
||||
return socketFactory.createSocket();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<InetAddress> getLocalIpAddresses() {
|
||||
// If the device doesn't have wifi, don't open any sockets
|
||||
if (wifiManager == null) return emptyList();
|
||||
// If we're connected to a wifi network, use that network
|
||||
WifiInfo info = wifiManager.getConnectionInfo();
|
||||
if (info != null && info.getIpAddress() != 0)
|
||||
return singletonList(intToInetAddress(info.getIpAddress()));
|
||||
// If we're running an access point, return its address
|
||||
if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS))
|
||||
return singletonList(WIFI_AP_ADDRESS);
|
||||
// No suitable addresses
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
private InetAddress intToInetAddress(int ip) {
|
||||
byte[] ipBytes = new byte[4];
|
||||
ipBytes[0] = (byte) (ip & 0xFF);
|
||||
ipBytes[1] = (byte) ((ip >> 8) & 0xFF);
|
||||
ipBytes[2] = (byte) ((ip >> 16) & 0xFF);
|
||||
ipBytes[3] = (byte) ((ip >> 24) & 0xFF);
|
||||
try {
|
||||
return InetAddress.getByAddress(ipBytes);
|
||||
} catch (UnknownHostException e) {
|
||||
// Should only be thrown if address has illegal length
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// On API 21 and later, a socket that is not created with the wifi
|
||||
// network's socket factory may try to connect via another network
|
||||
private SocketFactory getSocketFactory() {
|
||||
if (SDK_INT < 21) return SocketFactory.getDefault();
|
||||
for (Network net : connectivityManager.getAllNetworks()) {
|
||||
NetworkInfo info = connectivityManager.getNetworkInfo(net);
|
||||
if (info != null && info.getType() == TYPE_WIFI)
|
||||
return net.getSocketFactory();
|
||||
}
|
||||
LOG.warning("Could not find suitable socket factory");
|
||||
return SocketFactory.getDefault();
|
||||
}
|
||||
|
||||
private class NetworkStateReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent i) {
|
||||
if (!running) return;
|
||||
Object o = ctx.getSystemService(CONNECTIVITY_SERVICE);
|
||||
ConnectivityManager cm = (ConnectivityManager) o;
|
||||
NetworkInfo net = cm.getActiveNetworkInfo();
|
||||
if (net != null && net.getType() == TYPE_WIFI && net.isConnected()) {
|
||||
LOG.info("Connected to Wi-Fi");
|
||||
if (socket == null || socket.isClosed()) bind();
|
||||
if (isApEnabledEvent(i)) {
|
||||
// The state change may be broadcast before the AP address is
|
||||
// visible, so delay handling the event
|
||||
scheduler.schedule(this::handleConnectivityChange, 1, SECONDS);
|
||||
} else {
|
||||
LOG.info("Not connected to Wi-Fi");
|
||||
tryToClose(socket);
|
||||
handleConnectivityChange();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnectivityChange() {
|
||||
if (!running) return;
|
||||
Collection<InetAddress> addrs = getLocalIpAddresses();
|
||||
if (addrs.contains(WIFI_AP_ADDRESS)) {
|
||||
LOG.info("Providing wifi hotspot");
|
||||
// There's no corresponding Network object and thus no way
|
||||
// to get a suitable socket factory, so we won't be able to
|
||||
// make outgoing connections on API 21+ if another network
|
||||
// has internet access
|
||||
socketFactory = SocketFactory.getDefault();
|
||||
if (socket == null || socket.isClosed()) bind();
|
||||
} else if (addrs.isEmpty()) {
|
||||
LOG.info("Not connected to wifi");
|
||||
socketFactory = SocketFactory.getDefault();
|
||||
tryToClose(socket);
|
||||
} else {
|
||||
LOG.info("Connected to wifi");
|
||||
socketFactory = getSocketFactory();
|
||||
if (socket == null || socket.isClosed()) bind();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isApEnabledEvent(Intent i) {
|
||||
return WIFI_AP_STATE_CHANGED_ACTION.equals(i.getAction()) &&
|
||||
i.getIntExtra(EXTRA_WIFI_STATE, 0) == WIFI_AP_STATE_ENABLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@@ -27,12 +28,15 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
|
||||
private static final double BACKOFF_BASE = 1.2;
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final BackoffFactory backoffFactory;
|
||||
private final Context appContext;
|
||||
|
||||
public AndroidLanTcpPluginFactory(Executor ioExecutor,
|
||||
BackoffFactory backoffFactory, Context appContext) {
|
||||
ScheduledExecutorService scheduler, BackoffFactory backoffFactory,
|
||||
Context appContext) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.scheduler = scheduler;
|
||||
this.backoffFactory = backoffFactory;
|
||||
this.appContext = appContext;
|
||||
}
|
||||
@@ -51,7 +55,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
||||
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
||||
return new AndroidLanTcpPlugin(ioExecutor, backoff, appContext,
|
||||
callback, MAX_LATENCY, MAX_IDLE_TIME);
|
||||
return new AndroidLanTcpPlugin(ioExecutor, scheduler, backoff,
|
||||
appContext, callback, MAX_LATENCY, MAX_IDLE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.os.PowerManager;
|
||||
import net.freehaven.tor.control.EventHandler;
|
||||
import net.freehaven.tor.control.TorControlConnection;
|
||||
|
||||
import org.briarproject.bramble.PoliteExecutor;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
@@ -63,8 +64,6 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@@ -111,7 +110,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(TorPlugin.class.getName());
|
||||
|
||||
private final Executor ioExecutor;
|
||||
private final Executor ioExecutor, connectionStatusExecutor;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final Context appContext;
|
||||
private final LocationUtils locationUtils;
|
||||
@@ -125,7 +124,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
private final File torDirectory, torFile, geoIpFile, configFile;
|
||||
private final File doneFile, cookieFile;
|
||||
private final PowerManager.WakeLock wakeLock;
|
||||
private final Lock connectionStatusLock;
|
||||
private final AtomicReference<Future<?>> connectivityCheck =
|
||||
new AtomicReference<>();
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
@@ -167,7 +165,9 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
// This tag will prevent Huawei's powermanager from killing us.
|
||||
wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService");
|
||||
wakeLock.setReferenceCounted(false);
|
||||
connectionStatusLock = new ReentrantLock();
|
||||
// Don't execute more than one connection status check at a time
|
||||
connectionStatusExecutor = new PoliteExecutor("TorPlugin",
|
||||
ioExecutor, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -614,7 +614,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createKeyAgreementConnection(
|
||||
byte[] commitment, BdfList descriptor, long timeout) {
|
||||
byte[] commitment, BdfList descriptor) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@@ -697,56 +697,46 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
}
|
||||
|
||||
private void updateConnectionStatus() {
|
||||
ioExecutor.execute(() -> {
|
||||
connectionStatusExecutor.execute(() -> {
|
||||
if (!running) return;
|
||||
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
|
||||
ConnectivityManager cm = (ConnectivityManager) o;
|
||||
NetworkInfo net = cm.getActiveNetworkInfo();
|
||||
boolean online = net != null && net.isConnected();
|
||||
boolean wifi = online && net.getType() == TYPE_WIFI;
|
||||
String country = locationUtils.getCurrentCountry();
|
||||
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
|
||||
country);
|
||||
Settings s = callback.getSettings();
|
||||
int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
|
||||
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Online: " + online + ", wifi: " + wifi);
|
||||
if ("".equals(country)) LOG.info("Country code unknown");
|
||||
else LOG.info("Country code: " + country);
|
||||
}
|
||||
|
||||
try {
|
||||
connectionStatusLock.lock();
|
||||
updateConnectionStatusLocked();
|
||||
} finally {
|
||||
connectionStatusLock.unlock();
|
||||
if (!online) {
|
||||
LOG.info("Disabling network, device is offline");
|
||||
enableNetwork(false);
|
||||
} else if (blocked) {
|
||||
LOG.info("Disabling network, country is blocked");
|
||||
enableNetwork(false);
|
||||
} else if (network == PREF_TOR_NETWORK_NEVER
|
||||
|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
|
||||
LOG.info("Disabling network due to data setting");
|
||||
enableNetwork(false);
|
||||
} else {
|
||||
LOG.info("Enabling network");
|
||||
enableNetwork(true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Locking: connectionStatusLock
|
||||
private void updateConnectionStatusLocked() {
|
||||
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
|
||||
ConnectivityManager cm = (ConnectivityManager) o;
|
||||
NetworkInfo net = cm.getActiveNetworkInfo();
|
||||
boolean online = net != null && net.isConnected();
|
||||
boolean wifi = online && net.getType() == TYPE_WIFI;
|
||||
String country = locationUtils.getCurrentCountry();
|
||||
boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
|
||||
country);
|
||||
Settings s = callback.getSettings();
|
||||
int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
|
||||
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Online: " + online + ", wifi: " + wifi);
|
||||
if ("".equals(country)) LOG.info("Country code unknown");
|
||||
else LOG.info("Country code: " + country);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!online) {
|
||||
LOG.info("Disabling network, device is offline");
|
||||
enableNetwork(false);
|
||||
} else if (blocked) {
|
||||
LOG.info("Disabling network, country is blocked");
|
||||
enableNetwork(false);
|
||||
} else if (network == PREF_TOR_NETWORK_NEVER
|
||||
|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
|
||||
LOG.info("Disabling network due to data setting");
|
||||
enableNetwork(false);
|
||||
} else {
|
||||
LOG.info("Enabling network");
|
||||
enableNetwork(true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleConnectionStatusUpdate() {
|
||||
Future<?> newConnectivityCheck =
|
||||
scheduler.schedule(this::updateConnectionStatus, 1, MINUTES);
|
||||
|
||||
@@ -43,7 +43,7 @@ public interface DatabaseComponent {
|
||||
* @throws DataTooOldException if the data uses an older schema than the
|
||||
* current code and cannot be migrated
|
||||
*/
|
||||
boolean open() throws DbException;
|
||||
boolean open(@Nullable MigrationListener listener) throws DbException;
|
||||
|
||||
/**
|
||||
* Waits for any open transactions to finish and closes the database.
|
||||
@@ -259,31 +259,30 @@ public interface DatabaseComponent {
|
||||
Collection<LocalAuthor> getLocalAuthors(Transaction txn) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages that need to be validated by the given
|
||||
* client.
|
||||
* Returns the IDs of any messages that need to be validated.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToValidate(Transaction txn, ClientId c)
|
||||
Collection<MessageId> getMessagesToValidate(Transaction txn)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages that are valid but pending delivery due
|
||||
* to dependencies on other messages for the given client.
|
||||
* Returns the IDs of any messages that are pending delivery due to
|
||||
* dependencies on other messages.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getPendingMessages(Transaction txn, ClientId c)
|
||||
Collection<MessageId> getPendingMessages(Transaction txn)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages from the given client
|
||||
* that have a shared dependent, but are still not shared themselves.
|
||||
* Returns the IDs of any messages that have shared dependents but have
|
||||
* not yet been shared themselves.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToShare(Transaction txn,
|
||||
ClientId c) throws DbException;
|
||||
Collection<MessageId> getMessagesToShare(Transaction txn)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the message with the given ID, in serialised form, or null if
|
||||
@@ -378,6 +377,16 @@ public interface DatabaseComponent {
|
||||
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
|
||||
throws DbException;
|
||||
|
||||
/*
|
||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||
* message is due to be sent to the given contact. The returned value may
|
||||
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE if
|
||||
* no messages are scheduled to be sent.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
long getNextSendTime(Transaction txn, ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all settings in the given namespace.
|
||||
* <p/>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.briarproject.bramble.api.db;
|
||||
|
||||
public interface MigrationListener {
|
||||
|
||||
/**
|
||||
* This is called when a migration is started while opening the database.
|
||||
* It will be called once for each migration being applied.
|
||||
*/
|
||||
void onMigrationRun();
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package org.briarproject.bramble.api.keyagreement;
|
||||
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An class for managing a particular key agreement listener.
|
||||
@@ -24,11 +24,11 @@ public abstract class KeyAgreementListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for incoming connections, and returns a Callable that
|
||||
* will return a KeyAgreementConnection when an incoming connection is
|
||||
* received.
|
||||
* Blocks until an incoming connection is received and returns it.
|
||||
*
|
||||
* @throws IOException if an error occurs or {@link #close()} is called.
|
||||
*/
|
||||
public abstract Callable<KeyAgreementConnection> listen();
|
||||
public abstract KeyAgreementConnection accept() throws IOException;
|
||||
|
||||
/**
|
||||
* Closes the underlying server socket.
|
||||
|
||||
@@ -21,7 +21,25 @@ public interface LifecycleManager {
|
||||
* The result of calling {@link #startServices(String)}.
|
||||
*/
|
||||
enum StartResult {
|
||||
ALREADY_RUNNING, DB_ERROR, SERVICE_ERROR, SUCCESS
|
||||
ALREADY_RUNNING,
|
||||
DB_ERROR,
|
||||
DATA_TOO_OLD_ERROR,
|
||||
DATA_TOO_NEW_ERROR,
|
||||
SERVICE_ERROR,
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
/**
|
||||
* The state the lifecycle can be in.
|
||||
* Returned by {@link #getLifecycleState()}
|
||||
*/
|
||||
enum LifecycleState {
|
||||
|
||||
STARTING, MIGRATING_DATABASE, STARTING_SERVICES, RUNNING, STOPPING;
|
||||
|
||||
public boolean isAfter(LifecycleState state) {
|
||||
return ordinal() > state.ordinal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,4 +89,10 @@ public interface LifecycleManager {
|
||||
* the {@link DatabaseComponent} to be closed before returning.
|
||||
*/
|
||||
void waitForShutdown() throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Returns the current state of the lifecycle.
|
||||
*/
|
||||
LifecycleState getLifecycleState();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.briarproject.bramble.api.lifecycle.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when the app enters a new lifecycle state.
|
||||
*/
|
||||
public class LifecycleEvent extends Event {
|
||||
|
||||
private final LifecycleState state;
|
||||
|
||||
public LifecycleEvent(LifecycleState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public LifecycleState getLifecycleState() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.briarproject.bramble.api.lifecycle.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
|
||||
/**
|
||||
* An event that is broadcast when the app is shutting down.
|
||||
*/
|
||||
public class ShutdownEvent extends Event {
|
||||
}
|
||||
@@ -36,9 +36,9 @@ public interface DuplexPlugin extends Plugin {
|
||||
|
||||
/**
|
||||
* Attempts to connect to the remote peer specified in the given descriptor.
|
||||
* Returns null if no connection can be established within the given time.
|
||||
* Returns null if no connection can be established.
|
||||
*/
|
||||
@Nullable
|
||||
DuplexTransportConnection createKeyAgreementConnection(
|
||||
byte[] remoteCommitment, BdfList descriptor, long timeout);
|
||||
byte[] remoteCommitment, BdfList descriptor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.briarproject.bramble.api.plugin.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event that informs the Bluetooth plugin that we have enabled the
|
||||
* Bluetooth adapter.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class BluetoothEnabledEvent extends Event {
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* An event asks the Bluetooth plugin to enable the Bluetooth adapter.
|
||||
* An event that asks the Bluetooth plugin to enable the Bluetooth adapter.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
|
||||
@@ -126,6 +126,10 @@ public class StringUtils {
|
||||
return toUtf8(s).length > maxLength;
|
||||
}
|
||||
|
||||
public static boolean isValidMac(String mac) {
|
||||
return MAC.matcher(mac).matches();
|
||||
}
|
||||
|
||||
public static byte[] macToBytes(String mac) {
|
||||
if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException();
|
||||
return fromHexString(mac.replaceAll(":", ""));
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.DataTooNewException;
|
||||
import org.briarproject.bramble.api.db.DataTooOldException;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.MigrationListener;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
@@ -45,7 +46,7 @@ interface Database<T> {
|
||||
* @throws DataTooOldException if the data uses an older schema than the
|
||||
* current code and cannot be migrated
|
||||
*/
|
||||
boolean open() throws DbException;
|
||||
boolean open(@Nullable MigrationListener listener) throws DbException;
|
||||
|
||||
/**
|
||||
* Prevents new transactions from starting, waits for all current
|
||||
@@ -96,9 +97,12 @@ interface Database<T> {
|
||||
|
||||
/**
|
||||
* Stores a message.
|
||||
*
|
||||
* @param sender the contact from whom the message was received, or null
|
||||
* if the message was created locally.
|
||||
*/
|
||||
void addMessage(T txn, Message m, State state, boolean shared)
|
||||
throws DbException;
|
||||
void addMessage(T txn, Message m, State state, boolean shared,
|
||||
@Nullable ContactId sender) throws DbException;
|
||||
|
||||
/**
|
||||
* Adds a dependency between two messages in the given group.
|
||||
@@ -111,16 +115,6 @@ interface Database<T> {
|
||||
*/
|
||||
void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Initialises the status of the given message with respect to the given
|
||||
* contact.
|
||||
*
|
||||
* @param ack whether the message needs to be acknowledged.
|
||||
* @param seen whether the contact has seen the message.
|
||||
*/
|
||||
void addStatus(T txn, ContactId c, MessageId m, boolean ack, boolean seen)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Stores a transport.
|
||||
*/
|
||||
@@ -279,7 +273,7 @@ interface Database<T> {
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<ContactId> getGroupVisibility(T txn, GroupId g)
|
||||
Map<ContactId, Boolean> getGroupVisibility(T txn, GroupId g)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
@@ -430,31 +424,37 @@ interface Database<T> {
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages that need to be validated by the given
|
||||
* client.
|
||||
* Returns the IDs of any messages that need to be validated.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToValidate(T txn, ClientId c)
|
||||
throws DbException;
|
||||
Collection<MessageId> getMessagesToValidate(T txn) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages that are still pending due to
|
||||
* dependencies to other messages for the given client.
|
||||
* Returns the IDs of any messages that are pending delivery due to
|
||||
* dependencies on other messages.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getPendingMessages(T txn, ClientId c)
|
||||
throws DbException;
|
||||
Collection<MessageId> getPendingMessages(T txn) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the IDs of any messages from the given client
|
||||
* that have a shared dependent, but are still not shared themselves.
|
||||
* Returns the IDs of any messages that have a shared dependent but have
|
||||
* not yet been shared themselves.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
Collection<MessageId> getMessagesToShare(T txn, ClientId c)
|
||||
throws DbException;
|
||||
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||
* message is due to be sent to the given contact. The returned value may
|
||||
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE
|
||||
* if no messages are scheduled to be sent.
|
||||
* <p/>
|
||||
* Read-only.
|
||||
*/
|
||||
long getNextSendTime(T txn, ContactId c) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the message with the given ID, in serialised form, or null if
|
||||
@@ -573,13 +573,6 @@ interface Database<T> {
|
||||
*/
|
||||
void removeMessage(T txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Removes an offered message that was offered by the given contact, or
|
||||
* returns false if there is no such message.
|
||||
*/
|
||||
boolean removeOfferedMessage(T txn, ContactId c, MessageId m)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Removes the given offered messages that were offered by the given
|
||||
* contact.
|
||||
@@ -587,12 +580,6 @@ interface Database<T> {
|
||||
void removeOfferedMessages(T txn, ContactId c,
|
||||
Collection<MessageId> requested) throws DbException;
|
||||
|
||||
/**
|
||||
* Removes the status of the given message with respect to the given
|
||||
* contact.
|
||||
*/
|
||||
void removeStatus(T txn, ContactId c, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Removes a transport (and all associated state) from the database.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.db.ContactExistsException;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.MigrationListener;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
||||
import org.briarproject.bramble.api.db.NoSuchLocalAuthorException;
|
||||
@@ -100,8 +101,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() throws DbException {
|
||||
boolean reopened = db.open();
|
||||
public boolean open(@Nullable MigrationListener listener)
|
||||
throws DbException {
|
||||
boolean reopened = db.open(listener);
|
||||
shutdown.addShutdownHook(() -> {
|
||||
try {
|
||||
close();
|
||||
@@ -213,7 +215,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
if (!db.containsGroup(txn, m.getGroupId()))
|
||||
throw new NoSuchGroupException();
|
||||
if (!db.containsMessage(txn, m.getId())) {
|
||||
addMessage(txn, m, DELIVERED, shared, null);
|
||||
db.addMessage(txn, m, DELIVERED, shared, null);
|
||||
transaction.attach(new MessageAddedEvent(m, null));
|
||||
transaction.attach(new MessageStateChangedEvent(m.getId(), true,
|
||||
DELIVERED));
|
||||
@@ -222,16 +224,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
db.mergeMessageMetadata(txn, m.getId(), meta);
|
||||
}
|
||||
|
||||
private void addMessage(T txn, Message m, State state, boolean shared,
|
||||
@Nullable ContactId sender) throws DbException {
|
||||
db.addMessage(txn, m, state, shared);
|
||||
for (ContactId c : db.getGroupVisibility(txn, m.getGroupId())) {
|
||||
boolean offered = db.removeOfferedMessage(txn, c, m.getId());
|
||||
boolean seen = offered || (sender != null && c.equals(sender));
|
||||
db.addStatus(txn, c, m.getId(), seen, seen);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTransport(Transaction transaction, TransportId t,
|
||||
int maxLatency) throws DbException {
|
||||
@@ -463,24 +455,24 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToValidate(Transaction transaction,
|
||||
ClientId c) throws DbException {
|
||||
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
return db.getMessagesToValidate(txn, c);
|
||||
return db.getMessagesToValidate(txn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getPendingMessages(Transaction transaction,
|
||||
ClientId c) throws DbException {
|
||||
public Collection<MessageId> getPendingMessages(Transaction transaction)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
return db.getPendingMessages(txn, c);
|
||||
return db.getPendingMessages(txn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToShare(
|
||||
Transaction transaction, ClientId c) throws DbException {
|
||||
public Collection<MessageId> getMessagesToShare(Transaction transaction)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
return db.getMessagesToShare(txn, c);
|
||||
return db.getMessagesToShare(txn);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -579,6 +571,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return db.getMessageDependents(txn, m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextSendTime(Transaction transaction, ContactId c)
|
||||
throws DbException {
|
||||
T txn = unbox(transaction);
|
||||
return db.getNextSendTime(txn, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings getSettings(Transaction transaction, String namespace)
|
||||
throws DbException {
|
||||
@@ -673,7 +672,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
db.raiseSeenFlag(txn, c, m.getId());
|
||||
db.raiseAckFlag(txn, c, m.getId());
|
||||
} else {
|
||||
addMessage(txn, m, UNKNOWN, false, c);
|
||||
db.addMessage(txn, m, UNKNOWN, false, c);
|
||||
transaction.attach(new MessageAddedEvent(m, c));
|
||||
}
|
||||
transaction.attach(new MessageToAckEvent(c));
|
||||
@@ -741,7 +740,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
GroupId id = g.getId();
|
||||
if (!db.containsGroup(txn, id))
|
||||
throw new NoSuchGroupException();
|
||||
Collection<ContactId> affected = db.getGroupVisibility(txn, id);
|
||||
Collection<ContactId> affected =
|
||||
db.getGroupVisibility(txn, id).keySet();
|
||||
db.removeGroup(txn, id);
|
||||
transaction.attach(new GroupRemovedEvent(g));
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
@@ -811,19 +811,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
throw new NoSuchGroupException();
|
||||
Visibility old = db.getGroupVisibility(txn, c, g);
|
||||
if (old == v) return;
|
||||
if (old == INVISIBLE) {
|
||||
db.addGroupVisibility(txn, c, g, v == SHARED);
|
||||
for (MessageId m : db.getMessageIds(txn, g)) {
|
||||
boolean seen = db.removeOfferedMessage(txn, c, m);
|
||||
db.addStatus(txn, c, m, seen, seen);
|
||||
}
|
||||
} else if (v == INVISIBLE) {
|
||||
db.removeGroupVisibility(txn, c, g);
|
||||
for (MessageId m : db.getMessageIds(txn, g))
|
||||
db.removeStatus(txn, c, m);
|
||||
} else {
|
||||
db.setGroupVisibility(txn, c, g, v == SHARED);
|
||||
}
|
||||
if (old == INVISIBLE) db.addGroupVisibility(txn, c, g, v == SHARED);
|
||||
else if (v == INVISIBLE) db.removeGroupVisibility(txn, c, g);
|
||||
else db.setGroupVisibility(txn, c, g, v == SHARED);
|
||||
List<ContactId> affected = Collections.singletonList(c);
|
||||
transaction.attach(new GroupVisibilityUpdatedEvent(affected));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.bramble.db;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.db.DatabaseConfig;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.MigrationListener;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
@@ -13,6 +14,7 @@ import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
@@ -40,10 +42,11 @@ class H2Database extends JdbcDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() throws DbException {
|
||||
public boolean open(@Nullable MigrationListener listener)
|
||||
throws DbException {
|
||||
boolean reopen = config.databaseExists();
|
||||
if (!reopen) config.getDatabaseDirectory().mkdirs();
|
||||
super.open("org.h2.Driver", reopen);
|
||||
super.open("org.h2.Driver", reopen, listener);
|
||||
return reopen;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.db.DataTooOldException;
|
||||
import org.briarproject.bramble.api.db.DbClosedException;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.MigrationListener;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
@@ -33,6 +34,7 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -71,7 +73,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
|
||||
abstract class JdbcDatabase implements Database<Connection> {
|
||||
|
||||
// Package access for testing
|
||||
static final int CODE_SCHEMA_VERSION = 31;
|
||||
static final int CODE_SCHEMA_VERSION = 32;
|
||||
|
||||
private static final String CREATE_SETTINGS =
|
||||
"CREATE TABLE settings"
|
||||
@@ -187,6 +189,13 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
"CREATE TABLE statuses"
|
||||
+ " (messageId HASH NOT NULL,"
|
||||
+ " contactId INT NOT NULL,"
|
||||
+ " groupId HASH NOT NULL," // Denormalised
|
||||
+ " timestamp BIGINT NOT NULL," // Denormalised
|
||||
+ " length INT NOT NULL," // Denormalised
|
||||
+ " state INT NOT NULL," // Denormalised
|
||||
+ " groupShared BOOLEAN NOT NULL," // Denormalised
|
||||
+ " messageShared BOOLEAN NOT NULL," // Denormalised
|
||||
+ " deleted BOOLEAN NOT NULL," // Denormalised
|
||||
+ " ack BOOLEAN NOT NULL,"
|
||||
+ " seen BOOLEAN NOT NULL,"
|
||||
+ " requested BOOLEAN NOT NULL,"
|
||||
@@ -198,6 +207,9 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
+ " ON DELETE CASCADE,"
|
||||
+ " FOREIGN KEY (contactId)"
|
||||
+ " REFERENCES contacts (contactId)"
|
||||
+ " ON DELETE CASCADE,"
|
||||
+ " FOREIGN KEY (groupId)"
|
||||
+ " REFERENCES groups (groupId)"
|
||||
+ " ON DELETE CASCADE)";
|
||||
|
||||
private static final String CREATE_TRANSPORTS =
|
||||
@@ -251,6 +263,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
|
||||
+ " ON messageMetadata (groupId, state)";
|
||||
|
||||
private static final String INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID =
|
||||
"CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId"
|
||||
+ " ON statuses (contactId, groupId)";
|
||||
|
||||
private static final String INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP =
|
||||
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
|
||||
+ " ON statuses (contactId, timestamp)";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(JdbcDatabase.class.getName());
|
||||
|
||||
@@ -279,7 +299,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
protected void open(String driverClass, boolean reopen) throws DbException {
|
||||
protected void open(String driverClass, boolean reopen,
|
||||
@Nullable MigrationListener listener) throws DbException {
|
||||
// Load the JDBC driver
|
||||
try {
|
||||
Class.forName(driverClass);
|
||||
@@ -290,7 +311,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
Connection txn = startTransaction();
|
||||
try {
|
||||
if (reopen) {
|
||||
checkSchemaVersion(txn);
|
||||
checkSchemaVersion(txn, listener);
|
||||
} else {
|
||||
createTables(txn);
|
||||
storeSchemaVersion(txn, CODE_SCHEMA_VERSION);
|
||||
@@ -313,7 +334,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
* @throws DataTooOldException if the data uses an older schema than the
|
||||
* current code and cannot be migrated
|
||||
*/
|
||||
private void checkSchemaVersion(Connection txn) throws DbException {
|
||||
private void checkSchemaVersion(Connection txn,
|
||||
@Nullable MigrationListener listener) throws DbException {
|
||||
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
||||
int dataSchemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1);
|
||||
if (dataSchemaVersion == -1) throw new DbException();
|
||||
@@ -326,6 +348,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
if (start == dataSchemaVersion) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Migrating from schema " + start + " to " + end);
|
||||
if (listener != null) listener.onMigrationRun();
|
||||
// Apply the migration
|
||||
m.migrate(txn);
|
||||
// Store the new schema version
|
||||
@@ -339,7 +362,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
|
||||
// Package access for testing
|
||||
List<Migration<Connection>> getMigrations() {
|
||||
return Collections.singletonList(new Migration30_31());
|
||||
return Arrays.asList(new Migration30_31(), new Migration31_32());
|
||||
}
|
||||
|
||||
private void storeSchemaVersion(Connection txn, int version)
|
||||
@@ -397,6 +420,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
|
||||
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
|
||||
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
|
||||
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
|
||||
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
|
||||
s.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(s);
|
||||
@@ -574,7 +599,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
|
||||
@Override
|
||||
public void addGroupVisibility(Connection txn, ContactId c, GroupId g,
|
||||
boolean shared) throws DbException {
|
||||
boolean groupShared) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO groupVisibilities"
|
||||
@@ -583,16 +608,50 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setBytes(2, g.getBytes());
|
||||
ps.setBoolean(3, shared);
|
||||
ps.setBoolean(3, groupShared);
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
// Create a status row for each message in the group
|
||||
addStatus(txn, c, g, groupShared);
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addStatus(Connection txn, ContactId c, GroupId g,
|
||||
boolean groupShared) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT messageId, timestamp, state, shared,"
|
||||
+ " length, raw IS NULL"
|
||||
+ " FROM messages"
|
||||
+ " WHERE groupId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, g.getBytes());
|
||||
rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
MessageId id = new MessageId(rs.getBytes(1));
|
||||
long timestamp = rs.getLong(2);
|
||||
State state = State.fromValue(rs.getInt(3));
|
||||
boolean messageShared = rs.getBoolean(4);
|
||||
int length = rs.getInt(5);
|
||||
boolean deleted = rs.getBoolean(6);
|
||||
boolean seen = removeOfferedMessage(txn, c, id);
|
||||
addStatus(txn, id, c, g, timestamp, length, state, groupShared,
|
||||
messageShared, deleted, seen);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs);
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLocalAuthor(Connection txn, LocalAuthor a)
|
||||
throws DbException {
|
||||
@@ -618,7 +677,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
|
||||
@Override
|
||||
public void addMessage(Connection txn, Message m, State state,
|
||||
boolean shared) throws DbException {
|
||||
boolean messageShared, @Nullable ContactId sender)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
|
||||
@@ -629,13 +689,24 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
ps.setBytes(2, m.getGroupId().getBytes());
|
||||
ps.setLong(3, m.getTimestamp());
|
||||
ps.setInt(4, state.getValue());
|
||||
ps.setBoolean(5, shared);
|
||||
ps.setBoolean(5, messageShared);
|
||||
byte[] raw = m.getRaw();
|
||||
ps.setInt(6, raw.length);
|
||||
ps.setBytes(7, raw);
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
// Create a status row for each contact that can see the group
|
||||
Map<ContactId, Boolean> visibility =
|
||||
getGroupVisibility(txn, m.getGroupId());
|
||||
for (Entry<ContactId, Boolean> e : visibility.entrySet()) {
|
||||
ContactId c = e.getKey();
|
||||
boolean offered = removeOfferedMessage(txn, c, m.getId());
|
||||
boolean seen = offered || (sender != null && c.equals(sender));
|
||||
addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(),
|
||||
m.getLength(), state, e.getValue(), messageShared,
|
||||
false, seen);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
@@ -673,19 +744,28 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStatus(Connection txn, ContactId c, MessageId m, boolean ack,
|
||||
boolean seen) throws DbException {
|
||||
private void addStatus(Connection txn, MessageId m, ContactId c, GroupId g,
|
||||
long timestamp, int length, State state, boolean groupShared,
|
||||
boolean messageShared, boolean deleted, boolean seen)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO statuses (messageId, contactId, ack,"
|
||||
+ " seen, requested, expiry, txCount)"
|
||||
+ " VALUES (?, ?, ?, ?, FALSE, 0, 0)";
|
||||
String sql = "INSERT INTO statuses (messageId, contactId, groupId,"
|
||||
+ " timestamp, length, state, groupShared, messageShared,"
|
||||
+ " deleted, ack, seen, requested, expiry, txCount)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0)";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
ps.setBoolean(3, ack);
|
||||
ps.setBoolean(4, seen);
|
||||
ps.setBytes(3, g.getBytes());
|
||||
ps.setLong(4, timestamp);
|
||||
ps.setInt(5, length);
|
||||
ps.setInt(6, state.getValue());
|
||||
ps.setBoolean(7, groupShared);
|
||||
ps.setBoolean(8, messageShared);
|
||||
ps.setBoolean(9, deleted);
|
||||
ps.setBoolean(10, seen);
|
||||
ps.setBoolean(11, seen);
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
@@ -937,12 +1017,9 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT NULL FROM messages AS m"
|
||||
+ " JOIN groupVisibilities AS gv"
|
||||
+ " ON m.groupId = gv.groupId"
|
||||
+ " WHERE messageId = ?"
|
||||
+ " AND contactId = ?"
|
||||
+ " AND m.shared = TRUE";
|
||||
String sql = "SELECT NULL FROM statuses"
|
||||
+ " WHERE messageId = ? AND contactId = ?"
|
||||
+ " AND messageShared = TRUE";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
@@ -994,6 +1071,13 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
if (affected < 0) throw new DbStateException();
|
||||
if (affected > 1) throw new DbStateException();
|
||||
ps.close();
|
||||
// Update denormalised column in statuses
|
||||
sql = "UPDATE statuses SET deleted = TRUE WHERE messageId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
affected = ps.executeUpdate();
|
||||
if (affected < 0) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
@@ -1216,18 +1300,19 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ContactId> getGroupVisibility(Connection txn, GroupId g)
|
||||
public Map<ContactId, Boolean> getGroupVisibility(Connection txn, GroupId g)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT contactId FROM groupVisibilities"
|
||||
String sql = "SELECT contactId, shared FROM groupVisibilities"
|
||||
+ " WHERE groupId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, g.getBytes());
|
||||
rs = ps.executeQuery();
|
||||
List<ContactId> visible = new ArrayList<>();
|
||||
while (rs.next()) visible.add(new ContactId(rs.getInt(1)));
|
||||
Map<ContactId, Boolean> visible = new HashMap<>();
|
||||
while (rs.next())
|
||||
visible.put(new ContactId(rs.getInt(1)), rs.getBoolean(2));
|
||||
rs.close();
|
||||
ps.close();
|
||||
return visible;
|
||||
@@ -1505,12 +1590,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT m.messageId, txCount > 0, seen"
|
||||
+ " FROM messages AS m"
|
||||
+ " JOIN statuses AS s"
|
||||
+ " ON m.messageId = s.messageId"
|
||||
+ " WHERE groupId = ?"
|
||||
+ " AND contactId = ?";
|
||||
String sql = "SELECT messageId, txCount > 0, seen FROM statuses"
|
||||
+ " WHERE groupId = ? AND contactId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, g.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
@@ -1533,15 +1614,13 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageStatus getMessageStatus(Connection txn,
|
||||
ContactId c, MessageId m) throws DbException {
|
||||
public MessageStatus getMessageStatus(Connection txn, ContactId c,
|
||||
MessageId m) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT txCount > 0, seen"
|
||||
+ " FROM statuses"
|
||||
+ " WHERE messageId = ?"
|
||||
+ " AND contactId = ?";
|
||||
String sql = "SELECT txCount > 0, seen FROM statuses"
|
||||
+ " WHERE messageId = ? AND contactId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
@@ -1683,14 +1762,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT m.messageId FROM messages AS m"
|
||||
+ " JOIN groupVisibilities AS gv"
|
||||
+ " ON m.groupId = gv.groupId"
|
||||
+ " JOIN statuses AS s"
|
||||
+ " ON m.messageId = s.messageId"
|
||||
+ " AND gv.contactId = s.contactId"
|
||||
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
|
||||
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
|
||||
String sql = "SELECT messageId FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE"
|
||||
+ " AND seen = FALSE AND requested = FALSE"
|
||||
+ " AND expiry < ?"
|
||||
+ " ORDER BY timestamp LIMIT ?";
|
||||
@@ -1744,14 +1819,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT length, m.messageId FROM messages AS m"
|
||||
+ " JOIN groupVisibilities AS gv"
|
||||
+ " ON m.groupId = gv.groupId"
|
||||
+ " JOIN statuses AS s"
|
||||
+ " ON m.messageId = s.messageId"
|
||||
+ " AND gv.contactId = s.contactId"
|
||||
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
|
||||
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
|
||||
String sql = "SELECT length, messageId FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE"
|
||||
+ " AND seen = FALSE"
|
||||
+ " AND expiry < ?"
|
||||
+ " ORDER BY timestamp";
|
||||
@@ -1779,28 +1850,26 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToValidate(Connection txn,
|
||||
ClientId c) throws DbException {
|
||||
return getMessagesInState(txn, c, UNKNOWN);
|
||||
public Collection<MessageId> getMessagesToValidate(Connection txn)
|
||||
throws DbException {
|
||||
return getMessagesInState(txn, UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getPendingMessages(Connection txn,
|
||||
ClientId c) throws DbException {
|
||||
return getMessagesInState(txn, c, PENDING);
|
||||
public Collection<MessageId> getPendingMessages(Connection txn)
|
||||
throws DbException {
|
||||
return getMessagesInState(txn, PENDING);
|
||||
}
|
||||
|
||||
private Collection<MessageId> getMessagesInState(Connection txn, ClientId c,
|
||||
private Collection<MessageId> getMessagesInState(Connection txn,
|
||||
State state) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT messageId FROM messages AS m"
|
||||
+ " JOIN groups AS g ON m.groupId = g.groupId"
|
||||
+ " WHERE state = ? AND clientId = ? AND raw IS NOT NULL";
|
||||
String sql = "SELECT messageId FROM messages"
|
||||
+ " WHERE state = ? AND raw IS NOT NULL";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, state.getValue());
|
||||
ps.setString(2, c.getString());
|
||||
rs = ps.executeQuery();
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
|
||||
@@ -1815,8 +1884,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MessageId> getMessagesToShare(
|
||||
Connection txn, ClientId c) throws DbException {
|
||||
public Collection<MessageId> getMessagesToShare(Connection txn)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
@@ -1825,12 +1894,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
+ " ON m.messageId = d.dependencyId"
|
||||
+ " JOIN messages AS m1"
|
||||
+ " ON d.messageId = m1.messageId"
|
||||
+ " JOIN groups AS g"
|
||||
+ " ON m.groupId = g.groupId"
|
||||
+ " WHERE m.shared = FALSE AND m1.shared = TRUE"
|
||||
+ " AND g.clientId = ?";
|
||||
+ " WHERE m.state = ?"
|
||||
+ " AND m.shared = FALSE AND m1.shared = TRUE";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setString(1, c.getString());
|
||||
ps.setInt(1, DELIVERED.getValue());
|
||||
rs = ps.executeQuery();
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
|
||||
@@ -1844,6 +1911,36 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextSendTime(Connection txn, ContactId c)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT expiry FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE AND seen = FALSE"
|
||||
+ " ORDER BY expiry LIMIT 1";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
rs = ps.executeQuery();
|
||||
long nextSendTime = Long.MAX_VALUE;
|
||||
if (rs.next()) {
|
||||
nextSendTime = rs.getLong(1);
|
||||
if (rs.next()) throw new AssertionError();
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return nextSendTime;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs);
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public byte[] getRawMessage(Connection txn, MessageId m)
|
||||
@@ -1875,14 +1972,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT length, m.messageId FROM messages AS m"
|
||||
+ " JOIN groupVisibilities AS gv"
|
||||
+ " ON m.groupId = gv.groupId"
|
||||
+ " JOIN statuses AS s"
|
||||
+ " ON m.messageId = s.messageId"
|
||||
+ " AND gv.contactId = s.contactId"
|
||||
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
|
||||
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
|
||||
String sql = "SELECT length, messageId FROM statuses"
|
||||
+ " WHERE contactId = ? AND state = ?"
|
||||
+ " AND groupShared = TRUE AND messageShared = TRUE"
|
||||
+ " AND deleted = FALSE"
|
||||
+ " AND seen = FALSE AND requested = TRUE"
|
||||
+ " AND expiry < ?"
|
||||
+ " ORDER BY timestamp";
|
||||
@@ -2358,6 +2451,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
// Remove status rows for the messages in the group
|
||||
for (MessageId m : getMessageIds(txn, g)) removeStatus(txn, c, m);
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
@@ -2397,8 +2492,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeOfferedMessage(Connection txn, ContactId c,
|
||||
private boolean removeOfferedMessage(Connection txn, ContactId c,
|
||||
MessageId m) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
@@ -2442,16 +2536,15 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeStatus(Connection txn, ContactId c, MessageId m)
|
||||
private void removeStatus(Connection txn, ContactId c, MessageId m)
|
||||
throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "DELETE FROM statuses"
|
||||
+ " WHERE contactId = ? AND messageId = ?";
|
||||
+ " WHERE messageId = ? AND contactId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setBytes(2, m.getBytes());
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
@@ -2547,6 +2640,16 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||
ps.close();
|
||||
// Update denormalised column in statuses
|
||||
sql = "UPDATE statuses SET groupShared = ?"
|
||||
+ " WHERE contactId = ? AND groupId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBoolean(1, shared);
|
||||
ps.setInt(2, c.getInt());
|
||||
ps.setBytes(3, g.getBytes());
|
||||
affected = ps.executeUpdate();
|
||||
if (affected < 0) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
@@ -2565,6 +2668,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||
ps.close();
|
||||
// Update denormalised column in statuses
|
||||
sql = "UPDATE statuses SET messageShared = TRUE"
|
||||
+ " WHERE messageId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
affected = ps.executeUpdate();
|
||||
if (affected < 0) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
@@ -2591,6 +2702,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
affected = ps.executeUpdate();
|
||||
if (affected < 0) throw new DbStateException();
|
||||
ps.close();
|
||||
// Update denormalised column in statuses
|
||||
sql = "UPDATE statuses SET state = ? WHERE messageId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setInt(1, state.getValue());
|
||||
ps.setBytes(2, m.getBytes());
|
||||
affected = ps.executeUpdate();
|
||||
if (affected < 0) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.briarproject.bramble.db;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
class Migration31_32 implements Migration<Connection> {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(Migration31_32.class.getName());
|
||||
|
||||
@Override
|
||||
public int getStartVersion() {
|
||||
return 31;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndVersion() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrate(Connection txn) throws DbException {
|
||||
Statement s = null;
|
||||
try {
|
||||
s = txn.createStatement();
|
||||
// Add denormalised columns
|
||||
s.execute("ALTER TABLE statuses ADD COLUMN"
|
||||
+ " (groupId BINARY(32),"
|
||||
+ " timestamp BIGINT,"
|
||||
+ " length INT,"
|
||||
+ " state INT,"
|
||||
+ " groupShared BOOLEAN,"
|
||||
+ " messageShared BOOLEAN,"
|
||||
+ " deleted BOOLEAN)");
|
||||
// Populate columns from messages table
|
||||
s.execute("UPDATE statuses AS s SET (groupId, timestamp, length,"
|
||||
+ " state, messageShared, deleted) ="
|
||||
+ " (SELECT groupId, timestamp, length, state, shared,"
|
||||
+ " raw IS NULL FROM messages AS m"
|
||||
+ " WHERE s.messageId = m.messageId)");
|
||||
// Populate column from groupVisibilities table
|
||||
s.execute("UPDATE statuses AS s SET groupShared ="
|
||||
+ " (SELECT shared FROM groupVisibilities AS gv"
|
||||
+ " WHERE s.contactId = gv.contactId"
|
||||
+ " AND s.groupId = gv.groupId)");
|
||||
// Add not null constraints now columns have been populated
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN groupId SET NOT NULL");
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN timestamp"
|
||||
+ " SET NOT NULL");
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN length SET NOT NULL");
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN state SET NOT NULL");
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN groupShared"
|
||||
+ " SET NOT NULL");
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN messageShared"
|
||||
+ " SET NOT NULL");
|
||||
s.execute("ALTER TABLE statuses ALTER COLUMN deleted SET NOT NULL");
|
||||
// Add foreign key constraint
|
||||
s.execute("ALTER TABLE statuses"
|
||||
+ " ADD CONSTRAINT statusesForeignKeyGroupId"
|
||||
+ " FOREIGN KEY (groupId)"
|
||||
+ " REFERENCES groups (groupId)"
|
||||
+ " ON DELETE CASCADE");
|
||||
} catch (SQLException e) {
|
||||
tryToClose(s);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(@Nullable Statement s) {
|
||||
try {
|
||||
if (s != null) s.close();
|
||||
} catch (SQLException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.briarproject.bramble.keyagreement;
|
||||
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
interface ConnectionChooser {
|
||||
|
||||
/**
|
||||
* Submits a connection task to the chooser.
|
||||
*/
|
||||
void submit(Callable<KeyAgreementConnection> task);
|
||||
|
||||
/**
|
||||
* Returns a connection returned by any of the tasks submitted to the
|
||||
* chooser, waiting up to the given amount of time for a connection if
|
||||
* necessary. Returns null if the time elapses without a connection
|
||||
* becoming available.
|
||||
*
|
||||
* @param timeout the timeout in milliseconds
|
||||
* @throws InterruptedException if the thread is interrupted while waiting
|
||||
* for a connection to become available
|
||||
*/
|
||||
@Nullable
|
||||
KeyAgreementConnection poll(long timeout) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Stops the chooser. Any connections already returned to the chooser are
|
||||
* closed unless they have been removed from the chooser by calling
|
||||
* {@link #poll(long)}. Any connections subsequently returned to the
|
||||
* chooser will also be closed.
|
||||
*/
|
||||
void stop();
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.briarproject.bramble.keyagreement;
|
||||
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
@NotNullByDefault
|
||||
@ThreadSafe
|
||||
class ConnectionChooserImpl implements ConnectionChooser {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ConnectionChooserImpl.class.getName());
|
||||
|
||||
private final Clock clock;
|
||||
private final Executor ioExecutor;
|
||||
private final Object lock = new Object();
|
||||
|
||||
// The following are locking: lock
|
||||
private boolean stopped = false;
|
||||
private final Queue<KeyAgreementConnection> results = new LinkedList<>();
|
||||
|
||||
@Inject
|
||||
ConnectionChooserImpl(Clock clock, @IoExecutor Executor ioExecutor) {
|
||||
this.clock = clock;
|
||||
this.ioExecutor = ioExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(Callable<KeyAgreementConnection> task) {
|
||||
ioExecutor.execute(() -> {
|
||||
try {
|
||||
KeyAgreementConnection c = task.call();
|
||||
if (c != null) addResult(c);
|
||||
} catch (Exception e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public KeyAgreementConnection poll(long timeout)
|
||||
throws InterruptedException {
|
||||
long now = clock.currentTimeMillis();
|
||||
long end = now + timeout;
|
||||
synchronized (lock) {
|
||||
while (!stopped && results.isEmpty() && now < end) {
|
||||
lock.wait(end - now);
|
||||
now = clock.currentTimeMillis();
|
||||
}
|
||||
return results.poll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
List<KeyAgreementConnection> unused;
|
||||
synchronized (lock) {
|
||||
unused = new ArrayList<>(results);
|
||||
results.clear();
|
||||
stopped = true;
|
||||
lock.notifyAll();
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Closing " + unused.size() + " unused connections");
|
||||
for (KeyAgreementConnection c : unused) tryToClose(c.getConnection());
|
||||
}
|
||||
|
||||
private void addResult(KeyAgreementConnection c) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Got connection for " + c.getTransportId());
|
||||
boolean close = false;
|
||||
synchronized (lock) {
|
||||
if (stopped) {
|
||||
close = true;
|
||||
} else {
|
||||
results.add(c);
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
if (close) {
|
||||
LOG.info("Already stopped");
|
||||
tryToClose(c.getConnection());
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(DuplexTransportConnection conn) {
|
||||
try {
|
||||
conn.getReader().dispose(false, true);
|
||||
conn.getWriter().dispose(false);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,23 +13,19 @@ import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT;
|
||||
@@ -45,29 +41,27 @@ class KeyAgreementConnector {
|
||||
Logger.getLogger(KeyAgreementConnector.class.getName());
|
||||
|
||||
private final Callbacks callbacks;
|
||||
private final Clock clock;
|
||||
private final CryptoComponent crypto;
|
||||
private final PluginManager pluginManager;
|
||||
private final CompletionService<KeyAgreementConnection> connect;
|
||||
private final ConnectionChooser connectionChooser;
|
||||
|
||||
private final List<KeyAgreementListener> listeners = new ArrayList<>();
|
||||
private final List<Future<KeyAgreementConnection>> pending =
|
||||
new ArrayList<>();
|
||||
private final List<KeyAgreementListener> listeners =
|
||||
new CopyOnWriteArrayList<>();
|
||||
private final CountDownLatch aliceLatch = new CountDownLatch(1);
|
||||
private final AtomicBoolean waitingSent = new AtomicBoolean(false);
|
||||
|
||||
private volatile boolean connecting = false;
|
||||
private volatile boolean alice = false;
|
||||
private volatile boolean alice = false, stopped = false;
|
||||
|
||||
KeyAgreementConnector(Callbacks callbacks, Clock clock,
|
||||
KeyAgreementConnector(Callbacks callbacks,
|
||||
CryptoComponent crypto, PluginManager pluginManager,
|
||||
Executor ioExecutor) {
|
||||
ConnectionChooser connectionChooser) {
|
||||
this.callbacks = callbacks;
|
||||
this.clock = clock;
|
||||
this.crypto = crypto;
|
||||
this.pluginManager = pluginManager;
|
||||
connect = new ExecutorCompletionService<>(ioExecutor);
|
||||
this.connectionChooser = connectionChooser;
|
||||
}
|
||||
|
||||
public Payload listen(KeyPair localKeyPair) {
|
||||
Payload listen(KeyPair localKeyPair) {
|
||||
LOG.info("Starting BQP listeners");
|
||||
// Derive commitment
|
||||
byte[] commitment = crypto.deriveKeyCommitment(
|
||||
@@ -80,8 +74,9 @@ class KeyAgreementConnector {
|
||||
if (l != null) {
|
||||
TransportId id = plugin.getId();
|
||||
descriptors.add(new TransportDescriptor(id, l.getDescriptor()));
|
||||
pending.add(connect.submit(new ReadableTask(l.listen())));
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Listening via " + id);
|
||||
listeners.add(l);
|
||||
connectionChooser.submit(new ReadableTask(l::accept));
|
||||
}
|
||||
}
|
||||
return new Payload(commitment, descriptors);
|
||||
@@ -89,125 +84,92 @@ class KeyAgreementConnector {
|
||||
|
||||
void stopListening() {
|
||||
LOG.info("Stopping BQP listeners");
|
||||
for (KeyAgreementListener l : listeners) {
|
||||
l.close();
|
||||
}
|
||||
listeners.clear();
|
||||
stopped = true;
|
||||
aliceLatch.countDown();
|
||||
for (KeyAgreementListener l : listeners) l.close();
|
||||
connectionChooser.stop();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public KeyAgreementTransport connect(Payload remotePayload,
|
||||
boolean alice) {
|
||||
// Let the listeners know if we are Alice
|
||||
this.connecting = true;
|
||||
public KeyAgreementTransport connect(Payload remotePayload, boolean alice) {
|
||||
// Let the ReadableTasks know if we are Alice
|
||||
this.alice = alice;
|
||||
long end = clock.currentTimeMillis() + CONNECTION_TIMEOUT;
|
||||
aliceLatch.countDown();
|
||||
|
||||
// Start connecting over supported transports
|
||||
LOG.info("Starting outgoing BQP connections");
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Starting outgoing BQP connections as "
|
||||
+ (alice ? "Alice" : "Bob"));
|
||||
}
|
||||
for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
|
||||
Plugin p = pluginManager.getPlugin(d.getId());
|
||||
if (p instanceof DuplexPlugin) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connecting via " + d.getId());
|
||||
DuplexPlugin plugin = (DuplexPlugin) p;
|
||||
pending.add(connect.submit(new ReadableTask(
|
||||
new ConnectorTask(plugin, remotePayload.getCommitment(),
|
||||
d.getDescriptor(), end))));
|
||||
byte[] commitment = remotePayload.getCommitment();
|
||||
BdfList descriptor = d.getDescriptor();
|
||||
connectionChooser.submit(new ReadableTask(
|
||||
new ConnectorTask(plugin, commitment, descriptor)));
|
||||
}
|
||||
}
|
||||
|
||||
// Get chosen connection
|
||||
KeyAgreementConnection chosen = null;
|
||||
try {
|
||||
long now = clock.currentTimeMillis();
|
||||
Future<KeyAgreementConnection> f =
|
||||
connect.poll(end - now, MILLISECONDS);
|
||||
if (f == null)
|
||||
return null; // No task completed within the timeout.
|
||||
chosen = f.get();
|
||||
KeyAgreementConnection chosen =
|
||||
connectionChooser.poll(CONNECTION_TIMEOUT);
|
||||
if (chosen == null) return null;
|
||||
return new KeyAgreementTransport(chosen);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while waiting for connection");
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
} catch (ExecutionException | IOException e) {
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
} finally {
|
||||
stopListening();
|
||||
// Close all other connections
|
||||
closePending(chosen);
|
||||
}
|
||||
}
|
||||
|
||||
private void closePending(@Nullable KeyAgreementConnection chosen) {
|
||||
for (Future<KeyAgreementConnection> f : pending) {
|
||||
try {
|
||||
if (f.cancel(true)) {
|
||||
LOG.info("Cancelled task");
|
||||
} else if (!f.isCancelled()) {
|
||||
KeyAgreementConnection c = f.get();
|
||||
if (c != null && c != chosen)
|
||||
tryToClose(c.getConnection(), false);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while closing sockets");
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (ExecutionException e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
|
||||
try {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Closing connection, exception: " + exception);
|
||||
conn.getReader().dispose(exception, true);
|
||||
conn.getWriter().dispose(exception);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
}
|
||||
private void waitingForAlice() {
|
||||
if (!waitingSent.getAndSet(true)) callbacks.connectionWaiting();
|
||||
}
|
||||
|
||||
private class ConnectorTask implements Callable<KeyAgreementConnection> {
|
||||
|
||||
private final byte[] commitment;
|
||||
private final BdfList descriptor;
|
||||
private final long end;
|
||||
private final DuplexPlugin plugin;
|
||||
|
||||
private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
|
||||
BdfList descriptor, long end) {
|
||||
BdfList descriptor) {
|
||||
this.plugin = plugin;
|
||||
this.commitment = commitment;
|
||||
this.descriptor = descriptor;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public KeyAgreementConnection call() throws Exception {
|
||||
// Repeat attempts until we connect, get interrupted, or time out
|
||||
while (true) {
|
||||
long now = clock.currentTimeMillis();
|
||||
if (now > end) throw new IOException();
|
||||
// Repeat attempts until we connect, get stopped, or get interrupted
|
||||
while (!stopped) {
|
||||
DuplexTransportConnection conn =
|
||||
plugin.createKeyAgreementConnection(commitment,
|
||||
descriptor, end - now);
|
||||
descriptor);
|
||||
if (conn != null) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(plugin.getId().getString() +
|
||||
": Outgoing connection");
|
||||
LOG.info(plugin.getId() + ": Outgoing connection");
|
||||
return new KeyAgreementConnection(conn, plugin.getId());
|
||||
}
|
||||
// Wait 2s before retry (to circumvent transient failures)
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class ReadableTask
|
||||
implements Callable<KeyAgreementConnection> {
|
||||
private class ReadableTask implements Callable<KeyAgreementConnection> {
|
||||
|
||||
private final Callable<KeyAgreementConnection> connectionTask;
|
||||
|
||||
@@ -215,24 +177,23 @@ class KeyAgreementConnector {
|
||||
this.connectionTask = connectionTask;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public KeyAgreementConnection call() throws Exception {
|
||||
KeyAgreementConnection c = connectionTask.call();
|
||||
if (c == null) return null;
|
||||
aliceLatch.await();
|
||||
if (alice || stopped) return c;
|
||||
// Bob waits here for Alice to scan his QR code, determine her
|
||||
// role, and send her key
|
||||
InputStream in = c.getConnection().getReader().getInputStream();
|
||||
boolean waitingSent = false;
|
||||
while (!alice && in.available() == 0) {
|
||||
if (!waitingSent && connecting && !alice) {
|
||||
// Bob waits here until Alice obtains his payload.
|
||||
callbacks.connectionWaiting();
|
||||
waitingSent = true;
|
||||
}
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info(c.getTransportId().getString() +
|
||||
": Waiting for connection");
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
while (!stopped && in.available() == 0) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(c.getTransportId() + ": Waiting for data");
|
||||
waitingForAlice();
|
||||
Thread.sleep(500);
|
||||
}
|
||||
if (!alice && LOG.isLoggable(INFO))
|
||||
if (!stopped && LOG.isLoggable(INFO))
|
||||
LOG.info(c.getTransportId().getString() + ": Data available");
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
package org.briarproject.bramble.keyagreement;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||
import org.briarproject.bramble.api.data.BdfWriterFactory;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
|
||||
import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
|
||||
import org.briarproject.bramble.api.keyagreement.PayloadParser;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
@@ -22,13 +13,9 @@ import dagger.Provides;
|
||||
public class KeyAgreementModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
KeyAgreementTaskFactory provideKeyAgreementTaskFactory(Clock clock,
|
||||
CryptoComponent crypto, EventBus eventBus,
|
||||
@IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder,
|
||||
PluginManager pluginManager) {
|
||||
return new KeyAgreementTaskFactoryImpl(clock, crypto, eventBus,
|
||||
ioExecutor, payloadEncoder, pluginManager);
|
||||
KeyAgreementTaskFactory provideKeyAgreementTaskFactory(
|
||||
KeyAgreementTaskFactoryImpl keyAgreementTaskFactory) {
|
||||
return keyAgreementTaskFactory;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -40,4 +27,10 @@ public class KeyAgreementModule {
|
||||
PayloadParser providePayloadParser(BdfReaderFactory bdfReaderFactory) {
|
||||
return new PayloadParserImpl(bdfReaderFactory);
|
||||
}
|
||||
|
||||
@Provides
|
||||
ConnectionChooser provideConnectionChooser(
|
||||
ConnectionChooserImpl connectionChooser) {
|
||||
return connectionChooser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@ class KeyAgreementProtocol {
|
||||
byte[] theirPublicKey;
|
||||
if (alice) {
|
||||
sendKey();
|
||||
// Alice waits here until Bob obtains her payload.
|
||||
// Alice waits here for Bob to scan her QR code, determine his
|
||||
// role, receive her key and respond with his key
|
||||
callbacks.connectionWaiting();
|
||||
theirPublicKey = receiveKey();
|
||||
} else {
|
||||
|
||||
@@ -5,42 +5,37 @@ import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
|
||||
import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory {
|
||||
|
||||
private final Clock clock;
|
||||
private final CryptoComponent crypto;
|
||||
private final EventBus eventBus;
|
||||
private final Executor ioExecutor;
|
||||
private final PayloadEncoder payloadEncoder;
|
||||
private final PluginManager pluginManager;
|
||||
private final Provider<ConnectionChooser> connectionChooserProvider;
|
||||
|
||||
@Inject
|
||||
KeyAgreementTaskFactoryImpl(Clock clock, CryptoComponent crypto,
|
||||
EventBus eventBus, @IoExecutor Executor ioExecutor,
|
||||
PayloadEncoder payloadEncoder, PluginManager pluginManager) {
|
||||
this.clock = clock;
|
||||
KeyAgreementTaskFactoryImpl(CryptoComponent crypto, EventBus eventBus,
|
||||
PayloadEncoder payloadEncoder, PluginManager pluginManager,
|
||||
Provider<ConnectionChooser> connectionChooserProvider) {
|
||||
this.crypto = crypto;
|
||||
this.eventBus = eventBus;
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.payloadEncoder = payloadEncoder;
|
||||
this.pluginManager = pluginManager;
|
||||
this.connectionChooserProvider = connectionChooserProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyAgreementTask createTask() {
|
||||
return new KeyAgreementTaskImpl(clock, crypto, eventBus, payloadEncoder,
|
||||
pluginManager, ioExecutor);
|
||||
return new KeyAgreementTaskImpl(crypto, eventBus, payloadEncoder,
|
||||
pluginManager, connectionChooserProvider.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,16 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class KeyAgreementTaskImpl extends Thread implements
|
||||
KeyAgreementTask, KeyAgreementConnector.Callbacks,
|
||||
KeyAgreementProtocol.Callbacks {
|
||||
class KeyAgreementTaskImpl extends Thread implements KeyAgreementTask,
|
||||
KeyAgreementProtocol.Callbacks, KeyAgreementConnector.Callbacks {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(KeyAgreementTaskImpl.class.getName());
|
||||
@@ -43,15 +40,15 @@ class KeyAgreementTaskImpl extends Thread implements
|
||||
private Payload localPayload;
|
||||
private Payload remotePayload;
|
||||
|
||||
KeyAgreementTaskImpl(Clock clock, CryptoComponent crypto,
|
||||
EventBus eventBus, PayloadEncoder payloadEncoder,
|
||||
PluginManager pluginManager, Executor ioExecutor) {
|
||||
KeyAgreementTaskImpl(CryptoComponent crypto, EventBus eventBus,
|
||||
PayloadEncoder payloadEncoder, PluginManager pluginManager,
|
||||
ConnectionChooser connectionChooser) {
|
||||
this.crypto = crypto;
|
||||
this.eventBus = eventBus;
|
||||
this.payloadEncoder = payloadEncoder;
|
||||
localKeyPair = crypto.generateAgreementKeyPair();
|
||||
connector = new KeyAgreementConnector(this, clock, crypto,
|
||||
pluginManager, ioExecutor);
|
||||
connector = new KeyAgreementConnector(this, crypto, pluginManager,
|
||||
connectionChooser);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,10 +62,8 @@ class KeyAgreementTaskImpl extends Thread implements
|
||||
@Override
|
||||
public synchronized void stopListening() {
|
||||
if (localPayload != null) {
|
||||
if (remotePayload == null)
|
||||
connector.stopListening();
|
||||
else
|
||||
interrupt();
|
||||
if (remotePayload == null) connector.stopListening();
|
||||
else interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.briarproject.bramble.lifecycle;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.KeyPair;
|
||||
import org.briarproject.bramble.api.db.DataTooNewException;
|
||||
import org.briarproject.bramble.api.db.DataTooOldException;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.MigrationListener;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.identity.AuthorFactory;
|
||||
@@ -12,7 +15,7 @@ import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.lifecycle.Service;
|
||||
import org.briarproject.bramble.api.lifecycle.ServiceException;
|
||||
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
|
||||
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Client;
|
||||
|
||||
@@ -29,14 +32,21 @@ import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_NEW_ERROR;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_ERROR;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DB_ERROR;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class LifecycleManagerImpl implements LifecycleManager {
|
||||
class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(LifecycleManagerImpl.class.getName());
|
||||
@@ -54,6 +64,8 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
private final CountDownLatch startupLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
|
||||
|
||||
private volatile LifecycleState state = STARTING;
|
||||
|
||||
@Inject
|
||||
LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus,
|
||||
CryptoComponent crypto, AuthorFactory authorFactory,
|
||||
@@ -119,7 +131,7 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
LOG.info("Starting services");
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
boolean reopened = db.open();
|
||||
boolean reopened = db.open(this);
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
if (reopened)
|
||||
@@ -131,7 +143,10 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
registerLocalAuthor(createLocalAuthor(nickname));
|
||||
}
|
||||
|
||||
state = STARTING_SERVICES;
|
||||
dbLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(STARTING_SERVICES));
|
||||
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
for (Client c : clients) {
|
||||
@@ -157,8 +172,17 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
+ " took " + duration + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
state = RUNNING;
|
||||
startupLatch.countDown();
|
||||
eventBus.broadcast(new LifecycleEvent(RUNNING));
|
||||
return SUCCESS;
|
||||
} catch (DataTooOldException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return DATA_TOO_OLD_ERROR;
|
||||
} catch (DataTooNewException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return DATA_TOO_NEW_ERROR;
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return DB_ERROR;
|
||||
@@ -170,6 +194,12 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMigrationRun() {
|
||||
state = MIGRATING_DATABASE;
|
||||
eventBus.broadcast(new LifecycleEvent(MIGRATING_DATABASE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopServices() {
|
||||
try {
|
||||
@@ -180,7 +210,8 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
}
|
||||
try {
|
||||
LOG.info("Stopping services");
|
||||
eventBus.broadcast(new ShutdownEvent());
|
||||
state = STOPPING;
|
||||
eventBus.broadcast(new LifecycleEvent(STOPPING));
|
||||
for (Service s : services) {
|
||||
long start = System.currentTimeMillis();
|
||||
s.stopService();
|
||||
@@ -225,4 +256,8 @@ class LifecycleManagerImpl implements LifecycleManager {
|
||||
shutdownLatch.await();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleState getLifecycleState() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.briarproject.bramble.plugin.bluetooth;
|
||||
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.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
@@ -13,8 +15,11 @@ import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent;
|
||||
import org.briarproject.bramble.api.plugin.event.DisableBluetoothEvent;
|
||||
import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.util.OsUtils;
|
||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -23,30 +28,25 @@ import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.bluetooth.BluetoothStateException;
|
||||
import javax.bluetooth.LocalDevice;
|
||||
import javax.microedition.io.Connector;
|
||||
import javax.microedition.io.StreamConnection;
|
||||
import javax.microedition.io.StreamConnectionNotifier;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static javax.bluetooth.DiscoveryAgent.GIAC;
|
||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
|
||||
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 static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class BluetoothPlugin implements DuplexPlugin {
|
||||
abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(BluetoothPlugin.class.getName());
|
||||
@@ -58,9 +58,38 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
private final int maxLatency;
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
|
||||
private volatile boolean running = false;
|
||||
private volatile StreamConnectionNotifier socket = null;
|
||||
private volatile LocalDevice localDevice = null;
|
||||
private volatile boolean running = false, contactConnections = false;
|
||||
private volatile String contactConnectionsUuid = null;
|
||||
private volatile SS socket = null;
|
||||
|
||||
abstract void initialiseAdapter() throws IOException;
|
||||
|
||||
abstract boolean isAdapterEnabled();
|
||||
|
||||
abstract void enableAdapter();
|
||||
|
||||
abstract void disableAdapterIfEnabledByUs();
|
||||
|
||||
abstract void setEnabledByUs();
|
||||
|
||||
/**
|
||||
* Returns the local Bluetooth address, or null if no valid address can
|
||||
* be found.
|
||||
*/
|
||||
@Nullable
|
||||
abstract String getBluetoothAddress();
|
||||
|
||||
abstract SS openServerSocket(String uuid) throws IOException;
|
||||
|
||||
abstract void tryToClose(@Nullable SS ss);
|
||||
|
||||
abstract DuplexTransportConnection acceptConnection(SS ss)
|
||||
throws IOException;
|
||||
|
||||
abstract boolean isValidAddress(String address);
|
||||
|
||||
abstract DuplexTransportConnection connectTo(String address, String uuid)
|
||||
throws IOException;
|
||||
|
||||
BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom,
|
||||
Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
|
||||
@@ -71,6 +100,19 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
this.maxLatency = maxLatency;
|
||||
}
|
||||
|
||||
void onAdapterEnabled() {
|
||||
LOG.info("Bluetooth enabled");
|
||||
// We may not have been able to get the local address before
|
||||
ioExecutor.execute(this::updateProperties);
|
||||
if (shouldAllowContactConnections()) bind();
|
||||
}
|
||||
|
||||
void onAdapterDisabled() {
|
||||
LOG.info("Bluetooth disabled");
|
||||
tryToClose(socket);
|
||||
callback.transportDisabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
@@ -90,107 +132,103 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
@Override
|
||||
public void start() throws PluginException {
|
||||
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||
// Initialise the Bluetooth stack
|
||||
try {
|
||||
localDevice = LocalDevice.getLocalDevice();
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// On Linux the user may need to install libbluetooth-dev
|
||||
if (OsUtils.isLinux())
|
||||
callback.showMessage("BLUETOOTH_INSTALL_LIBS");
|
||||
throw new PluginException(e);
|
||||
} catch (BluetoothStateException e) {
|
||||
initialiseAdapter();
|
||||
} catch (IOException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Local address " + localDevice.getBluetoothAddress());
|
||||
updateProperties();
|
||||
running = true;
|
||||
bind();
|
||||
loadSettings();
|
||||
if (shouldAllowContactConnections()) {
|
||||
if (isAdapterEnabled()) bind();
|
||||
else enableAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
contactConnections =
|
||||
callback.getSettings().getBoolean(PREF_BT_ENABLE, false);
|
||||
}
|
||||
|
||||
private boolean shouldAllowContactConnections() {
|
||||
return contactConnections;
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
ioExecutor.execute(() -> {
|
||||
if (!running) return;
|
||||
// Advertise the Bluetooth address to contacts
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put(PROP_ADDRESS, localDevice.getBluetoothAddress());
|
||||
callback.mergeLocalProperties(p);
|
||||
if (!isRunning() || !shouldAllowContactConnections()) return;
|
||||
// Bind a server socket to accept connections from contacts
|
||||
String url = makeUrl("localhost", getUuid());
|
||||
StreamConnectionNotifier ss;
|
||||
SS ss;
|
||||
try {
|
||||
ss = (StreamConnectionNotifier) Connector.open(url);
|
||||
ss = openServerSocket(contactConnectionsUuid);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return;
|
||||
}
|
||||
if (!running) {
|
||||
if (!isRunning() || !shouldAllowContactConnections()) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
socket = ss;
|
||||
backoff.reset();
|
||||
callback.transportEnabled();
|
||||
acceptContactConnections(ss);
|
||||
acceptContactConnections();
|
||||
});
|
||||
}
|
||||
|
||||
private String makeUrl(String address, String uuid) {
|
||||
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
||||
}
|
||||
|
||||
private String getUuid() {
|
||||
String uuid = callback.getLocalProperties().get(PROP_UUID);
|
||||
private void updateProperties() {
|
||||
TransportProperties p = callback.getLocalProperties();
|
||||
String address = p.get(PROP_ADDRESS);
|
||||
String uuid = p.get(PROP_UUID);
|
||||
boolean changed = false;
|
||||
if (address == null) {
|
||||
address = getBluetoothAddress();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Local address " + scrubMacAddress(address));
|
||||
if (!StringUtils.isNullOrEmpty(address)) {
|
||||
p.put(PROP_ADDRESS, address);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
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);
|
||||
changed = true;
|
||||
}
|
||||
return uuid;
|
||||
contactConnectionsUuid = uuid;
|
||||
if (changed) callback.mergeLocalProperties(p);
|
||||
}
|
||||
|
||||
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() {
|
||||
while (true) {
|
||||
StreamConnection s;
|
||||
DuplexTransportConnection conn;
|
||||
try {
|
||||
s = ss.acceptAndOpen();
|
||||
conn = acceptConnection(socket);
|
||||
} catch (IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
|
||||
return;
|
||||
}
|
||||
backoff.reset();
|
||||
callback.incomingConnectionCreated(wrapSocket(s));
|
||||
callback.incomingConnectionCreated(conn);
|
||||
if (!running) return;
|
||||
}
|
||||
}
|
||||
|
||||
private DuplexTransportConnection wrapSocket(StreamConnection s) {
|
||||
return new BluetoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
tryToClose(socket);
|
||||
callback.transportDisabled();
|
||||
disableAdapterIfEnabledByUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
return running && isAdapterEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -205,7 +243,7 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
|
||||
@Override
|
||||
public void poll(Collection<ContactId> connected) {
|
||||
if (!running) return;
|
||||
if (!isRunning() || !shouldAllowContactConnections()) return;
|
||||
backoff.increment();
|
||||
// Try to connect to known devices in parallel
|
||||
Map<ContactId, TransportProperties> remote =
|
||||
@@ -218,41 +256,56 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
String uuid = e.getValue().get(PROP_UUID);
|
||||
if (StringUtils.isNullOrEmpty(uuid)) continue;
|
||||
ioExecutor.execute(() -> {
|
||||
if (!running) return;
|
||||
StreamConnection s = connect(makeUrl(address, uuid));
|
||||
if (s != null) {
|
||||
if (!isRunning() || !shouldAllowContactConnections()) return;
|
||||
DuplexTransportConnection conn = connect(address, uuid);
|
||||
if (conn != null) {
|
||||
backoff.reset();
|
||||
callback.outgoingConnectionCreated(c, wrapSocket(s));
|
||||
callback.outgoingConnectionCreated(c, conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private StreamConnection connect(String url) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Connecting to " + url);
|
||||
private DuplexTransportConnection connect(String address, String uuid) {
|
||||
// Validate the address
|
||||
if (!isValidAddress(address)) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
// Not scrubbing here to be able to figure out the problem
|
||||
LOG.warning("Invalid address " + address);
|
||||
return null;
|
||||
}
|
||||
// Validate the UUID
|
||||
try {
|
||||
StreamConnection s = (StreamConnection) Connector.open(url);
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Connected to " + url);
|
||||
return s;
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
UUID.fromString(uuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
||||
return null;
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connecting to " + scrubMacAddress(address));
|
||||
try {
|
||||
DuplexTransportConnection conn = connectTo(address, uuid);
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connected to " + scrubMacAddress(address));
|
||||
return conn;
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Could not connect to " + url);
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Could not connect to " + scrubMacAddress(address));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createConnection(ContactId c) {
|
||||
if (!running) return null;
|
||||
if (!isRunning() || !shouldAllowContactConnections()) return null;
|
||||
TransportProperties p = callback.getRemoteProperties(c);
|
||||
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);
|
||||
if (s == null) return null;
|
||||
return new BluetoothTransportConnection(this, s);
|
||||
return connect(address, uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -262,35 +315,34 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
|
||||
@Override
|
||||
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
|
||||
if (!running) return null;
|
||||
if (!isRunning()) return null;
|
||||
// There's no point listening if we can't discover our own address
|
||||
String address = getBluetoothAddress();
|
||||
if (address == null) return null;
|
||||
// No truncation necessary because COMMIT_LENGTH = 16
|
||||
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
||||
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
||||
String url = makeUrl("localhost", uuid);
|
||||
// Make the device discoverable if possible
|
||||
makeDeviceDiscoverable();
|
||||
// Bind a server socket for receiving key agreementconnections
|
||||
StreamConnectionNotifier ss;
|
||||
// Bind a server socket for receiving key agreement connections
|
||||
SS ss;
|
||||
try {
|
||||
ss = (StreamConnectionNotifier) Connector.open(url);
|
||||
ss = openServerSocket(uuid);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
if (!running) {
|
||||
if (!isRunning()) {
|
||||
tryToClose(ss);
|
||||
return null;
|
||||
}
|
||||
BdfList descriptor = new BdfList();
|
||||
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
||||
String address = localDevice.getBluetoothAddress();
|
||||
descriptor.add(StringUtils.macToBytes(address));
|
||||
return new BluetoothKeyAgreementListener(descriptor, ss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createKeyAgreementConnection(
|
||||
byte[] commitment, BdfList descriptor, long timeout) {
|
||||
byte[] commitment, BdfList descriptor) {
|
||||
if (!isRunning()) return null;
|
||||
String address;
|
||||
try {
|
||||
@@ -303,10 +355,7 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
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);
|
||||
return connect(address, uuid);
|
||||
}
|
||||
|
||||
private String parseAddress(BdfList descriptor) throws FormatException {
|
||||
@@ -315,44 +364,56 @@ class BluetoothPlugin implements DuplexPlugin {
|
||||
return StringUtils.macToString(mac);
|
||||
}
|
||||
|
||||
private void makeDeviceDiscoverable() {
|
||||
// Try to make the device discoverable (requires root on Linux)
|
||||
try {
|
||||
localDevice.setDiscoverable(GIAC);
|
||||
} catch (BluetoothStateException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof EnableBluetoothEvent) {
|
||||
ioExecutor.execute(this::enableAdapter);
|
||||
} else if (e instanceof DisableBluetoothEvent) {
|
||||
ioExecutor.execute(this::disableAdapterIfEnabledByUs);
|
||||
} else if (e instanceof BluetoothEnabledEvent) {
|
||||
setEnabledByUs();
|
||||
} else if (e instanceof SettingsUpdatedEvent) {
|
||||
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
|
||||
if (s.getNamespace().equals(ID.getString()))
|
||||
ioExecutor.execute(this::onSettingsUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSettingsUpdated() {
|
||||
boolean wasAllowed = shouldAllowContactConnections();
|
||||
loadSettings();
|
||||
boolean isAllowed = shouldAllowContactConnections();
|
||||
if (wasAllowed && !isAllowed) {
|
||||
LOG.info("Contact connections disabled");
|
||||
tryToClose(socket);
|
||||
callback.transportDisabled();
|
||||
disableAdapterIfEnabledByUs();
|
||||
} else if (!wasAllowed && isAllowed) {
|
||||
LOG.info("Contact connections enabled");
|
||||
if (isAdapterEnabled()) bind();
|
||||
else enableAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
||||
|
||||
private final StreamConnectionNotifier ss;
|
||||
private final SS ss;
|
||||
|
||||
private BluetoothKeyAgreementListener(BdfList descriptor,
|
||||
StreamConnectionNotifier ss) {
|
||||
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
|
||||
super(descriptor);
|
||||
this.ss = ss;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Callable<KeyAgreementConnection> listen() {
|
||||
return () -> {
|
||||
StreamConnection s = ss.acceptAndOpen();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(ID.getString() + ": Incoming connection");
|
||||
return new KeyAgreementConnection(
|
||||
new BluetoothTransportConnection(
|
||||
BluetoothPlugin.this, s), ID);
|
||||
};
|
||||
public KeyAgreementConnection accept() throws IOException {
|
||||
DuplexTransportConnection conn = acceptConnection(ss);
|
||||
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
|
||||
return new KeyAgreementConnection(conn, ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
ss.close();
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
tryToClose(ss);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -224,7 +223,7 @@ class LanTcpPlugin extends TcpPlugin {
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createKeyAgreementConnection(
|
||||
byte[] commitment, BdfList descriptor, long timeout) {
|
||||
byte[] commitment, BdfList descriptor) {
|
||||
if (!isRunning()) return null;
|
||||
InetSocketAddress remote;
|
||||
try {
|
||||
@@ -242,10 +241,11 @@ class LanTcpPlugin extends TcpPlugin {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connecting to " + scrubSocketAddress(remote));
|
||||
Socket s = createSocket();
|
||||
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
|
||||
s.connect(remote);
|
||||
s.setSoTimeout(socketTimeout);
|
||||
if (LOG.isLoggable(INFO))
|
||||
@@ -283,14 +283,11 @@ class LanTcpPlugin extends TcpPlugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Callable<KeyAgreementConnection> listen() {
|
||||
return () -> {
|
||||
Socket s = ss.accept();
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info(ID.getString() + ": Incoming connection");
|
||||
return new KeyAgreementConnection(
|
||||
new TcpTransportConnection(LanTcpPlugin.this, s), ID);
|
||||
};
|
||||
public KeyAgreementConnection accept() throws IOException {
|
||||
Socket s = ss.accept();
|
||||
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
|
||||
return new KeyAgreementConnection(new TcpTransportConnection(
|
||||
LanTcpPlugin.this, s), ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.plugin.tcp;
|
||||
|
||||
import org.briarproject.bramble.PoliteExecutor;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
@@ -47,7 +48,7 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(TcpPlugin.class.getName());
|
||||
|
||||
protected final Executor ioExecutor;
|
||||
protected final Executor ioExecutor, bindExecutor;
|
||||
protected final Backoff backoff;
|
||||
protected final DuplexPluginCallback callback;
|
||||
protected final int maxLatency, maxIdleTime, socketTimeout;
|
||||
@@ -90,6 +91,8 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
if (maxIdleTime > Integer.MAX_VALUE / 2)
|
||||
socketTimeout = Integer.MAX_VALUE;
|
||||
else socketTimeout = maxIdleTime * 2;
|
||||
// Don't execute more than one bind operation at a time
|
||||
bindExecutor = new PoliteExecutor("TcpPlugin", ioExecutor, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,8 +113,9 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
}
|
||||
|
||||
protected void bind() {
|
||||
ioExecutor.execute(() -> {
|
||||
bindExecutor.execute(() -> {
|
||||
if (!running) return;
|
||||
if (socket != null && !socket.isClosed()) return;
|
||||
ServerSocket ss = null;
|
||||
for (InetSocketAddress addr : getLocalSocketAddresses()) {
|
||||
try {
|
||||
@@ -243,10 +247,11 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Connecting to " + scrubSocketAddress(remote));
|
||||
Socket s = createSocket();
|
||||
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
|
||||
s.connect(remote);
|
||||
s.setSoTimeout(socketTimeout);
|
||||
if (LOG.isLoggable(INFO))
|
||||
@@ -261,6 +266,10 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Socket createSocket() throws IOException {
|
||||
return new Socket();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
InetSocketAddress parseSocketAddress(String ipPort) {
|
||||
if (StringUtils.isNullOrEmpty(ipPort)) return null;
|
||||
@@ -297,7 +306,7 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createKeyAgreementConnection(
|
||||
byte[] commitment, BdfList descriptor, long timeout) {
|
||||
byte[] commitment, BdfList descriptor) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
|
||||
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Ack;
|
||||
import org.briarproject.bramble.api.sync.Offer;
|
||||
@@ -30,6 +30,7 @@ import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
@@ -37,6 +38,7 @@ import javax.annotation.concurrent.ThreadSafe;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
|
||||
|
||||
@@ -50,13 +52,14 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD
|
||||
@NotNullByDefault
|
||||
class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
|
||||
// Check for retransmittable records once every 60 seconds
|
||||
private static final int RETX_QUERY_INTERVAL = 60 * 1000;
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(DuplexOutgoingSession.class.getName());
|
||||
|
||||
private static final ThrowingRunnable<IOException> CLOSE = () -> {
|
||||
};
|
||||
private static final ThrowingRunnable<IOException>
|
||||
NEXT_SEND_TIME_DECREASED = () -> {
|
||||
};
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final Executor dbExecutor;
|
||||
@@ -72,6 +75,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
private final AtomicBoolean generateOfferQueued = new AtomicBoolean(false);
|
||||
private final AtomicBoolean generateRequestQueued =
|
||||
new AtomicBoolean(false);
|
||||
private final AtomicLong nextSendTime = new AtomicLong(Long.MAX_VALUE);
|
||||
|
||||
private volatile boolean interrupted = false;
|
||||
|
||||
@@ -101,15 +105,15 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
generateRequest();
|
||||
long now = clock.currentTimeMillis();
|
||||
long nextKeepalive = now + maxIdleTime;
|
||||
long nextRetxQuery = now + RETX_QUERY_INTERVAL;
|
||||
boolean dataToFlush = true;
|
||||
// Write records until interrupted
|
||||
try {
|
||||
while (!interrupted) {
|
||||
// Work out how long we should wait for a record
|
||||
now = clock.currentTimeMillis();
|
||||
long wait = Math.min(nextKeepalive, nextRetxQuery) - now;
|
||||
if (wait < 0) wait = 0;
|
||||
long keepaliveWait = Math.max(0, nextKeepalive - now);
|
||||
long sendWait = Math.max(0, nextSendTime.get() - now);
|
||||
long wait = Math.min(keepaliveWait, sendWait);
|
||||
// Flush any unflushed data if we're going to wait
|
||||
if (wait > 0 && dataToFlush && writerTasks.isEmpty()) {
|
||||
recordWriter.flush();
|
||||
@@ -121,20 +125,25 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
MILLISECONDS);
|
||||
if (task == null) {
|
||||
now = clock.currentTimeMillis();
|
||||
if (now >= nextRetxQuery) {
|
||||
// Check for retransmittable records
|
||||
if (now >= nextSendTime.get()) {
|
||||
// Check for retransmittable messages
|
||||
LOG.info("Checking for retransmittable messages");
|
||||
setNextSendTime(Long.MAX_VALUE);
|
||||
generateBatch();
|
||||
generateOffer();
|
||||
nextRetxQuery = now + RETX_QUERY_INTERVAL;
|
||||
}
|
||||
if (now >= nextKeepalive) {
|
||||
// Flush the stream to keep it alive
|
||||
LOG.info("Sending keepalive");
|
||||
recordWriter.flush();
|
||||
dataToFlush = false;
|
||||
nextKeepalive = now + maxIdleTime;
|
||||
}
|
||||
} else if (task == CLOSE) {
|
||||
LOG.info("Closed");
|
||||
break;
|
||||
} else if (task == NEXT_SEND_TIME_DECREASED) {
|
||||
LOG.info("Next send time decreased");
|
||||
} else {
|
||||
task.run();
|
||||
dataToFlush = true;
|
||||
@@ -170,6 +179,11 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
dbExecutor.execute(new GenerateRequest());
|
||||
}
|
||||
|
||||
private void setNextSendTime(long time) {
|
||||
long old = nextSendTime.getAndSet(time);
|
||||
if (time < old) writerTasks.add(NEXT_SEND_TIME_DECREASED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
interrupted = true;
|
||||
@@ -196,8 +210,9 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
} else if (e instanceof MessageToRequestEvent) {
|
||||
if (((MessageToRequestEvent) e).getContactId().equals(contactId))
|
||||
generateRequest();
|
||||
} else if (e instanceof ShutdownEvent) {
|
||||
interrupt();
|
||||
} else if (e instanceof LifecycleEvent) {
|
||||
LifecycleEvent l = (LifecycleEvent) e;
|
||||
if (l.getLifecycleState() == STOPPING) interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +274,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
try {
|
||||
b = db.generateRequestedBatch(txn, contactId,
|
||||
MAX_RECORD_PAYLOAD_LENGTH, maxLatency);
|
||||
setNextSendTime(db.getNextSendTime(txn, contactId));
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
@@ -305,6 +321,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
|
||||
try {
|
||||
o = db.generateOffer(txn, contactId, MAX_MESSAGE_IDS,
|
||||
maxLatency);
|
||||
setNextSendTime(db.getNextSendTime(txn, contactId));
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
|
||||
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Ack;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
@@ -27,6 +27,7 @@ import java.util.logging.Logger;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
|
||||
/**
|
||||
* An incoming {@link SyncSession}.
|
||||
@@ -96,8 +97,9 @@ class IncomingSession implements SyncSession, EventListener {
|
||||
if (e instanceof ContactRemovedEvent) {
|
||||
ContactRemovedEvent c = (ContactRemovedEvent) e;
|
||||
if (c.getContactId().equals(contactId)) interrupt();
|
||||
} else if (e instanceof ShutdownEvent) {
|
||||
interrupt();
|
||||
} else if (e instanceof LifecycleEvent) {
|
||||
LifecycleEvent l = (LifecycleEvent) e;
|
||||
if (l.getLifecycleState() == STOPPING) interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent;
|
||||
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Ack;
|
||||
import org.briarproject.bramble.api.sync.RecordWriter;
|
||||
@@ -28,6 +28,7 @@ import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
|
||||
|
||||
@@ -109,8 +110,9 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
|
||||
if (e instanceof ContactRemovedEvent) {
|
||||
ContactRemovedEvent c = (ContactRemovedEvent) e;
|
||||
if (c.getContactId().equals(contactId)) interrupt();
|
||||
} else if (e instanceof ShutdownEvent) {
|
||||
interrupt();
|
||||
} else if (e instanceof LifecycleEvent) {
|
||||
LifecycleEvent l = (LifecycleEvent) e;
|
||||
if (l.getLifecycleState() == STOPPING) interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,11 +71,9 @@ class ValidationManagerImpl implements ValidationManager, Service,
|
||||
@Override
|
||||
public void startService() {
|
||||
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||
for (ClientId c : validators.keySet()) {
|
||||
validateOutstandingMessagesAsync(c);
|
||||
deliverOutstandingMessagesAsync(c);
|
||||
shareOutstandingMessagesAsync(c);
|
||||
}
|
||||
validateOutstandingMessagesAsync();
|
||||
deliverOutstandingMessagesAsync();
|
||||
shareOutstandingMessagesAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,17 +91,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
|
||||
hooks.put(c, hook);
|
||||
}
|
||||
|
||||
private void validateOutstandingMessagesAsync(ClientId c) {
|
||||
dbExecutor.execute(() -> validateOutstandingMessages(c));
|
||||
private void validateOutstandingMessagesAsync() {
|
||||
dbExecutor.execute(this::validateOutstandingMessages);
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void validateOutstandingMessages(ClientId c) {
|
||||
private void validateOutstandingMessages() {
|
||||
try {
|
||||
Queue<MessageId> unvalidated = new LinkedList<>();
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
unvalidated.addAll(db.getMessagesToValidate(txn, c));
|
||||
unvalidated.addAll(db.getMessagesToValidate(txn));
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
@@ -148,17 +146,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverOutstandingMessagesAsync(ClientId c) {
|
||||
dbExecutor.execute(() -> deliverOutstandingMessages(c));
|
||||
private void deliverOutstandingMessagesAsync() {
|
||||
dbExecutor.execute(this::deliverOutstandingMessages);
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void deliverOutstandingMessages(ClientId c) {
|
||||
private void deliverOutstandingMessages() {
|
||||
try {
|
||||
Queue<MessageId> pending = new LinkedList<>();
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
pending.addAll(db.getPendingMessages(txn, c));
|
||||
pending.addAll(db.getPendingMessages(txn));
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
@@ -353,17 +351,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
|
||||
return pending;
|
||||
}
|
||||
|
||||
private void shareOutstandingMessagesAsync(ClientId c) {
|
||||
dbExecutor.execute(() -> shareOutstandingMessages(c));
|
||||
private void shareOutstandingMessagesAsync() {
|
||||
dbExecutor.execute(this::shareOutstandingMessages);
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void shareOutstandingMessages(ClientId c) {
|
||||
private void shareOutstandingMessages() {
|
||||
try {
|
||||
Queue<MessageId> toShare = new LinkedList<>();
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
toShare.addAll(db.getMessagesToShare(txn, c));
|
||||
toShare.addAll(db.getMessagesToShare(txn));
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
|
||||
@@ -4,8 +4,9 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.Scheduler;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@@ -25,7 +26,10 @@ public class SystemModule {
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
public SystemModule() {
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
// Discard tasks that are submitted during shutdown
|
||||
RejectedExecutionHandler policy =
|
||||
new ScheduledThreadPoolExecutor.DiscardPolicy();
|
||||
scheduler = new ScheduledThreadPoolExecutor(1, policy);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.briarproject.bramble.api.transport.IncomingKeys;
|
||||
import org.briarproject.bramble.api.transport.OutgoingKeys;
|
||||
import org.briarproject.bramble.api.transport.TransportKeys;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.briarproject.bramble.test.TestUtils;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.jmock.Expectations;
|
||||
@@ -56,9 +57,12 @@ import org.junit.Test;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
@@ -134,7 +138,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
int shutdownHandle = 12345;
|
||||
context.checking(new Expectations() {{
|
||||
// open()
|
||||
oneOf(database).open();
|
||||
oneOf(database).open(null);
|
||||
will(returnValue(false));
|
||||
oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
|
||||
will(returnValue(shutdownHandle));
|
||||
@@ -160,7 +164,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
ContactStatusChangedEvent.class)));
|
||||
// getContacts()
|
||||
oneOf(database).getContacts(txn);
|
||||
will(returnValue(Collections.singletonList(contact)));
|
||||
will(returnValue(singletonList(contact)));
|
||||
// addGroup()
|
||||
oneOf(database).containsGroup(txn, groupId);
|
||||
will(returnValue(false));
|
||||
@@ -171,12 +175,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
will(returnValue(true));
|
||||
// getGroups()
|
||||
oneOf(database).getGroups(txn, clientId);
|
||||
will(returnValue(Collections.singletonList(group)));
|
||||
will(returnValue(singletonList(group)));
|
||||
// removeGroup()
|
||||
oneOf(database).containsGroup(txn, groupId);
|
||||
will(returnValue(true));
|
||||
oneOf(database).getGroupVisibility(txn, groupId);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
will(returnValue(emptyMap()));
|
||||
oneOf(database).removeGroup(txn, groupId);
|
||||
oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class)));
|
||||
oneOf(eventBus).broadcast(with(any(
|
||||
@@ -199,18 +203,18 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||
shutdown);
|
||||
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
Transaction transaction = db.startTransaction(false);
|
||||
try {
|
||||
db.addLocalAuthor(transaction, localAuthor);
|
||||
assertEquals(contactId,
|
||||
db.addContact(transaction, author, localAuthorId, true,
|
||||
true));
|
||||
assertEquals(Collections.singletonList(contact),
|
||||
assertEquals(singletonList(contact),
|
||||
db.getContacts(transaction));
|
||||
db.addGroup(transaction, group); // First time - listeners called
|
||||
db.addGroup(transaction, group); // Second time - not called
|
||||
assertEquals(Collections.singletonList(group),
|
||||
assertEquals(singletonList(group),
|
||||
db.getGroups(transaction, clientId));
|
||||
db.removeGroup(transaction, group);
|
||||
db.removeContact(transaction, contactId);
|
||||
@@ -255,13 +259,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
will(returnValue(true));
|
||||
oneOf(database).containsMessage(txn, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addMessage(txn, message, DELIVERED, true);
|
||||
oneOf(database).addMessage(txn, message, DELIVERED, true, null);
|
||||
oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
|
||||
oneOf(database).getGroupVisibility(txn, groupId);
|
||||
will(returnValue(Collections.singletonList(contactId)));
|
||||
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addStatus(txn, contactId, messageId, false, false);
|
||||
oneOf(database).commitTransaction(txn);
|
||||
// The message was added, so the listeners should be called
|
||||
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
|
||||
@@ -397,7 +396,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
|
||||
transaction = db.startTransaction(false);
|
||||
try {
|
||||
Ack a = new Ack(Collections.singletonList(messageId));
|
||||
Ack a = new Ack(singletonList(messageId));
|
||||
db.receiveAck(transaction, contactId, a);
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -418,7 +417,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
|
||||
transaction = db.startTransaction(false);
|
||||
try {
|
||||
Offer o = new Offer(Collections.singletonList(messageId));
|
||||
Offer o = new Offer(singletonList(messageId));
|
||||
db.receiveOffer(transaction, contactId, o);
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -429,7 +428,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
|
||||
transaction = db.startTransaction(false);
|
||||
try {
|
||||
Request r = new Request(Collections.singletonList(messageId));
|
||||
Request r = new Request(singletonList(messageId));
|
||||
db.receiveRequest(transaction, contactId, r);
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -1022,7 +1021,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
|
||||
Transaction transaction = db.startTransaction(false);
|
||||
try {
|
||||
Ack a = new Ack(Collections.singletonList(messageId));
|
||||
Ack a = new Ack(singletonList(messageId));
|
||||
db.receiveAck(transaction, contactId, a);
|
||||
db.commitTransaction(transaction);
|
||||
} finally {
|
||||
@@ -1042,12 +1041,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
will(returnValue(VISIBLE));
|
||||
oneOf(database).containsMessage(txn, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addMessage(txn, message, UNKNOWN, false);
|
||||
oneOf(database).getGroupVisibility(txn, groupId);
|
||||
will(returnValue(Collections.singletonList(contactId)));
|
||||
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addStatus(txn, contactId, messageId, true, true);
|
||||
oneOf(database).addMessage(txn, message, UNKNOWN, false, contactId);
|
||||
// Second time
|
||||
oneOf(database).containsContact(txn, contactId);
|
||||
will(returnValue(true));
|
||||
@@ -1197,7 +1191,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
|
||||
Transaction transaction = db.startTransaction(false);
|
||||
try {
|
||||
Request r = new Request(Collections.singletonList(messageId));
|
||||
Request r = new Request(singletonList(messageId));
|
||||
db.receiveRequest(transaction, contactId, r);
|
||||
db.commitTransaction(transaction);
|
||||
} finally {
|
||||
@@ -1206,7 +1200,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangingVisibilityCallsListeners() throws Exception {
|
||||
public void testChangingVisibilityFromInvisibleToVisibleCallsListeners()
|
||||
throws Exception {
|
||||
AtomicReference<GroupVisibilityUpdatedEvent> event =
|
||||
new AtomicReference<>();
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(database).startTransaction();
|
||||
will(returnValue(txn));
|
||||
@@ -1215,16 +1213,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
oneOf(database).containsGroup(txn, groupId);
|
||||
will(returnValue(true));
|
||||
oneOf(database).getGroupVisibility(txn, contactId, groupId);
|
||||
will(returnValue(INVISIBLE)); // Not yet visible
|
||||
will(returnValue(INVISIBLE));
|
||||
oneOf(database).addGroupVisibility(txn, contactId, groupId, false);
|
||||
oneOf(database).getMessageIds(txn, groupId);
|
||||
will(returnValue(Collections.singletonList(messageId)));
|
||||
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addStatus(txn, contactId, messageId, false, false);
|
||||
oneOf(database).commitTransaction(txn);
|
||||
oneOf(eventBus).broadcast(with(any(
|
||||
GroupVisibilityUpdatedEvent.class)));
|
||||
will(new CaptureArgumentAction<>(event,
|
||||
GroupVisibilityUpdatedEvent.class, 0));
|
||||
}});
|
||||
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||
shutdown);
|
||||
@@ -1236,6 +1231,48 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
} finally {
|
||||
db.endTransaction(transaction);
|
||||
}
|
||||
|
||||
GroupVisibilityUpdatedEvent e = event.get();
|
||||
assertNotNull(e);
|
||||
assertEquals(singletonList(contactId), e.getAffectedContacts());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangingVisibilityFromVisibleToInvisibleCallsListeners()
|
||||
throws Exception {
|
||||
AtomicReference<GroupVisibilityUpdatedEvent> event =
|
||||
new AtomicReference<>();
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(database).startTransaction();
|
||||
will(returnValue(txn));
|
||||
oneOf(database).containsContact(txn, contactId);
|
||||
will(returnValue(true));
|
||||
oneOf(database).containsGroup(txn, groupId);
|
||||
will(returnValue(true));
|
||||
oneOf(database).getGroupVisibility(txn, contactId, groupId);
|
||||
will(returnValue(VISIBLE));
|
||||
oneOf(database).removeGroupVisibility(txn, contactId, groupId);
|
||||
oneOf(database).commitTransaction(txn);
|
||||
oneOf(eventBus).broadcast(with(any(
|
||||
GroupVisibilityUpdatedEvent.class)));
|
||||
will(new CaptureArgumentAction<>(event,
|
||||
GroupVisibilityUpdatedEvent.class, 0));
|
||||
}});
|
||||
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||
shutdown);
|
||||
|
||||
Transaction transaction = db.startTransaction(false);
|
||||
try {
|
||||
db.setGroupVisibility(transaction, contactId, groupId, INVISIBLE);
|
||||
db.commitTransaction(transaction);
|
||||
} finally {
|
||||
db.endTransaction(transaction);
|
||||
}
|
||||
|
||||
GroupVisibilityUpdatedEvent e = event.get();
|
||||
assertNotNull(e);
|
||||
assertEquals(singletonList(contactId), e.getAffectedContacts());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1267,8 +1304,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
@Test
|
||||
public void testTransportKeys() throws Exception {
|
||||
TransportKeys transportKeys = createTransportKeys();
|
||||
Map<ContactId, TransportKeys> keys = Collections.singletonMap(
|
||||
contactId, transportKeys);
|
||||
Map<ContactId, TransportKeys> keys =
|
||||
singletonMap(contactId, transportKeys);
|
||||
context.checking(new Expectations() {{
|
||||
// startTransaction()
|
||||
oneOf(database).startTransaction();
|
||||
@@ -1464,7 +1501,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
|
||||
context.checking(new Expectations() {{
|
||||
// open()
|
||||
oneOf(database).open();
|
||||
oneOf(database).open(null);
|
||||
will(returnValue(false));
|
||||
oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
|
||||
will(returnValue(shutdownHandle));
|
||||
@@ -1476,13 +1513,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
will(returnValue(true));
|
||||
oneOf(database).containsMessage(txn, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addMessage(txn, message, DELIVERED, true);
|
||||
oneOf(database).getGroupVisibility(txn, groupId);
|
||||
will(returnValue(Collections.singletonList(contactId)));
|
||||
oneOf(database).addMessage(txn, message, DELIVERED, true, null);
|
||||
oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
|
||||
oneOf(database).removeOfferedMessage(txn, contactId, messageId);
|
||||
will(returnValue(false));
|
||||
oneOf(database).addStatus(txn, contactId, messageId, false, false);
|
||||
// addMessageDependencies()
|
||||
oneOf(database).containsMessage(txn, messageId);
|
||||
will(returnValue(true));
|
||||
@@ -1511,7 +1543,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||
shutdown);
|
||||
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
Transaction transaction = db.startTransaction(false);
|
||||
try {
|
||||
db.addLocalMessage(transaction, message, metadata, true);
|
||||
|
||||
@@ -62,7 +62,7 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
public void testDoesNotRunMigrationsWhenCreatingDatabase()
|
||||
throws Exception {
|
||||
Database<Connection> db = createDatabase(singletonList(migration));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
db.close();
|
||||
}
|
||||
@@ -72,14 +72,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
throws Exception {
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
// Override the data schema version
|
||||
setDataSchemaVersion(db, -1);
|
||||
db.close();
|
||||
// Reopen the DB - an exception should be thrown
|
||||
db = createDatabase(asList(migration, migration1));
|
||||
db.open();
|
||||
db.open(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,12 +87,12 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
throws Exception {
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
db.close();
|
||||
// Reopen the DB - migrations should not be run
|
||||
db = createDatabase(asList(migration, migration1));
|
||||
assertTrue(db.open());
|
||||
assertTrue(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
db.close();
|
||||
}
|
||||
@@ -101,14 +101,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
public void testThrowsExceptionIfDataIsNewerThanCode() throws Exception {
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
// Override the data schema version
|
||||
setDataSchemaVersion(db, CODE_SCHEMA_VERSION + 1);
|
||||
db.close();
|
||||
// Reopen the DB - an exception should be thrown
|
||||
db = createDatabase(asList(migration, migration1));
|
||||
db.open();
|
||||
db.open(null);
|
||||
}
|
||||
|
||||
@Test(expected = DataTooOldException.class)
|
||||
@@ -116,13 +116,13 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
throws Exception {
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(emptyList());
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 1);
|
||||
db.close();
|
||||
// Reopen the DB - an exception should be thrown
|
||||
db = createDatabase(emptyList());
|
||||
db.open();
|
||||
db.open(null);
|
||||
}
|
||||
|
||||
@Test(expected = DataTooOldException.class)
|
||||
@@ -141,14 +141,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
// Override the data schema version
|
||||
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 3);
|
||||
db.close();
|
||||
// Reopen the DB - an exception should be thrown
|
||||
db = createDatabase(asList(migration, migration1));
|
||||
db.open();
|
||||
db.open(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -170,14 +170,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
// Override the data schema version
|
||||
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
|
||||
db.close();
|
||||
// Reopen the DB - the first migration should be run
|
||||
db = createDatabase(asList(migration, migration1));
|
||||
assertTrue(db.open());
|
||||
assertTrue(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
db.close();
|
||||
}
|
||||
@@ -202,14 +202,14 @@ public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
|
||||
|
||||
// Open the DB for the first time
|
||||
Database<Connection> db = createDatabase(asList(migration, migration1));
|
||||
assertFalse(db.open());
|
||||
assertFalse(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
// Override the data schema version
|
||||
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
|
||||
db.close();
|
||||
// Reopen the DB - both migrations should be run
|
||||
db = createDatabase(asList(migration, migration1));
|
||||
assertTrue(db.open());
|
||||
assertTrue(db.open(null));
|
||||
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
|
||||
db.close();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
@@ -17,6 +16,7 @@ import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.MessageStatus;
|
||||
import org.briarproject.bramble.api.sync.ValidationManager.State;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.transport.IncomingKeys;
|
||||
import org.briarproject.bramble.api.transport.OutgoingKeys;
|
||||
import org.briarproject.bramble.api.transport.TransportKeys;
|
||||
@@ -24,7 +24,6 @@ import org.briarproject.bramble.system.SystemClock;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.briarproject.bramble.test.TestDatabaseConfig;
|
||||
import org.briarproject.bramble.test.TestUtils;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -44,7 +43,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||
@@ -54,6 +52,12 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERE
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
||||
import static org.briarproject.bramble.test.TestUtils.getAuthor;
|
||||
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -73,7 +77,6 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
private final ClientId clientId;
|
||||
private final Group group;
|
||||
private final Author author;
|
||||
private final AuthorId localAuthorId;
|
||||
private final LocalAuthor localAuthor;
|
||||
private final MessageId messageId;
|
||||
private final long timestamp;
|
||||
@@ -84,19 +87,16 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
private final ContactId contactId;
|
||||
|
||||
public H2DatabaseTest() throws Exception {
|
||||
groupId = new GroupId(TestUtils.getRandomId());
|
||||
clientId = new ClientId(StringUtils.getRandomString(5));
|
||||
groupId = new GroupId(getRandomId());
|
||||
clientId = new ClientId(getRandomString(123));
|
||||
byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
|
||||
group = new Group(groupId, clientId, descriptor);
|
||||
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
|
||||
author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
|
||||
localAuthorId = new AuthorId(TestUtils.getRandomId());
|
||||
author = getAuthor();
|
||||
localAuthor = getLocalAuthor();
|
||||
messageId = new MessageId(getRandomId());
|
||||
timestamp = System.currentTimeMillis();
|
||||
localAuthor = new LocalAuthor(localAuthorId, "Bob",
|
||||
new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
|
||||
messageId = new MessageId(TestUtils.getRandomId());
|
||||
size = 1234;
|
||||
raw = TestUtils.getRandomBytes(size);
|
||||
raw = getRandomBytes(size);
|
||||
message = new Message(messageId, groupId, timestamp, raw);
|
||||
transportId = new TransportId("id");
|
||||
contactId = new ContactId(1);
|
||||
@@ -114,14 +114,14 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
Connection txn = db.startTransaction();
|
||||
assertFalse(db.containsContact(txn, contactId));
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
assertTrue(db.containsContact(txn, contactId));
|
||||
assertFalse(db.containsGroup(txn, groupId));
|
||||
db.addGroup(txn, group);
|
||||
assertTrue(db.containsGroup(txn, groupId));
|
||||
assertFalse(db.containsMessage(txn, messageId));
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
assertTrue(db.containsMessage(txn, messageId));
|
||||
db.commitTransaction(txn);
|
||||
db.close();
|
||||
@@ -159,7 +159,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and a message
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// Removing the group should remove the message
|
||||
assertTrue(db.containsMessage(txn, messageId));
|
||||
@@ -177,22 +177,15 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and a shared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// The message has no status yet, so it should not be sendable
|
||||
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
|
||||
ONE_MEGABYTE);
|
||||
assertTrue(ids.isEmpty());
|
||||
ids = db.getMessagesToOffer(txn, contactId, 100);
|
||||
assertTrue(ids.isEmpty());
|
||||
|
||||
// Adding a status with seen = false should make the message sendable
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
|
||||
// The contact has not seen the message, so it should be sendable
|
||||
Collection<MessageId> ids =
|
||||
db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
|
||||
assertEquals(Collections.singletonList(messageId), ids);
|
||||
ids = db.getMessagesToOffer(txn, contactId, 100);
|
||||
assertEquals(Collections.singletonList(messageId), ids);
|
||||
@@ -215,12 +208,11 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and a shared but unvalidated message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, UNKNOWN, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, UNKNOWN, true, null);
|
||||
|
||||
// The message has not been validated, so it should not be sendable
|
||||
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
|
||||
@@ -261,11 +253,10 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, an invisible group and a shared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// The group is invisible, so the message should not be sendable
|
||||
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
|
||||
@@ -313,12 +304,11 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and an unshared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, DELIVERED, false);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, false, null);
|
||||
|
||||
// The message is not shared, so it should not be sendable
|
||||
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
|
||||
@@ -345,12 +335,11 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and a shared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// The message is sendable, but too large to send
|
||||
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
|
||||
@@ -372,20 +361,16 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact and a visible group
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, false);
|
||||
|
||||
// Add some messages to ack
|
||||
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId1 = new MessageId(getRandomId());
|
||||
Message message1 = new Message(messageId1, groupId, timestamp, raw);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, true);
|
||||
db.raiseAckFlag(txn, contactId, messageId);
|
||||
db.addMessage(txn, message1, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId1, false, true);
|
||||
db.raiseAckFlag(txn, contactId, messageId1);
|
||||
db.addMessage(txn, message, DELIVERED, true, contactId);
|
||||
db.addMessage(txn, message1, DELIVERED, true, contactId);
|
||||
|
||||
// Both message IDs should be returned
|
||||
Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
|
||||
@@ -398,6 +383,14 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
|
||||
contactId, 1234));
|
||||
|
||||
// Raise the ack flag again
|
||||
db.raiseAckFlag(txn, contactId, messageId);
|
||||
db.raiseAckFlag(txn, contactId, messageId1);
|
||||
|
||||
// Both message IDs should be returned
|
||||
ids = db.getMessagesToAck(txn, contactId, 1234);
|
||||
assertEquals(Arrays.asList(messageId, messageId1), ids);
|
||||
|
||||
db.commitTransaction(txn);
|
||||
db.close();
|
||||
}
|
||||
@@ -409,12 +402,11 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and a shared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// Retrieve the message from the database and mark it as sent
|
||||
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
|
||||
@@ -455,7 +447,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
// Storing a message should reduce the free space
|
||||
Connection txn = db.startTransaction();
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
db.commitTransaction(txn);
|
||||
assertTrue(db.getFreeSpace() < free);
|
||||
|
||||
@@ -567,7 +559,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact and a shared group
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
@@ -587,7 +579,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
|
||||
// The group is not in the database
|
||||
@@ -603,15 +595,14 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
Database<Connection> db = open(false);
|
||||
Connection txn = db.startTransaction();
|
||||
|
||||
// Add a contact, a group and a message
|
||||
// Add a contact, an invisible group and a message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// The group is not visible
|
||||
// The group is not visible so the message should not be visible
|
||||
assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
|
||||
|
||||
db.commitTransaction(txn);
|
||||
@@ -625,37 +616,37 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact and a group
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
|
||||
// The group should not be visible to the contact
|
||||
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||
assertEquals(Collections.emptyList(),
|
||||
assertEquals(Collections.emptyMap(),
|
||||
db.getGroupVisibility(txn, groupId));
|
||||
|
||||
// Make the group visible to the contact
|
||||
db.addGroupVisibility(txn, contactId, groupId, false);
|
||||
assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||
assertEquals(Collections.singletonList(contactId),
|
||||
assertEquals(Collections.singletonMap(contactId, false),
|
||||
db.getGroupVisibility(txn, groupId));
|
||||
|
||||
// Share the group with the contact
|
||||
db.setGroupVisibility(txn, contactId, groupId, true);
|
||||
assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
|
||||
assertEquals(Collections.singletonList(contactId),
|
||||
assertEquals(Collections.singletonMap(contactId, true),
|
||||
db.getGroupVisibility(txn, groupId));
|
||||
|
||||
// Unshare the group with the contact
|
||||
db.setGroupVisibility(txn, contactId, groupId, false);
|
||||
assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||
assertEquals(Collections.singletonList(contactId),
|
||||
assertEquals(Collections.singletonMap(contactId, false),
|
||||
db.getGroupVisibility(txn, groupId));
|
||||
|
||||
// Make the group invisible again
|
||||
db.removeGroupVisibility(txn, contactId, groupId);
|
||||
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||
assertEquals(Collections.emptyList(),
|
||||
assertEquals(Collections.emptyMap(),
|
||||
db.getGroupVisibility(txn, groupId));
|
||||
|
||||
db.commitTransaction(txn);
|
||||
@@ -675,7 +666,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add the contact, the transport and the transport keys
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addTransport(txn, transportId, 123);
|
||||
db.addTransportKeys(txn, contactId, keys);
|
||||
@@ -737,7 +728,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add the contact, transport and transport keys
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addTransport(txn, transportId, 123);
|
||||
db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
|
||||
@@ -773,7 +764,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add the contact, transport and transport keys
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addTransport(txn, transportId, 123);
|
||||
db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
|
||||
@@ -808,7 +799,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
|
||||
// Add a contact associated with the local author
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
|
||||
// Ensure contact is returned from database by Author ID
|
||||
@@ -833,18 +824,19 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a local author - no contacts should be associated
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
|
||||
Collection<ContactId> contacts =
|
||||
db.getContacts(txn, localAuthor.getId());
|
||||
assertEquals(Collections.emptyList(), contacts);
|
||||
|
||||
// Add a contact associated with the local author
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
contacts = db.getContacts(txn, localAuthorId);
|
||||
contacts = db.getContacts(txn, localAuthor.getId());
|
||||
assertEquals(Collections.singletonList(contactId), contacts);
|
||||
|
||||
// Remove the local author - the contact should be removed
|
||||
db.removeLocalAuthor(txn, localAuthorId);
|
||||
contacts = db.getContacts(txn, localAuthorId);
|
||||
db.removeLocalAuthor(txn, localAuthor.getId());
|
||||
contacts = db.getContacts(txn, localAuthor.getId());
|
||||
assertEquals(Collections.emptyList(), contacts);
|
||||
assertFalse(db.containsContact(txn, contactId));
|
||||
|
||||
@@ -859,14 +851,14 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact - initially there should be no offered messages
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
assertEquals(0, db.countOfferedMessages(txn, contactId));
|
||||
|
||||
// Add some offered messages and count them
|
||||
List<MessageId> ids = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MessageId m = new MessageId(TestUtils.getRandomId());
|
||||
MessageId m = new MessageId(getRandomId());
|
||||
db.addOfferedMessage(txn, contactId, m);
|
||||
ids.add(m);
|
||||
}
|
||||
@@ -875,8 +867,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
// Remove some of the offered messages and count again
|
||||
List<MessageId> half = ids.subList(0, 5);
|
||||
db.removeOfferedMessages(txn, contactId, half);
|
||||
assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
|
||||
assertEquals(4, db.countOfferedMessages(txn, contactId));
|
||||
assertEquals(5, db.countOfferedMessages(txn, contactId));
|
||||
|
||||
db.commitTransaction(txn);
|
||||
db.close();
|
||||
@@ -927,7 +918,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and a message
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// Attach some metadata to the message
|
||||
Metadata metadata = new Metadata();
|
||||
@@ -998,7 +989,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and a message
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// Attach some metadata to the message
|
||||
Metadata metadata = new Metadata();
|
||||
@@ -1051,7 +1042,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
@Test
|
||||
public void testMetadataQueries() throws Exception {
|
||||
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId1 = new MessageId(getRandomId());
|
||||
Message message1 = new Message(messageId1, groupId, timestamp, raw);
|
||||
|
||||
Database<Connection> db = open(false);
|
||||
@@ -1059,8 +1050,8 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and two messages
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message1, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
db.addMessage(txn, message1, DELIVERED, true, null);
|
||||
|
||||
// Attach some metadata to the messages
|
||||
Metadata metadata = new Metadata();
|
||||
@@ -1155,7 +1146,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
@Test
|
||||
public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception {
|
||||
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId1 = new MessageId(getRandomId());
|
||||
Message message1 = new Message(messageId1, groupId, timestamp, raw);
|
||||
|
||||
Database<Connection> db = open(false);
|
||||
@@ -1163,8 +1154,8 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and two messages
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addMessage(txn, message1, DELIVERED, true);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
db.addMessage(txn, message1, DELIVERED, true, null);
|
||||
|
||||
// Attach some metadata to the messages
|
||||
Metadata metadata = new Metadata();
|
||||
@@ -1226,10 +1217,10 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
@Test
|
||||
public void testMessageDependencies() throws Exception {
|
||||
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId4 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId1 = new MessageId(getRandomId());
|
||||
MessageId messageId2 = new MessageId(getRandomId());
|
||||
MessageId messageId3 = new MessageId(getRandomId());
|
||||
MessageId messageId4 = new MessageId(getRandomId());
|
||||
Message message1 = new Message(messageId1, groupId, timestamp, raw);
|
||||
Message message2 = new Message(messageId2, groupId, timestamp, raw);
|
||||
|
||||
@@ -1238,9 +1229,9 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and some messages
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, PENDING, true);
|
||||
db.addMessage(txn, message1, DELIVERED, true);
|
||||
db.addMessage(txn, message2, INVALID, true);
|
||||
db.addMessage(txn, message, PENDING, true, contactId);
|
||||
db.addMessage(txn, message1, DELIVERED, true, contactId);
|
||||
db.addMessage(txn, message2, INVALID, true, contactId);
|
||||
|
||||
// Add dependencies
|
||||
db.addMessageDependency(txn, groupId, messageId, messageId1);
|
||||
@@ -1307,26 +1298,26 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and a message
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, PENDING, true);
|
||||
db.addMessage(txn, message, PENDING, true, contactId);
|
||||
|
||||
// Add a second group
|
||||
GroupId groupId1 = new GroupId(TestUtils.getRandomId());
|
||||
GroupId groupId1 = new GroupId(getRandomId());
|
||||
Group group1 = new Group(groupId1, clientId,
|
||||
TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
|
||||
getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
|
||||
db.addGroup(txn, group1);
|
||||
|
||||
// Add a message to the second group
|
||||
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId1 = new MessageId(getRandomId());
|
||||
Message message1 = new Message(messageId1, groupId1, timestamp, raw);
|
||||
db.addMessage(txn, message1, DELIVERED, true);
|
||||
db.addMessage(txn, message1, DELIVERED, true, contactId);
|
||||
|
||||
// Create an ID for a missing message
|
||||
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId2 = new MessageId(getRandomId());
|
||||
|
||||
// Add another message to the first group
|
||||
MessageId messageId3 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId messageId3 = new MessageId(getRandomId());
|
||||
Message message3 = new Message(messageId3, groupId, timestamp, raw);
|
||||
db.addMessage(txn, message3, DELIVERED, true);
|
||||
db.addMessage(txn, message3, DELIVERED, true, contactId);
|
||||
|
||||
// Add dependencies between the messages
|
||||
db.addMessageDependency(txn, groupId, messageId, messageId1);
|
||||
@@ -1359,10 +1350,10 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
@Test
|
||||
public void testGetPendingMessagesForDelivery() throws Exception {
|
||||
MessageId mId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId2 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId3 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId4 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId1 = new MessageId(getRandomId());
|
||||
MessageId mId2 = new MessageId(getRandomId());
|
||||
MessageId mId3 = new MessageId(getRandomId());
|
||||
MessageId mId4 = new MessageId(getRandomId());
|
||||
Message m1 = new Message(mId1, groupId, timestamp, raw);
|
||||
Message m2 = new Message(mId2, groupId, timestamp, raw);
|
||||
Message m3 = new Message(mId3, groupId, timestamp, raw);
|
||||
@@ -1373,20 +1364,20 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and some messages with different states
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, m1, UNKNOWN, true);
|
||||
db.addMessage(txn, m2, INVALID, true);
|
||||
db.addMessage(txn, m3, PENDING, true);
|
||||
db.addMessage(txn, m4, DELIVERED, true);
|
||||
db.addMessage(txn, m1, UNKNOWN, true, contactId);
|
||||
db.addMessage(txn, m2, INVALID, true, contactId);
|
||||
db.addMessage(txn, m3, PENDING, true, contactId);
|
||||
db.addMessage(txn, m4, DELIVERED, true, contactId);
|
||||
|
||||
Collection<MessageId> result;
|
||||
|
||||
// Retrieve messages to be validated
|
||||
result = db.getMessagesToValidate(txn, clientId);
|
||||
result = db.getMessagesToValidate(txn);
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(result.contains(mId1));
|
||||
|
||||
// Retrieve pending messages
|
||||
result = db.getPendingMessages(txn, clientId);
|
||||
result = db.getPendingMessages(txn);
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(result.contains(mId3));
|
||||
|
||||
@@ -1396,10 +1387,10 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
@Test
|
||||
public void testGetMessagesToShare() throws Exception {
|
||||
MessageId mId1 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId2 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId3 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId4 = new MessageId(TestUtils.getRandomId());
|
||||
MessageId mId1 = new MessageId(getRandomId());
|
||||
MessageId mId2 = new MessageId(getRandomId());
|
||||
MessageId mId3 = new MessageId(getRandomId());
|
||||
MessageId mId4 = new MessageId(getRandomId());
|
||||
Message m1 = new Message(mId1, groupId, timestamp, raw);
|
||||
Message m2 = new Message(mId2, groupId, timestamp, raw);
|
||||
Message m3 = new Message(mId3, groupId, timestamp, raw);
|
||||
@@ -1410,10 +1401,10 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and some messages
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, m1, DELIVERED, true);
|
||||
db.addMessage(txn, m2, DELIVERED, false);
|
||||
db.addMessage(txn, m3, DELIVERED, false);
|
||||
db.addMessage(txn, m4, DELIVERED, true);
|
||||
db.addMessage(txn, m1, DELIVERED, true, contactId);
|
||||
db.addMessage(txn, m2, DELIVERED, false, contactId);
|
||||
db.addMessage(txn, m3, DELIVERED, false, contactId);
|
||||
db.addMessage(txn, m4, DELIVERED, true, contactId);
|
||||
|
||||
// Introduce dependencies between the messages
|
||||
db.addMessageDependency(txn, groupId, mId1, mId2);
|
||||
@@ -1421,8 +1412,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
db.addMessageDependency(txn, groupId, mId4, mId3);
|
||||
|
||||
// Retrieve messages to be shared
|
||||
Collection<MessageId> result =
|
||||
db.getMessagesToShare(txn, clientId);
|
||||
Collection<MessageId> result = db.getMessagesToShare(txn);
|
||||
assertEquals(2, result.size());
|
||||
assertTrue(result.contains(mId2));
|
||||
assertTrue(result.contains(mId3));
|
||||
@@ -1438,12 +1428,11 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and a shared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// The message should not be sent or seen
|
||||
MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
|
||||
@@ -1507,9 +1496,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
@Test
|
||||
public void testDifferentLocalAuthorsCanHaveTheSameContact()
|
||||
throws Exception {
|
||||
AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId());
|
||||
LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol",
|
||||
new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
|
||||
LocalAuthor localAuthor1 = getLocalAuthor();
|
||||
|
||||
Database<Connection> db = open(false);
|
||||
Connection txn = db.startTransaction();
|
||||
@@ -1520,15 +1507,15 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add the same contact for each local author
|
||||
ContactId contactId =
|
||||
db.addContact(txn, author, localAuthorId, true, true);
|
||||
db.addContact(txn, author, localAuthor.getId(), true, true);
|
||||
ContactId contactId1 =
|
||||
db.addContact(txn, author, localAuthorId1, true, true);
|
||||
db.addContact(txn, author, localAuthor1.getId(), true, true);
|
||||
|
||||
// The contacts should be distinct
|
||||
assertNotEquals(contactId, contactId1);
|
||||
assertEquals(2, db.getContacts(txn).size());
|
||||
assertEquals(1, db.getContacts(txn, localAuthorId).size());
|
||||
assertEquals(1, db.getContacts(txn, localAuthorId1).size());
|
||||
assertEquals(1, db.getContacts(txn, localAuthor.getId()).size());
|
||||
assertEquals(1, db.getContacts(txn, localAuthor1.getId()).size());
|
||||
|
||||
db.commitTransaction(txn);
|
||||
db.close();
|
||||
@@ -1541,12 +1528,11 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact, a shared group and a shared message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
db.addMessage(txn, message, DELIVERED, true);
|
||||
db.addStatus(txn, contactId, messageId, false, false);
|
||||
db.addMessage(txn, message, DELIVERED, true, null);
|
||||
|
||||
// The message should be visible to the contact
|
||||
assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
|
||||
@@ -1587,7 +1573,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a contact
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthorId,
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
|
||||
// The contact should be active
|
||||
@@ -1620,7 +1606,7 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
|
||||
// Add a group and a message
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, UNKNOWN, false);
|
||||
db.addMessage(txn, message, UNKNOWN, false, contactId);
|
||||
|
||||
// Walk the message through the validation and delivery states
|
||||
assertEquals(UNKNOWN, db.getMessageState(txn, messageId));
|
||||
@@ -1635,6 +1621,56 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
db.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextSendTime() throws Exception {
|
||||
long now = System.currentTimeMillis();
|
||||
Database<Connection> db = open(false, new StoppedClock(now));
|
||||
Connection txn = db.startTransaction();
|
||||
|
||||
// Add a contact, a group and a message
|
||||
db.addLocalAuthor(txn, localAuthor);
|
||||
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
|
||||
true, true));
|
||||
db.addGroup(txn, group);
|
||||
db.addMessage(txn, message, UNKNOWN, false, null);
|
||||
|
||||
// There should be no messages to send
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Share the group with the contact - still no messages to send
|
||||
db.addGroupVisibility(txn, contactId, groupId, true);
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Set the message's state to DELIVERED - still no messages to send
|
||||
db.setMessageState(txn, messageId, DELIVERED);
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Share the message - now it should be sendable immediately
|
||||
db.setMessageShared(txn, messageId);
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Mark the message as requested - it should still be sendable
|
||||
db.raiseRequestedFlag(txn, contactId, messageId);
|
||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Update the message's expiry time as though we sent it - now the
|
||||
// message should be sendable after one round-trip
|
||||
db.updateExpiryTime(txn, contactId, messageId, 1000);
|
||||
assertEquals(now + 2000, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Update the message's expiry time again - now it should be sendable
|
||||
// after two round-trips
|
||||
db.updateExpiryTime(txn, contactId, messageId, 1000);
|
||||
assertEquals(now + 4000, db.getNextSendTime(txn, contactId));
|
||||
|
||||
// Delete the message - there should be no messages to send
|
||||
db.deleteMessage(txn, messageId);
|
||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||
|
||||
db.commitTransaction(txn);
|
||||
db.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionHandling() throws Exception {
|
||||
Database<Connection> db = open(false);
|
||||
@@ -1652,28 +1688,33 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
private Database<Connection> open(boolean resume) throws Exception {
|
||||
return open(resume, new SystemClock());
|
||||
}
|
||||
|
||||
private Database<Connection> open(boolean resume, Clock clock)
|
||||
throws Exception {
|
||||
Database<Connection> db = new H2Database(new TestDatabaseConfig(testDir,
|
||||
MAX_SIZE), new SystemClock());
|
||||
MAX_SIZE), clock);
|
||||
if (!resume) TestUtils.deleteTestDirectory(testDir);
|
||||
db.open();
|
||||
db.open(null);
|
||||
return db;
|
||||
}
|
||||
|
||||
private TransportKeys createTransportKeys() {
|
||||
SecretKey inPrevTagKey = TestUtils.getSecretKey();
|
||||
SecretKey inPrevHeaderKey = TestUtils.getSecretKey();
|
||||
SecretKey inPrevTagKey = getSecretKey();
|
||||
SecretKey inPrevHeaderKey = getSecretKey();
|
||||
IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
|
||||
1, 123, new byte[4]);
|
||||
SecretKey inCurrTagKey = TestUtils.getSecretKey();
|
||||
SecretKey inCurrHeaderKey = TestUtils.getSecretKey();
|
||||
SecretKey inCurrTagKey = getSecretKey();
|
||||
SecretKey inCurrHeaderKey = getSecretKey();
|
||||
IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
|
||||
2, 234, new byte[4]);
|
||||
SecretKey inNextTagKey = TestUtils.getSecretKey();
|
||||
SecretKey inNextHeaderKey = TestUtils.getSecretKey();
|
||||
SecretKey inNextTagKey = getSecretKey();
|
||||
SecretKey inNextHeaderKey = getSecretKey();
|
||||
IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
|
||||
3, 345, new byte[4]);
|
||||
SecretKey outCurrTagKey = TestUtils.getSecretKey();
|
||||
SecretKey outCurrHeaderKey = TestUtils.getSecretKey();
|
||||
SecretKey outCurrTagKey = getSecretKey();
|
||||
SecretKey outCurrHeaderKey = getSecretKey();
|
||||
OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
|
||||
2, 456);
|
||||
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
|
||||
@@ -1683,4 +1724,23 @@ public class H2DatabaseTest extends BrambleTestCase {
|
||||
public void tearDown() {
|
||||
TestUtils.deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
private static class StoppedClock implements Clock {
|
||||
|
||||
private final long time;
|
||||
|
||||
private StoppedClock(long time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long currentTimeMillis() {
|
||||
return time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sleep(long milliseconds) throws InterruptedException {
|
||||
Thread.sleep(milliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import static junit.framework.TestCase.assertNotNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
@@ -34,7 +35,7 @@ public class Migration30_31Test extends BrambleTestCase {
|
||||
|
||||
private static final String CREATE_GROUPS_STUB =
|
||||
"CREATE TABLE groups"
|
||||
+ " (groupID BINARY(32) NOT NULL,"
|
||||
+ " (groupId BINARY(32) NOT NULL,"
|
||||
+ " PRIMARY KEY (groupId))";
|
||||
|
||||
private static final String CREATE_MESSAGES =
|
||||
@@ -66,8 +67,8 @@ public class Migration30_31Test extends BrambleTestCase {
|
||||
private final String url = "jdbc:h2:" + db.getAbsolutePath();
|
||||
private final GroupId groupId = new GroupId(getRandomId());
|
||||
private final GroupId groupId1 = new GroupId(getRandomId());
|
||||
private final Message message = TestUtils.getMessage(groupId);
|
||||
private final Message message1 = TestUtils.getMessage(groupId1);
|
||||
private final Message message = getMessage(groupId);
|
||||
private final Message message1 = getMessage(groupId1);
|
||||
private final Metadata meta = new Metadata(), meta1 = new Metadata();
|
||||
|
||||
private Connection connection = null;
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
package org.briarproject.bramble.db;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.ValidationManager.State;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.briarproject.bramble.test.TestUtils;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import static java.sql.Types.BINARY;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
|
||||
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class Migration31_32Test extends BrambleTestCase {
|
||||
|
||||
private static final String CREATE_GROUPS_STUB =
|
||||
"CREATE TABLE groups"
|
||||
+ " (groupId BINARY(32) NOT NULL,"
|
||||
+ " PRIMARY KEY (groupId))";
|
||||
|
||||
private static final String CREATE_CONTACTS_STUB =
|
||||
"CREATE TABLE contacts"
|
||||
+ " (contactId INT NOT NULL,"
|
||||
+ " PRIMARY KEY (contactId))";
|
||||
|
||||
private static final String CREATE_GROUP_VISIBILITIES_STUB =
|
||||
"CREATE TABLE groupVisibilities"
|
||||
+ " (contactId INT NOT NULL,"
|
||||
+ " groupId BINARY(32) NOT NULL,"
|
||||
+ " shared BOOLEAN NOT NULL,"
|
||||
+ " PRIMARY KEY (contactId, groupId))";
|
||||
|
||||
private static final String CREATE_MESSAGES =
|
||||
"CREATE TABLE messages"
|
||||
+ " (messageId BINARY(32) NOT NULL,"
|
||||
+ " groupId BINARY(32) NOT NULL,"
|
||||
+ " timestamp BIGINT NOT NULL,"
|
||||
+ " state INT NOT NULL,"
|
||||
+ " shared BOOLEAN NOT NULL,"
|
||||
+ " length INT NOT NULL,"
|
||||
+ " raw BLOB," // Null if message has been deleted
|
||||
+ " PRIMARY KEY (messageId),"
|
||||
+ " FOREIGN KEY (groupId)"
|
||||
+ " REFERENCES groups (groupId)"
|
||||
+ " ON DELETE CASCADE)";
|
||||
|
||||
private static final String CREATE_STATUSES_31 =
|
||||
"CREATE TABLE statuses"
|
||||
+ " (messageId BINARY(32) NOT NULL,"
|
||||
+ " contactId INT NOT NULL,"
|
||||
+ " ack BOOLEAN NOT NULL,"
|
||||
+ " seen BOOLEAN NOT NULL,"
|
||||
+ " requested BOOLEAN NOT NULL,"
|
||||
+ " expiry BIGINT NOT NULL,"
|
||||
+ " txCount INT NOT NULL,"
|
||||
+ " PRIMARY KEY (messageId, contactId),"
|
||||
+ " FOREIGN KEY (messageId)"
|
||||
+ " REFERENCES messages (messageId)"
|
||||
+ " ON DELETE CASCADE,"
|
||||
+ " FOREIGN KEY (contactId)"
|
||||
+ " REFERENCES contacts (contactId)"
|
||||
+ " ON DELETE CASCADE)";
|
||||
|
||||
private final File testDir = TestUtils.getTestDirectory();
|
||||
private final File db = new File(testDir, "db");
|
||||
private final String url = "jdbc:h2:" + db.getAbsolutePath();
|
||||
private final GroupId groupId = new GroupId(getRandomId());
|
||||
private final GroupId groupId1 = new GroupId(getRandomId());
|
||||
private final ContactId contactId = new ContactId(123);
|
||||
private final ContactId contactId1 = new ContactId(234);
|
||||
private final Message message = getMessage(groupId);
|
||||
private final Message message1 = getMessage(groupId1);
|
||||
private final Message message2 = getMessage(groupId1);
|
||||
|
||||
private Connection connection = null;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
assertTrue(testDir.mkdirs());
|
||||
Class.forName("org.h2.Driver");
|
||||
connection = DriverManager.getConnection(url);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
if (connection != null) connection.close();
|
||||
TestUtils.deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMigration() throws Exception {
|
||||
try {
|
||||
Statement s = connection.createStatement();
|
||||
s.execute(CREATE_GROUPS_STUB);
|
||||
s.execute(CREATE_CONTACTS_STUB);
|
||||
s.execute(CREATE_GROUP_VISIBILITIES_STUB);
|
||||
s.execute(CREATE_MESSAGES);
|
||||
s.execute(CREATE_STATUSES_31);
|
||||
s.close();
|
||||
|
||||
addGroup(groupId);
|
||||
addMessage(message, DELIVERED, true, false);
|
||||
|
||||
addGroup(groupId1);
|
||||
addMessage(message1, UNKNOWN, false, false);
|
||||
addMessage(message2, DELIVERED, true, true);
|
||||
|
||||
addContact(contactId);
|
||||
|
||||
addGroupVisibility(contactId, groupId, true);
|
||||
addStatus31(message.getId(), contactId);
|
||||
|
||||
addGroupVisibility(contactId, groupId1, false);
|
||||
addStatus31(message1.getId(), contactId);
|
||||
addStatus31(message2.getId(), contactId);
|
||||
|
||||
addContact(contactId1);
|
||||
|
||||
addGroupVisibility(contactId1, groupId1, true);
|
||||
addStatus31(message1.getId(), contactId1);
|
||||
addStatus31(message2.getId(), contactId1);
|
||||
|
||||
new Migration31_32().migrate(connection);
|
||||
|
||||
assertTrue(containsStatus(message.getId(), contactId));
|
||||
Status32 status = getStatus32(message.getId(), contactId);
|
||||
assertEquals(groupId, status.groupId);
|
||||
assertEquals(message.getTimestamp(), status.timestamp);
|
||||
assertEquals(message.getLength(), status.length);
|
||||
assertEquals(DELIVERED, status.state);
|
||||
assertTrue(status.groupShared);
|
||||
assertTrue(status.messageShared);
|
||||
assertFalse(status.deleted);
|
||||
|
||||
assertTrue(containsStatus(message1.getId(), contactId));
|
||||
status = getStatus32(message1.getId(), contactId);
|
||||
assertEquals(groupId1, status.groupId);
|
||||
assertEquals(message1.getTimestamp(), status.timestamp);
|
||||
assertEquals(message1.getLength(), status.length);
|
||||
assertEquals(UNKNOWN, status.state);
|
||||
assertFalse(status.groupShared);
|
||||
assertFalse(status.messageShared);
|
||||
assertFalse(status.deleted);
|
||||
|
||||
assertTrue(containsStatus(message2.getId(), contactId));
|
||||
status = getStatus32(message2.getId(), contactId);
|
||||
assertEquals(groupId1, status.groupId);
|
||||
assertEquals(message2.getTimestamp(), status.timestamp);
|
||||
assertEquals(message2.getLength(), status.length);
|
||||
assertEquals(DELIVERED, status.state);
|
||||
assertFalse(status.groupShared);
|
||||
assertTrue(status.messageShared);
|
||||
assertTrue(status.deleted);
|
||||
|
||||
assertFalse(containsStatus(message.getId(), contactId1));
|
||||
|
||||
assertTrue(containsStatus(message1.getId(), contactId1));
|
||||
status = getStatus32(message1.getId(), contactId1);
|
||||
assertEquals(groupId1, status.groupId);
|
||||
assertEquals(message1.getTimestamp(), status.timestamp);
|
||||
assertEquals(message1.getLength(), status.length);
|
||||
assertEquals(UNKNOWN, status.state);
|
||||
assertTrue(status.groupShared);
|
||||
assertFalse(status.messageShared);
|
||||
assertFalse(status.deleted);
|
||||
|
||||
assertTrue(containsStatus(message2.getId(), contactId1));
|
||||
status = getStatus32(message2.getId(), contactId1);
|
||||
assertEquals(groupId1, status.groupId);
|
||||
assertEquals(message2.getTimestamp(), status.timestamp);
|
||||
assertEquals(message2.getLength(), status.length);
|
||||
assertEquals(DELIVERED, status.state);
|
||||
assertTrue(status.groupShared);
|
||||
assertTrue(status.messageShared);
|
||||
assertTrue(status.deleted);
|
||||
} catch (SQLException e) {
|
||||
connection.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void addGroup(GroupId g) throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO groups (groupId) VALUES (?)";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setBytes(1, g.getBytes());
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void addContact(ContactId c) throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO contacts (contactId) VALUES (?)";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void addGroupVisibility(ContactId c, GroupId g, boolean shared)
|
||||
throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO groupVisibilities"
|
||||
+ " (contactId, groupId, shared) VALUES (?, ?, ?)";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setInt(1, c.getInt());
|
||||
ps.setBytes(2, g.getBytes());
|
||||
ps.setBoolean(3, shared);
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void addMessage(Message m, State state, boolean shared,
|
||||
boolean deleted) throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
|
||||
+ " state, shared, length, raw)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getId().getBytes());
|
||||
ps.setBytes(2, m.getGroupId().getBytes());
|
||||
ps.setLong(3, m.getTimestamp());
|
||||
ps.setInt(4, state.getValue());
|
||||
ps.setBoolean(5, shared);
|
||||
byte[] raw = m.getRaw();
|
||||
ps.setInt(6, raw.length);
|
||||
if (deleted) ps.setNull(7, BINARY);
|
||||
else ps.setBytes(7, raw);
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void addStatus31(MessageId m, ContactId c) throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
try {
|
||||
String sql = "INSERT INTO statuses (messageId, contactId, ack,"
|
||||
+ " seen, requested, expiry, txCount)"
|
||||
+ " VALUES (?, ?, FALSE, FALSE, FALSE, 0, 0)";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected != 1) throw new DbStateException();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsStatus(MessageId m, ContactId c)
|
||||
throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT COUNT (*) FROM statuses"
|
||||
+ " WHERE messageId = ? AND contactId = ?";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
rs = ps.executeQuery();
|
||||
if (!rs.next()) throw new DbStateException();
|
||||
int count = rs.getInt(1);
|
||||
if (count < 0 || count > 1) throw new DbStateException();
|
||||
if (rs.next()) throw new DbStateException();
|
||||
rs.close();
|
||||
ps.close();
|
||||
return count > 0;
|
||||
} catch (SQLException e) {
|
||||
if (rs != null) rs.close();
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private Status32 getStatus32(MessageId m, ContactId c) throws SQLException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT groupId, timestamp, length, state,"
|
||||
+ " groupShared, messageShared, deleted"
|
||||
+ " FROM statuses"
|
||||
+ " WHERE messageId = ? AND contactId = ?";
|
||||
ps = connection.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
ps.setInt(2, c.getInt());
|
||||
rs = ps.executeQuery();
|
||||
if (!rs.next()) throw new DbStateException();
|
||||
GroupId groupId = new GroupId(rs.getBytes(1));
|
||||
long timestamp = rs.getLong(2);
|
||||
int length = rs.getInt(3);
|
||||
State state = State.fromValue(rs.getInt(4));
|
||||
boolean groupShared = rs.getBoolean(5);
|
||||
boolean messageShared = rs.getBoolean(6);
|
||||
boolean deleted = rs.getBoolean(7);
|
||||
if (rs.next()) throw new DbStateException();
|
||||
rs.close();
|
||||
ps.close();
|
||||
return new Status32(groupId, timestamp, length, state,
|
||||
groupShared, messageShared, deleted);
|
||||
} catch (SQLException e) {
|
||||
if (rs != null) rs.close();
|
||||
if (ps != null) ps.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Status32 {
|
||||
|
||||
private final GroupId groupId;
|
||||
private final long timestamp;
|
||||
private final int length;
|
||||
private final State state;
|
||||
private final boolean groupShared, messageShared, deleted;
|
||||
|
||||
private Status32(GroupId groupId, long timestamp, int length,
|
||||
State state, boolean groupShared, boolean messageShared,
|
||||
boolean deleted) {
|
||||
this.groupId = groupId;
|
||||
this.timestamp = timestamp;
|
||||
this.length = length;
|
||||
this.state = state;
|
||||
this.groupShared = groupShared;
|
||||
this.messageShared = messageShared;
|
||||
this.deleted = deleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.bramble.plugin.tcp;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
@@ -26,11 +25,9 @@ import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
@@ -198,9 +195,16 @@ public class LanTcpPluginTest extends BrambleTestCase {
|
||||
KeyAgreementListener kal =
|
||||
plugin.createKeyAgreementListener(new byte[COMMIT_LENGTH]);
|
||||
assertNotNull(kal);
|
||||
Callable<KeyAgreementConnection> c = kal.listen();
|
||||
FutureTask<KeyAgreementConnection> f = new FutureTask<>(c);
|
||||
new Thread(f).start();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicBoolean error = new AtomicBoolean(false);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
kal.accept();
|
||||
latch.countDown();
|
||||
} catch (IOException e) {
|
||||
error.set(true);
|
||||
}
|
||||
}).start();
|
||||
// The plugin should have bound a socket and stored the port number
|
||||
BdfList descriptor = kal.getDescriptor();
|
||||
assertEquals(3, descriptor.size());
|
||||
@@ -216,10 +220,12 @@ public class LanTcpPluginTest extends BrambleTestCase {
|
||||
InetSocketAddress socketAddr = new InetSocketAddress(addr, port);
|
||||
Socket s = new Socket();
|
||||
s.connect(socketAddr, 100);
|
||||
assertNotNull(f.get(5, SECONDS));
|
||||
// Check that the connection was accepted
|
||||
assertTrue(latch.await(5, SECONDS));
|
||||
assertFalse(error.get());
|
||||
// Clean up
|
||||
s.close();
|
||||
kal.close();
|
||||
// Stop the plugin
|
||||
plugin.stop();
|
||||
}
|
||||
|
||||
@@ -268,7 +274,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
|
||||
descriptor.add(local.getPort());
|
||||
// Connect to the port
|
||||
DuplexTransportConnection d = plugin.createKeyAgreementConnection(
|
||||
new byte[COMMIT_LENGTH], descriptor, 5000);
|
||||
new byte[COMMIT_LENGTH], descriptor);
|
||||
assertNotNull(d);
|
||||
// Check that the connection was accepted
|
||||
assertTrue(latch.await(5, SECONDS));
|
||||
|
||||
@@ -100,21 +100,21 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// validateOutstandingMessages()
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getMessagesToValidate(txn, clientId);
|
||||
oneOf(db).getMessagesToValidate(txn);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
// deliverOutstandingMessages()
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn1));
|
||||
oneOf(db).getPendingMessages(txn1, clientId);
|
||||
oneOf(db).getPendingMessages(txn1);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn1);
|
||||
oneOf(db).endTransaction(txn1);
|
||||
// shareOutstandingMessages()
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn2));
|
||||
oneOf(db).getMessagesToShare(txn2, clientId);
|
||||
oneOf(db).getMessagesToShare(txn2);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn2);
|
||||
oneOf(db).endTransaction(txn2);
|
||||
@@ -138,7 +138,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get messages to validate
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getMessagesToValidate(txn, clientId);
|
||||
oneOf(db).getMessagesToValidate(txn);
|
||||
will(returnValue(Arrays.asList(messageId, messageId1)));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
@@ -199,14 +199,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get pending messages to deliver
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn5));
|
||||
oneOf(db).getPendingMessages(txn5, clientId);
|
||||
oneOf(db).getPendingMessages(txn5);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn5);
|
||||
oneOf(db).endTransaction(txn5);
|
||||
// Get messages to share
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn6));
|
||||
oneOf(db).getMessagesToShare(txn6, clientId);
|
||||
oneOf(db).getMessagesToShare(txn6);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn6);
|
||||
oneOf(db).endTransaction(txn6);
|
||||
@@ -227,14 +227,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get messages to validate
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getMessagesToValidate(txn, clientId);
|
||||
oneOf(db).getMessagesToValidate(txn);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
// Get pending messages to deliver
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn1));
|
||||
oneOf(db).getPendingMessages(txn1, clientId);
|
||||
oneOf(db).getPendingMessages(txn1);
|
||||
will(returnValue(Collections.singletonList(messageId)));
|
||||
oneOf(db).commitTransaction(txn1);
|
||||
oneOf(db).endTransaction(txn1);
|
||||
@@ -292,7 +292,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get messages to share
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn4));
|
||||
oneOf(db).getMessagesToShare(txn4, clientId);
|
||||
oneOf(db).getMessagesToShare(txn4);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn4);
|
||||
oneOf(db).endTransaction(txn4);
|
||||
@@ -313,14 +313,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// No messages to validate
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getMessagesToValidate(txn, clientId);
|
||||
oneOf(db).getMessagesToValidate(txn);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
// No pending messages to deliver
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn1));
|
||||
oneOf(db).getPendingMessages(txn1, clientId);
|
||||
oneOf(db).getPendingMessages(txn1);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn1);
|
||||
oneOf(db).endTransaction(txn1);
|
||||
@@ -328,7 +328,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get messages to share
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn2));
|
||||
oneOf(db).getMessagesToShare(txn2, clientId);
|
||||
oneOf(db).getMessagesToShare(txn2);
|
||||
will(returnValue(Collections.singletonList(messageId)));
|
||||
oneOf(db).commitTransaction(txn2);
|
||||
oneOf(db).endTransaction(txn2);
|
||||
@@ -416,7 +416,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get messages to validate
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getMessagesToValidate(txn, clientId);
|
||||
oneOf(db).getMessagesToValidate(txn);
|
||||
will(returnValue(Arrays.asList(messageId, messageId1)));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
@@ -457,14 +457,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get pending messages to deliver
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn4));
|
||||
oneOf(db).getPendingMessages(txn4, clientId);
|
||||
oneOf(db).getPendingMessages(txn4);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn4);
|
||||
oneOf(db).endTransaction(txn4);
|
||||
// Get messages to share
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn5));
|
||||
oneOf(db).getMessagesToShare(txn5, clientId);
|
||||
oneOf(db).getMessagesToShare(txn5);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn5);
|
||||
oneOf(db).endTransaction(txn5);
|
||||
@@ -487,7 +487,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get messages to validate
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getMessagesToValidate(txn, clientId);
|
||||
oneOf(db).getMessagesToValidate(txn);
|
||||
will(returnValue(Arrays.asList(messageId, messageId1)));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
@@ -533,14 +533,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
|
||||
// Get pending messages to deliver
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn4));
|
||||
oneOf(db).getPendingMessages(txn4, clientId);
|
||||
oneOf(db).getPendingMessages(txn4);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn4);
|
||||
oneOf(db).endTransaction(txn4);
|
||||
// Get messages to share
|
||||
oneOf(db).startTransaction(true);
|
||||
will(returnValue(txn5));
|
||||
oneOf(db).getMessagesToShare(txn5, clientId);
|
||||
oneOf(db).getMessagesToShare(txn5);
|
||||
will(returnValue(Collections.emptyList()));
|
||||
oneOf(db).commitTransaction(txn5);
|
||||
oneOf(db).endTransaction(txn5);
|
||||
|
||||
@@ -17,6 +17,8 @@ import javax.inject.Singleton;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
|
||||
@Module
|
||||
public class TestLifecycleModule {
|
||||
|
||||
@@ -57,6 +59,11 @@ public class TestLifecycleModule {
|
||||
@Override
|
||||
public void waitForShutdown() throws InterruptedException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleState getLifecycleState() {
|
||||
return RUNNING;
|
||||
}
|
||||
};
|
||||
return lifecycleManager;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.plugin;
|
||||
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.ShutdownManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
@@ -8,7 +9,7 @@ import org.briarproject.bramble.api.plugin.PluginConfig;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
|
||||
import org.briarproject.bramble.api.reliability.ReliabilityLayerFactory;
|
||||
import org.briarproject.bramble.plugin.bluetooth.BluetoothPluginFactory;
|
||||
import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory;
|
||||
import org.briarproject.bramble.plugin.file.RemovableDrivePluginFactory;
|
||||
import org.briarproject.bramble.plugin.modem.ModemPluginFactory;
|
||||
import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory;
|
||||
@@ -30,9 +31,10 @@ public class DesktopPluginModule extends PluginModule {
|
||||
PluginConfig getPluginConfig(@IoExecutor Executor ioExecutor,
|
||||
SecureRandom random, BackoffFactory backoffFactory,
|
||||
ReliabilityLayerFactory reliabilityFactory,
|
||||
ShutdownManager shutdownManager) {
|
||||
DuplexPluginFactory bluetooth = new BluetoothPluginFactory(ioExecutor,
|
||||
random, backoffFactory);
|
||||
ShutdownManager shutdownManager, EventBus eventBus) {
|
||||
DuplexPluginFactory bluetooth =
|
||||
new JavaBluetoothPluginFactory(ioExecutor, random, eventBus,
|
||||
backoffFactory);
|
||||
DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor,
|
||||
reliabilityFactory);
|
||||
DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor,
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.bluetooth.BluetoothStateException;
|
||||
import javax.bluetooth.LocalDevice;
|
||||
import javax.microedition.io.Connector;
|
||||
import javax.microedition.io.StreamConnection;
|
||||
import javax.microedition.io.StreamConnectionNotifier;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.StringUtils.isValidMac;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class JavaBluetoothPlugin extends BluetoothPlugin<StreamConnectionNotifier> {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(JavaBluetoothPlugin.class.getName());
|
||||
|
||||
// Non-null if the plugin started successfully
|
||||
private volatile LocalDevice localDevice = null;
|
||||
|
||||
JavaBluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom,
|
||||
Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
|
||||
super(ioExecutor, secureRandom, backoff, callback, maxLatency);
|
||||
}
|
||||
|
||||
@Override
|
||||
void initialiseAdapter() throws IOException {
|
||||
try {
|
||||
localDevice = LocalDevice.getLocalDevice();
|
||||
} catch (UnsatisfiedLinkError | BluetoothStateException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isAdapterEnabled() {
|
||||
return localDevice != null && LocalDevice.isPowerOn();
|
||||
}
|
||||
|
||||
@Override
|
||||
void enableAdapter() {
|
||||
// Nothing we can do on this platform
|
||||
LOG.info("Could not enable Bluetooth");
|
||||
}
|
||||
|
||||
@Override
|
||||
void disableAdapterIfEnabledByUs() {
|
||||
// We didn't enable it so we don't need to disable it
|
||||
}
|
||||
|
||||
@Override
|
||||
void setEnabledByUs() {
|
||||
// Irrelevant on this platform
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
String getBluetoothAddress() {
|
||||
return localDevice.getBluetoothAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
StreamConnectionNotifier openServerSocket(String uuid) throws IOException {
|
||||
String url = makeUrl("localhost", uuid);
|
||||
return (StreamConnectionNotifier) Connector.open(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
void tryToClose(@Nullable StreamConnectionNotifier ss) {
|
||||
try {
|
||||
if (ss != null) ss.close();
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
DuplexTransportConnection acceptConnection(StreamConnectionNotifier ss)
|
||||
throws IOException {
|
||||
return wrapSocket(ss.acceptAndOpen());
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isValidAddress(String address) {
|
||||
return isValidMac(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
DuplexTransportConnection connectTo(String address, String uuid)
|
||||
throws IOException {
|
||||
String url = makeUrl(address, uuid);
|
||||
return wrapSocket((StreamConnection) Connector.open(url));
|
||||
}
|
||||
|
||||
private String makeUrl(String address, String uuid) {
|
||||
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
||||
}
|
||||
|
||||
private DuplexTransportConnection wrapSocket(StreamConnection s) {
|
||||
return new JavaBluetoothTransportConnection(this, s);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.plugin.bluetooth;
|
||||
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Backoff;
|
||||
import org.briarproject.bramble.api.plugin.BackoffFactory;
|
||||
@@ -17,7 +18,7 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class BluetoothPluginFactory implements DuplexPluginFactory {
|
||||
public class JavaBluetoothPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
|
||||
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
|
||||
@@ -27,12 +28,15 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
|
||||
private final Executor ioExecutor;
|
||||
private final SecureRandom secureRandom;
|
||||
private final BackoffFactory backoffFactory;
|
||||
private final EventBus eventBus;
|
||||
|
||||
public BluetoothPluginFactory(Executor ioExecutor,
|
||||
SecureRandom secureRandom, BackoffFactory backoffFactory) {
|
||||
public JavaBluetoothPluginFactory(Executor ioExecutor,
|
||||
SecureRandom secureRandom, EventBus eventBus,
|
||||
BackoffFactory backoffFactory) {
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.secureRandom = secureRandom;
|
||||
this.backoffFactory = backoffFactory;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,7 +53,9 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
||||
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
||||
return new BluetoothPlugin(ioExecutor, secureRandom, backoff, callback,
|
||||
MAX_LATENCY);
|
||||
JavaBluetoothPlugin plugin = new JavaBluetoothPlugin(ioExecutor,
|
||||
secureRandom, backoff, callback, MAX_LATENCY);
|
||||
eventBus.addListener(plugin);
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,12 @@ import java.io.OutputStream;
|
||||
import javax.microedition.io.StreamConnection;
|
||||
|
||||
@NotNullByDefault
|
||||
class BluetoothTransportConnection extends AbstractDuplexTransportConnection {
|
||||
class JavaBluetoothTransportConnection
|
||||
extends AbstractDuplexTransportConnection {
|
||||
|
||||
private final StreamConnection stream;
|
||||
|
||||
BluetoothTransportConnection(Plugin plugin, StreamConnection stream) {
|
||||
JavaBluetoothTransportConnection(Plugin plugin, StreamConnection stream) {
|
||||
super(plugin);
|
||||
this.stream = stream;
|
||||
}
|
||||
@@ -177,7 +177,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createKeyAgreementConnection(
|
||||
byte[] commitment, BdfList descriptor, long timeout) {
|
||||
byte[] commitment, BdfList descriptor) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = pt_BR: pt-rBR, fr_FR: fr, nb_NO: nb, zh-Hans: zh-rCN
|
||||
lang_map = pt_BR: pt-rBR, nb_NO: nb, zh-Hans: zh-rCN
|
||||
|
||||
[briar.stringsxml-5]
|
||||
file_filter = src/main/res/values-<lang>/strings.xml
|
||||
|
||||
@@ -189,8 +189,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 26
|
||||
versionCode 1618
|
||||
versionName "0.16.18"
|
||||
versionCode 1620
|
||||
versionName "0.16.20"
|
||||
applicationId "org.briarproject.briar.beta"
|
||||
resValue "string", "app_package", "org.briarproject.briar.beta"
|
||||
resValue "string", "app_name", "Briar Beta"
|
||||
|
||||
@@ -77,6 +77,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.login.OpenDatabaseActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"/>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
|
||||
android:theme="@style/BriarTheme.NoActionBar"
|
||||
@@ -369,7 +374,12 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.panic.ExitActivity"
|
||||
android:name="org.briarproject.briar.android.logout.ExitActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.logout.HideUiActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.net.Uri;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
@@ -61,6 +62,7 @@ import javax.inject.Inject;
|
||||
import static android.app.Notification.DEFAULT_LIGHTS;
|
||||
import static android.app.Notification.DEFAULT_SOUND;
|
||||
import static android.app.Notification.DEFAULT_VIBRATE;
|
||||
import static android.app.Notification.VISIBILITY_SECRET;
|
||||
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
@@ -89,12 +91,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
private static final int BLOG_POST_NOTIFICATION_ID = 6;
|
||||
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 7;
|
||||
|
||||
// Channel IDs
|
||||
private static final String CONTACT_CHANNEL_ID = "contacts";
|
||||
private static final String GROUP_CHANNEL_ID = "groups";
|
||||
private static final String FORUM_CHANNEL_ID = "forums";
|
||||
private static final String BLOG_CHANNEL_ID = "blogs";
|
||||
|
||||
private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
|
||||
|
||||
private static final Logger LOG =
|
||||
@@ -171,9 +167,15 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
@TargetApi(26)
|
||||
private void createNotificationChannel(String channelId,
|
||||
@StringRes int name) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel nc =
|
||||
new NotificationChannel(channelId, appContext.getString(name),
|
||||
IMPORTANCE_DEFAULT));
|
||||
IMPORTANCE_DEFAULT);
|
||||
nc.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
nc.enableVibration(true);
|
||||
nc.enableLights(true);
|
||||
nc.setLightColor(
|
||||
ContextCompat.getColor(appContext, R.color.briar_green_light));
|
||||
notificationManager.createNotificationChannel(nc);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -94,6 +94,7 @@ public class AppModule {
|
||||
|
||||
@Override
|
||||
public boolean databaseExists() {
|
||||
// FIXME should not run on UiThread #620
|
||||
if (!dir.isDirectory()) return false;
|
||||
File[] files = dir.listFiles();
|
||||
return files != null && files.length > 0;
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
@@ -17,19 +20,26 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.logout.HideUiActivity;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
import org.briarproject.briar.android.splash.SplashScreenActivity;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
import static android.app.NotificationManager.IMPORTANCE_NONE;
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static android.content.Intent.ACTION_SHUTDOWN;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE;
|
||||
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
|
||||
@@ -61,6 +71,9 @@ public class BriarService extends Service {
|
||||
private final AtomicBoolean created = new AtomicBoolean(false);
|
||||
private final Binder binder = new BriarBinder();
|
||||
|
||||
@Nullable
|
||||
private BroadcastReceiver receiver = null;
|
||||
|
||||
@Inject
|
||||
protected DatabaseConfig databaseConfig;
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@@ -88,6 +101,7 @@ public class BriarService extends Service {
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create notification channels
|
||||
if (SDK_INT >= 26) {
|
||||
NotificationManager nm = (NotificationManager)
|
||||
@@ -142,6 +156,19 @@ public class BriarService extends Service {
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
// Register for device shutdown broadcasts
|
||||
receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
LOG.info("Device is shutting down");
|
||||
shutdownFromBackground();
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_SHUTDOWN);
|
||||
filter.addAction("android.intent.action.QUICKBOOT_POWEROFF");
|
||||
filter.addAction("com.htc.intent.action.QUICKBOOT_POWEROFF");
|
||||
registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
private void showStartupFailureNotification(StartResult result) {
|
||||
@@ -186,6 +213,7 @@ public class BriarService extends Service {
|
||||
super.onDestroy();
|
||||
LOG.info("Destroyed");
|
||||
stopForeground(true);
|
||||
if (receiver != null) unregisterReceiver(receiver);
|
||||
// Stop the services in a background thread
|
||||
new Thread() {
|
||||
@Override
|
||||
@@ -199,7 +227,48 @@ public class BriarService extends Service {
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
LOG.warning("Memory is low");
|
||||
// FIXME: Work out what to do about it
|
||||
shutdownFromBackground();
|
||||
showLowMemoryShutdownNotification();
|
||||
}
|
||||
|
||||
private void shutdownFromBackground() {
|
||||
// Stop the service
|
||||
stopSelf();
|
||||
// Hide the UI
|
||||
Intent i = new Intent(this, HideUiActivity.class);
|
||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK
|
||||
| FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||
| FLAG_ACTIVITY_NO_ANIMATION
|
||||
| FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(i);
|
||||
// Wait for shutdown to complete, then exit
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (started) lifecycleManager.waitForShutdown();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.info("Interrupted while waiting for shutdown");
|
||||
}
|
||||
LOG.info("Exiting");
|
||||
System.exit(0);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void showLowMemoryShutdownNotification() {
|
||||
androidExecutor.runOnUiThread(() -> {
|
||||
NotificationCompat.Builder b = new NotificationCompat.Builder(
|
||||
BriarService.this, FAILURE_CHANNEL_ID);
|
||||
b.setSmallIcon(android.R.drawable.stat_notify_error);
|
||||
b.setContentTitle(getText(
|
||||
R.string.low_memory_shutdown_notification_title));
|
||||
b.setContentText(getText(
|
||||
R.string.low_memory_shutdown_notification_text));
|
||||
Intent i = new Intent(this, SplashScreenActivity.class);
|
||||
b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0));
|
||||
b.setAutoCancel(true);
|
||||
Object o = getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(FAILURE_NOTIFICATION_ID, b.build());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,29 +3,31 @@ package org.briarproject.briar.android;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.fragment.ErrorFragment;
|
||||
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
|
||||
import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID;
|
||||
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
|
||||
|
||||
public class StartupFailureActivity extends BaseActivity {
|
||||
public class StartupFailureActivity extends BaseActivity implements
|
||||
BaseFragmentListener {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
setContentView(R.layout.activity_startup_failure);
|
||||
setContentView(R.layout.activity_fragment_container);
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent i) {
|
||||
@@ -41,12 +43,31 @@ public class StartupFailureActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
// show proper error message
|
||||
TextView view = findViewById(R.id.errorView);
|
||||
if (result.equals(StartResult.DB_ERROR)) {
|
||||
view.setText(getText(R.string.startup_failed_db_error));
|
||||
} else if (result.equals(StartResult.SERVICE_ERROR)) {
|
||||
view.setText(getText(R.string.startup_failed_service_error));
|
||||
String errorMsg;
|
||||
switch (result) {
|
||||
case DATA_TOO_OLD_ERROR:
|
||||
errorMsg = getString(R.string.startup_failed_db_error);
|
||||
break;
|
||||
case DATA_TOO_NEW_ERROR:
|
||||
errorMsg =
|
||||
getString(R.string.startup_failed_data_too_new_error);
|
||||
break;
|
||||
case DB_ERROR:
|
||||
errorMsg =
|
||||
getString(R.string.startup_failed_data_too_old_error);
|
||||
break;
|
||||
case SERVICE_ERROR:
|
||||
errorMsg = getString(R.string.startup_failed_service_error);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
showInitialFragment(ErrorFragment.newInstance(errorMsg));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runOnDbThread(Runnable runnable) {
|
||||
throw new AssertionError("Deprecated and should not be used");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.android.activity;
|
||||
import android.app.Activity;
|
||||
|
||||
import org.briarproject.briar.android.AndroidComponent;
|
||||
import org.briarproject.briar.android.StartupFailureActivity;
|
||||
import org.briarproject.briar.android.blog.BlogActivity;
|
||||
import org.briarproject.briar.android.blog.BlogFragment;
|
||||
import org.briarproject.briar.android.blog.BlogModule;
|
||||
@@ -31,6 +32,7 @@ import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
|
||||
import org.briarproject.briar.android.login.AuthorNameFragment;
|
||||
import org.briarproject.briar.android.login.ChangePasswordActivity;
|
||||
import org.briarproject.briar.android.login.DozeFragment;
|
||||
import org.briarproject.briar.android.login.OpenDatabaseActivity;
|
||||
import org.briarproject.briar.android.login.PasswordActivity;
|
||||
import org.briarproject.briar.android.login.PasswordFragment;
|
||||
import org.briarproject.briar.android.login.SetupActivity;
|
||||
@@ -87,6 +89,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(SetupActivity activity);
|
||||
|
||||
void inject(OpenDatabaseActivity activity);
|
||||
|
||||
void inject(NavDrawerActivity activity);
|
||||
|
||||
void inject(PasswordActivity activity);
|
||||
@@ -151,6 +155,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(RssFeedManageActivity activity);
|
||||
|
||||
void inject(StartupFailureActivity activity);
|
||||
|
||||
// Fragments
|
||||
void inject(AuthorNameFragment fragment);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.briarproject.briar.android.controller.BriarController;
|
||||
import org.briarproject.briar.android.controller.DbController;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.briar.android.login.PasswordActivity;
|
||||
import org.briarproject.briar.android.panic.ExitActivity;
|
||||
import org.briarproject.briar.android.logout.ExitActivity;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@ public interface RequestCodes {
|
||||
int REQUEST_RINGTONE = 7;
|
||||
int REQUEST_PERMISSION_CAMERA = 8;
|
||||
int REQUEST_DOZE_WHITELISTING = 9;
|
||||
int REQUEST_ENABLE_BLUETOOTH = 10;
|
||||
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ abstract class BasePostFragment extends BaseFragment {
|
||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
getContext().startActivity(i);
|
||||
}
|
||||
});
|
||||
}, getFragmentManager());
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
||||
|
||||
adapter = new BlogPostAdapter(getActivity(), this);
|
||||
adapter =
|
||||
new BlogPostAdapter(getActivity(), this, getFragmentManager());
|
||||
list = v.findViewById(R.id.postList);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(adapter);
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
package org.briarproject.briar.android.blog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.BriarAdapter;
|
||||
|
||||
class BlogPostAdapter
|
||||
extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
|
||||
|
||||
private final OnBlogPostClickListener listener;
|
||||
@Nullable
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) {
|
||||
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener,
|
||||
@Nullable FragmentManager fragmentManager) {
|
||||
super(ctx, BlogPostItem.class);
|
||||
this.listener = listener;
|
||||
this.fragmentManager = fragmentManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -23,8 +32,7 @@ class BlogPostAdapter
|
||||
int viewType) {
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
R.layout.list_item_blog_post, parent, false);
|
||||
BlogPostViewHolder ui = new BlogPostViewHolder(v, false, listener);
|
||||
return ui;
|
||||
return new BlogPostViewHolder(v, false, listener, fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Spanned;
|
||||
@@ -52,12 +53,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@NonNull
|
||||
private final OnBlogPostClickListener listener;
|
||||
@Nullable
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
BlogPostViewHolder(View v, boolean fullText,
|
||||
@NonNull OnBlogPostClickListener listener) {
|
||||
@NonNull OnBlogPostClickListener listener,
|
||||
@Nullable FragmentManager fragmentManager) {
|
||||
super(v);
|
||||
this.fullText = fullText;
|
||||
this.listener = listener;
|
||||
this.fragmentManager = fragmentManager;
|
||||
|
||||
ctx = v.getContext();
|
||||
layout = v.findViewById(R.id.postLayout);
|
||||
@@ -117,7 +122,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||
if (fullText) {
|
||||
body.setText(bodyText);
|
||||
body.setTextIsSelectable(true);
|
||||
makeLinksClickable(body);
|
||||
makeLinksClickable(body, fragmentManager);
|
||||
} else {
|
||||
body.setTextIsSelectable(false);
|
||||
if (bodyText.length() > TEASER_LENGTH)
|
||||
|
||||
@@ -74,7 +74,8 @@ public class FeedFragment extends BaseFragment implements
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
||||
|
||||
adapter = new BlogPostAdapter(getActivity(), this);
|
||||
adapter =
|
||||
new BlogPostAdapter(getActivity(), this, getFragmentManager());
|
||||
|
||||
layoutManager = new LinearLayoutManager(getActivity());
|
||||
list = v.findViewById(R.id.postList);
|
||||
|
||||
@@ -146,7 +146,7 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
ui.input.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
private class ViewHolder {
|
||||
|
||||
private final ScrollView scrollView;
|
||||
private final ProgressBar progressBar;
|
||||
@@ -167,7 +167,7 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
public void onAuthorClick(BlogPostItem post) {
|
||||
// probably don't want to allow author clicks here
|
||||
}
|
||||
});
|
||||
}, getFragmentManager());
|
||||
input = v.findViewById(R.id.inputText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,7 @@ public interface ConfigController {
|
||||
void deleteAccount(Context ctx);
|
||||
|
||||
boolean accountExists();
|
||||
|
||||
boolean accountSignedIn();
|
||||
|
||||
}
|
||||
|
||||
@@ -52,4 +52,10 @@ public class ConfigControllerImpl implements ConfigController {
|
||||
String hex = getEncryptedDatabaseKey();
|
||||
return hex != null && databaseConfig.databaseExists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accountSignedIn() {
|
||||
return databaseConfig.getEncryptionKey() != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,19 +52,15 @@ class ForumListAdapter
|
||||
// Post Count
|
||||
int postCount = item.getPostCount();
|
||||
if (postCount > 0) {
|
||||
ui.avatar.setProblem(false);
|
||||
ui.postCount.setText(ctx.getResources()
|
||||
.getQuantityString(R.plurals.posts, postCount,
|
||||
postCount));
|
||||
ui.postCount.setTextColor(
|
||||
ContextCompat
|
||||
.getColor(ctx, R.color.briar_text_secondary));
|
||||
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
|
||||
} else {
|
||||
ui.avatar.setProblem(true);
|
||||
ui.postCount.setText(ctx.getString(R.string.no_posts));
|
||||
ui.postCount.setTextColor(
|
||||
ContextCompat
|
||||
.getColor(ctx, R.color.briar_text_tertiary));
|
||||
ContextCompat.getColor(ctx, R.color.briar_text_tertiary));
|
||||
}
|
||||
|
||||
// Date
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.briarproject.briar.android.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ErrorFragment extends BaseFragment {
|
||||
|
||||
private static final String TAG = ErrorFragment.class.getSimpleName();
|
||||
|
||||
private static final String ERROR_MSG = "errorMessage";
|
||||
|
||||
public static ErrorFragment newInstance(String message) {
|
||||
ErrorFragment f = new ErrorFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ERROR_MSG, message);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
||||
private String errorMessage;
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle args = getArguments();
|
||||
if (args == null) throw new AssertionError();
|
||||
errorMessage = args.getString(ERROR_MSG);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View v = inflater
|
||||
.inflate(R.layout.fragment_error, container, false);
|
||||
TextView msg = v.findViewById(R.id.errorMessage);
|
||||
msg.setText(errorMessage);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
// not necessary
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import android.hardware.Camera.AutoFocusCallback;
|
||||
import android.hardware.Camera.CameraInfo;
|
||||
import android.hardware.Camera.Parameters;
|
||||
import android.hardware.Camera.Size;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.util.AttributeSet;
|
||||
@@ -22,6 +21,7 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
import static android.hardware.Camera.Parameters.FLASH_MODE_OFF;
|
||||
import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
|
||||
@@ -32,6 +32,7 @@ import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
|
||||
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
|
||||
import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO;
|
||||
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
@@ -41,7 +42,12 @@ import static java.util.logging.Level.WARNING;
|
||||
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
AutoFocusCallback, View.OnClickListener {
|
||||
|
||||
// Heuristic for the ideal preview size - small previews don't have enough
|
||||
// detail, large previews are slow to decode
|
||||
private static final int IDEAL_PIXELS = 500 * 1000;
|
||||
|
||||
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CameraView.class.getName());
|
||||
|
||||
@@ -49,6 +55,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
|
||||
@Nullable
|
||||
private Camera camera = null;
|
||||
private int cameraIndex = 0;
|
||||
private PreviewConsumer previewConsumer = null;
|
||||
private Surface surface = null;
|
||||
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
|
||||
@@ -89,15 +96,32 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void start() throws CameraException {
|
||||
public void start(int rotationDegrees) throws CameraException {
|
||||
LOG.info("Opening camera");
|
||||
try {
|
||||
camera = Camera.open();
|
||||
int cameras = Camera.getNumberOfCameras();
|
||||
if (cameras == 0) throw new CameraException("No camera");
|
||||
// Try to find a back-facing camera
|
||||
for (int i = 0; i < cameras; i++) {
|
||||
CameraInfo info = new CameraInfo();
|
||||
Camera.getCameraInfo(i, info);
|
||||
if (info.facing == CAMERA_FACING_BACK) {
|
||||
LOG.info("Using back-facing camera");
|
||||
camera = Camera.open(i);
|
||||
cameraIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we can't find a back-facing camera, use a front-facing one
|
||||
if (camera == null) {
|
||||
LOG.info("Using front-facing camera");
|
||||
camera = Camera.open(0);
|
||||
cameraIndex = 0;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
if (camera == null) throw new CameraException("No back-facing camera");
|
||||
setDisplayOrientation(0);
|
||||
setDisplayOrientation(rotationDegrees);
|
||||
// Use barcode scene mode if it's available
|
||||
Parameters params = camera.getParameters();
|
||||
params = setSceneMode(camera, params);
|
||||
@@ -163,7 +187,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
private void startConsumer() throws CameraException {
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
startAutoFocus();
|
||||
previewConsumer.start(camera);
|
||||
previewConsumer.start(camera, cameraIndex);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -199,13 +223,17 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Camera#setDisplayOrientation(int)}.
|
||||
*/
|
||||
@UiThread
|
||||
private void setDisplayOrientation(int rotationDegrees)
|
||||
throws CameraException {
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
int orientation;
|
||||
CameraInfo info = new CameraInfo();
|
||||
try {
|
||||
Camera.getCameraInfo(0, info);
|
||||
Camera.getCameraInfo(cameraIndex, info);
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
@@ -215,9 +243,11 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
} else {
|
||||
orientation = (info.orientation - rotationDegrees + 360) % 360;
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Display orientation " + orientation + " degrees");
|
||||
if (camera == null) throw new CameraException("Camera is null");
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Screen rotation " + rotationDegrees
|
||||
+ " degrees, camera orientation " + orientation
|
||||
+ " degrees");
|
||||
}
|
||||
try {
|
||||
camera.setDisplayOrientation(orientation);
|
||||
} catch (RuntimeException e) {
|
||||
@@ -285,8 +315,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
|
||||
@UiThread
|
||||
private void setVideoStabilisation(Parameters params) {
|
||||
if (Build.VERSION.SDK_INT >= 15 &&
|
||||
params.isVideoStabilizationSupported()) {
|
||||
if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) {
|
||||
params.setVideoStabilization(true);
|
||||
}
|
||||
}
|
||||
@@ -313,6 +342,8 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
@UiThread
|
||||
private void setPreviewSize(Parameters params) {
|
||||
if (surfaceWidth == 0 || surfaceHeight == 0) return;
|
||||
// Choose a preview size that's close to the aspect ratio of the
|
||||
// surface and close to the ideal size for decoding
|
||||
float idealRatio = (float) surfaceWidth / surfaceHeight;
|
||||
boolean rotatePreview = displayOrientation % 180 == 90;
|
||||
List<Size> sizes = params.getSupportedPreviewSizes();
|
||||
@@ -323,11 +354,12 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
int height = rotatePreview ? size.width : size.height;
|
||||
float ratio = (float) width / height;
|
||||
float stretch = Math.max(ratio / idealRatio, idealRatio / ratio);
|
||||
int pixels = width * height;
|
||||
float score = pixels / stretch;
|
||||
float pixels = width * height;
|
||||
float zoom = Math.max(pixels / IDEAL_PIXELS, IDEAL_PIXELS / pixels);
|
||||
float score = 1 / (stretch * zoom);
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Size " + size.width + "x" + size.height
|
||||
+ ", stretch " + stretch + ", pixels " + pixels
|
||||
+ ", stretch " + stretch + ", zoom " + zoom
|
||||
+ ", score " + score);
|
||||
}
|
||||
if (bestSize == null || score > bestScore) {
|
||||
@@ -358,7 +390,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
} catch (RuntimeException e) {
|
||||
throw new CameraException(e);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 15) {
|
||||
if (SDK_INT >= 15) {
|
||||
LOG.info("Video stabilisation enabled: "
|
||||
+ params.getVideoStabilization());
|
||||
}
|
||||
@@ -389,8 +421,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
surface.release();
|
||||
}
|
||||
surface = holder.getSurface();
|
||||
// Start the preview when the camera and the surface are both ready
|
||||
if (camera != null && !previewStarted) startPreview(holder);
|
||||
// We'll start the preview when surfaceChanged() is called
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -416,7 +447,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
surfaceWidth = w;
|
||||
surfaceHeight = h;
|
||||
if (camera == null) return; // We are stopped
|
||||
stopPreview();
|
||||
if (previewStarted) stopPreview();
|
||||
try {
|
||||
Parameters params = camera.getParameters();
|
||||
setPreviewSize(params);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.briarproject.briar.android.keyagreement;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog.Builder;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
@@ -23,6 +29,7 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
|
||||
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.R.string;
|
||||
import org.briarproject.briar.R.style;
|
||||
@@ -39,9 +46,15 @@ import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ENABLE_BLUETOOTH;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -50,6 +63,10 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
BaseFragmentListener, IntroScreenSeenListener, EventListener,
|
||||
ContactExchangeListener {
|
||||
|
||||
private enum BluetoothState {
|
||||
UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(KeyAgreementActivity.class.getName());
|
||||
|
||||
@@ -62,7 +79,10 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
@Inject
|
||||
volatile IdentityManager identityManager;
|
||||
|
||||
private boolean isResumed = false, enableWasRequested = false;
|
||||
private boolean continueClicked, gotCameraPermission;
|
||||
private BluetoothState bluetoothState = BluetoothState.UNKNOWN;
|
||||
private BroadcastReceiver bluetoothReceiver = null;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
@@ -84,6 +104,15 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
if (state == null) {
|
||||
showInitialFragment(IntroFragment.newInstance());
|
||||
}
|
||||
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
|
||||
bluetoothReceiver = new BluetoothStateReceiver();
|
||||
registerReceiver(bluetoothReceiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (bluetoothReceiver != null) unregisterReceiver(bluetoothReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,29 +141,80 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
isResumed = true;
|
||||
// Workaround for
|
||||
// https://code.google.com/p/android/issues/detail?id=190966
|
||||
if (continueClicked && gotCameraPermission) {
|
||||
showQrCodeFragment();
|
||||
}
|
||||
if (canShowQrCodeFragment()) showQrCodeFragment();
|
||||
}
|
||||
|
||||
boolean canShowQrCodeFragment() {
|
||||
return isResumed && continueClicked
|
||||
&& (SDK_INT < 23 || gotCameraPermission)
|
||||
&& bluetoothState != BluetoothState.UNKNOWN
|
||||
&& bluetoothState != BluetoothState.WAITING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
isResumed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showNextScreen() {
|
||||
// FIXME #824
|
||||
// showNextFragment(ShowQrCodeFragment.newInstance());
|
||||
continueClicked = true;
|
||||
if (checkPermissions()) {
|
||||
showQrCodeFragment();
|
||||
if (shouldRequestEnableBluetooth()) requestEnableBluetooth();
|
||||
else if (canShowQrCodeFragment()) showQrCodeFragment();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRequestEnableBluetooth() {
|
||||
return bluetoothState == BluetoothState.UNKNOWN
|
||||
|| bluetoothState == BluetoothState.REFUSED;
|
||||
}
|
||||
|
||||
private void requestEnableBluetooth() {
|
||||
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bt == null) {
|
||||
setBluetoothState(BluetoothState.NO_ADAPTER);
|
||||
} else if (bt.isEnabled()) {
|
||||
setBluetoothState(BluetoothState.ENABLED);
|
||||
} else {
|
||||
enableWasRequested = true;
|
||||
setBluetoothState(BluetoothState.WAITING);
|
||||
Intent i = new Intent(ACTION_REQUEST_ENABLE);
|
||||
startActivityForResult(i, REQUEST_ENABLE_BLUETOOTH);
|
||||
}
|
||||
}
|
||||
|
||||
private void setBluetoothState(BluetoothState bluetoothState) {
|
||||
LOG.info("Setting Bluetooth state to " + bluetoothState);
|
||||
this.bluetoothState = bluetoothState;
|
||||
if (enableWasRequested && bluetoothState == BluetoothState.ENABLED) {
|
||||
eventBus.broadcast(new BluetoothEnabledEvent());
|
||||
enableWasRequested = false;
|
||||
}
|
||||
if (canShowQrCodeFragment()) showQrCodeFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int request, int result, Intent data) {
|
||||
// If the request was granted we'll catch the state change event
|
||||
if (request == REQUEST_ENABLE_BLUETOOTH && result == RESULT_CANCELED)
|
||||
setBluetoothState(BluetoothState.REFUSED);
|
||||
}
|
||||
|
||||
private void showQrCodeFragment() {
|
||||
BaseFragment f = ShowQrCodeFragment.newInstance();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.addToBackStack(f.getUniqueTag())
|
||||
.commit();
|
||||
// FIXME #824
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
if (fm.findFragmentByTag(ShowQrCodeFragment.TAG) == null) {
|
||||
BaseFragment f = ShowQrCodeFragment.newInstance();
|
||||
fm.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.addToBackStack(f.getUniqueTag())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPermissions() {
|
||||
@@ -154,8 +234,10 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
} else {
|
||||
requestPermission();
|
||||
}
|
||||
gotCameraPermission = false;
|
||||
return false;
|
||||
} else {
|
||||
gotCameraPermission = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -174,6 +256,7 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
if (grantResults.length > 0 &&
|
||||
grantResults[0] == PERMISSION_GRANTED) {
|
||||
gotCameraPermission = true;
|
||||
showNextScreen();
|
||||
} else {
|
||||
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||
CAMERA)) {
|
||||
@@ -258,4 +341,14 @@ public class KeyAgreementActivity extends BriarActivity implements
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if (state == STATE_ON) setBluetoothState(BluetoothState.ENABLED);
|
||||
else setBluetoothState(BluetoothState.UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
interface PreviewConsumer {
|
||||
|
||||
@UiThread
|
||||
void start(Camera camera);
|
||||
void start(Camera camera, int cameraIndex);
|
||||
|
||||
@UiThread
|
||||
void stop();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.briar.android.keyagreement;
|
||||
|
||||
import android.hardware.Camera;
|
||||
import android.hardware.Camera.CameraInfo;
|
||||
import android.hardware.Camera.PreviewCallback;
|
||||
import android.hardware.Camera.Size;
|
||||
import android.os.AsyncTask;
|
||||
@@ -36,20 +37,23 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
private final ResultCallback callback;
|
||||
|
||||
private Camera camera = null;
|
||||
private int cameraIndex = 0;
|
||||
|
||||
QrCodeDecoder(ResultCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Camera camera) {
|
||||
public void start(Camera camera, int cameraIndex) {
|
||||
this.camera = camera;
|
||||
this.cameraIndex = cameraIndex;
|
||||
askForPreviewFrame();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
camera = null;
|
||||
cameraIndex = 0;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -63,43 +67,54 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
if (camera == this.camera) {
|
||||
try {
|
||||
Size size = camera.getParameters().getPreviewSize();
|
||||
new DecoderTask(data, size.width, size.height).execute();
|
||||
// The preview should be in NV21 format: width * height bytes of
|
||||
// Y followed by width * height / 2 bytes of interleaved U and V
|
||||
if (data.length == size.width * size.height * 3 / 2) {
|
||||
CameraInfo info = new CameraInfo();
|
||||
Camera.getCameraInfo(cameraIndex, info);
|
||||
new DecoderTask(data, size.width, size.height,
|
||||
info.orientation).execute();
|
||||
} else {
|
||||
// Camera parameters have changed - ask for a new preview
|
||||
LOG.info("Preview size does not match camera parameters");
|
||||
askForPreviewFrame();
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOG.log(WARNING, "Error getting camera parameters.", e);
|
||||
}
|
||||
} else {
|
||||
LOG.info("Camera has changed, ignoring preview frame");
|
||||
}
|
||||
}
|
||||
|
||||
private class DecoderTask extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private final byte[] data;
|
||||
private final int width, height;
|
||||
private final int width, height, orientation;
|
||||
|
||||
private DecoderTask(byte[] data, int width, int height) {
|
||||
private DecoderTask(byte[] data, int width, int height,
|
||||
int orientation) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
long now = System.currentTimeMillis();
|
||||
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
|
||||
height, 0, 0, width, height, false);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
|
||||
Result result = null;
|
||||
BinaryBitmap bitmap = binarize(data, width, height, orientation);
|
||||
Result result;
|
||||
try {
|
||||
result = reader.decode(bitmap);
|
||||
} catch (ReaderException e) {
|
||||
return null; // No barcode found
|
||||
// No barcode found
|
||||
return null;
|
||||
} catch (RuntimeException e) {
|
||||
return null; // Preview data did not match width and height
|
||||
LOG.warning("Invalid preview frame");
|
||||
return null;
|
||||
} finally {
|
||||
reader.reset();
|
||||
}
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Decoding barcode took " + duration + " ms");
|
||||
callback.handleResult(result);
|
||||
return null;
|
||||
}
|
||||
@@ -110,6 +125,19 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
}
|
||||
}
|
||||
|
||||
private static BinaryBitmap binarize(byte[] data, int width, int height,
|
||||
int orientation) {
|
||||
// Crop to a square at the top (portrait) or left (landscape) of the
|
||||
// screen - this will be faster to decode and should include
|
||||
// everything visible in the viewfinder
|
||||
int crop = Math.min(width, height);
|
||||
int left = orientation >= 180 ? width - crop : 0;
|
||||
int top = orientation >= 180 ? height - crop : 0;
|
||||
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
|
||||
height, left, top, crop, crop, false);
|
||||
return new BinaryBitmap(new HybridBinarizer(src));
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
interface ResultCallback {
|
||||
|
||||
|
||||
@@ -32,21 +32,24 @@ class QrCodeUtils {
|
||||
// Generate QR code
|
||||
BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE,
|
||||
smallestDimen, smallestDimen);
|
||||
// Convert QR code to Bitmap
|
||||
int width = encoded.getWidth();
|
||||
int height = encoded.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
pixels[y * width + x] = encoded.get(x, y) ? BLACK : WHITE;
|
||||
}
|
||||
}
|
||||
Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888);
|
||||
qr.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
return qr;
|
||||
return renderQrCode(encoded);
|
||||
} catch (WriterException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap renderQrCode(BitMatrix matrix) {
|
||||
int width = matrix.getWidth();
|
||||
int height = matrix.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
pixels[y * width + x] = matrix.get(x, y) ? BLACK : WHITE;
|
||||
}
|
||||
}
|
||||
Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888);
|
||||
qr.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
return qr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package org.briarproject.briar.android.keyagreement;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.hardware.Camera;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.util.Base64;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Display;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -37,7 +38,6 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseEventFragment;
|
||||
@@ -49,12 +49,11 @@ import java.util.logging.Logger;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.widget.LinearLayout.HORIZONTAL;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
@@ -64,7 +63,8 @@ import static java.util.logging.Level.WARNING;
|
||||
public class ShowQrCodeFragment extends BaseEventFragment
|
||||
implements QrCodeDecoder.ResultCallback {
|
||||
|
||||
private static final String TAG = ShowQrCodeFragment.class.getName();
|
||||
static final String TAG = ShowQrCodeFragment.class.getName();
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(TAG);
|
||||
|
||||
@Inject
|
||||
@@ -85,9 +85,9 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
private ImageView qrCode;
|
||||
private TextView mainProgressTitle;
|
||||
private ViewGroup mainProgressContainer;
|
||||
private boolean fullscreen = false;
|
||||
|
||||
private BluetoothStateReceiver receiver;
|
||||
private boolean gotRemotePayload, waitingForBluetooth;
|
||||
private boolean gotRemotePayload;
|
||||
private volatile boolean gotLocalPayload;
|
||||
private KeyAgreementTask task;
|
||||
|
||||
@@ -130,6 +130,34 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
qrCode = view.findViewById(R.id.qr_code);
|
||||
mainProgressTitle = view.findViewById(R.id.title_progress_bar);
|
||||
mainProgressContainer = view.findViewById(R.id.container_progress);
|
||||
ImageView fullscreenButton = view.findViewById(R.id.fullscreen_button);
|
||||
fullscreenButton.setOnClickListener(v -> {
|
||||
View qrCodeContainer = view.findViewById(R.id.qr_code_container);
|
||||
LinearLayout cameraOverlay = view.findViewById(R.id.camera_overlay);
|
||||
LayoutParams statusParams, qrCodeParams;
|
||||
if (fullscreen) {
|
||||
// Shrink the QR code container to fill half its parent
|
||||
if (cameraOverlay.getOrientation() == HORIZONTAL) {
|
||||
statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
|
||||
qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
|
||||
} else {
|
||||
statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
|
||||
qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
|
||||
}
|
||||
fullscreenButton.setImageResource(
|
||||
R.drawable.ic_fullscreen_black_48dp);
|
||||
} else {
|
||||
// Grow the QR code container to fill its parent
|
||||
statusParams = new LayoutParams(0, 0, 0f);
|
||||
qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
|
||||
fullscreenButton.setImageResource(
|
||||
R.drawable.ic_fullscreen_exit_black_48dp);
|
||||
}
|
||||
statusView.setLayoutParams(statusParams);
|
||||
qrCodeContainer.setLayoutParams(qrCodeParams);
|
||||
cameraOverlay.invalidate();
|
||||
fullscreen = !fullscreen;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,23 +172,29 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
try {
|
||||
cameraView.start();
|
||||
cameraView.start(getScreenRotationDegrees());
|
||||
} catch (CameraException e) {
|
||||
logCameraExceptionAndFinish(e);
|
||||
}
|
||||
// Listen for changes to the Bluetooth state
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STATE_CHANGED);
|
||||
receiver = new BluetoothStateReceiver();
|
||||
getActivity().registerReceiver(receiver, filter);
|
||||
startListening();
|
||||
}
|
||||
|
||||
// Enable BT adapter if it is not already on.
|
||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (adapter != null && !adapter.isEnabled()) {
|
||||
waitingForBluetooth = true;
|
||||
eventBus.broadcast(new EnableBluetoothEvent());
|
||||
} else {
|
||||
startListening();
|
||||
/**
|
||||
* See {@link Camera#setDisplayOrientation(int)}.
|
||||
*/
|
||||
private int getScreenRotationDegrees() {
|
||||
Display d = getActivity().getWindowManager().getDefaultDisplay();
|
||||
switch (d.getRotation()) {
|
||||
case Surface.ROTATION_0:
|
||||
return 0;
|
||||
case Surface.ROTATION_90:
|
||||
return 90;
|
||||
case Surface.ROTATION_180:
|
||||
return 180;
|
||||
case Surface.ROTATION_270:
|
||||
return 270;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +202,6 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
stopListening();
|
||||
if (receiver != null) getActivity().unregisterReceiver(receiver);
|
||||
try {
|
||||
cameraView.stop();
|
||||
} catch (CameraException e) {
|
||||
@@ -205,6 +238,15 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
|
||||
@UiThread
|
||||
private void reset() {
|
||||
// If we've stopped the camera view, restart it
|
||||
if (gotRemotePayload) {
|
||||
try {
|
||||
cameraView.start(getScreenRotationDegrees());
|
||||
} catch (CameraException e) {
|
||||
logCameraExceptionAndFinish(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
statusView.setVisibility(INVISIBLE);
|
||||
cameraView.setVisibility(VISIBLE);
|
||||
gotRemotePayload = false;
|
||||
@@ -219,12 +261,17 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Remote payload is " + encoded.length + " bytes");
|
||||
Payload remotePayload = payloadParser.parse(encoded);
|
||||
gotRemotePayload = true;
|
||||
cameraView.stop();
|
||||
cameraView.setVisibility(INVISIBLE);
|
||||
statusView.setVisibility(VISIBLE);
|
||||
status.setText(R.string.connecting_to_device);
|
||||
task.connectAndRunProtocol(remotePayload);
|
||||
} catch (CameraException e) {
|
||||
logCameraExceptionAndFinish(e);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
// TODO show failure
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, "QR Code Invalid", e);
|
||||
reset();
|
||||
Toast.makeText(getActivity(), R.string.qr_code_invalid,
|
||||
LENGTH_LONG).show();
|
||||
}
|
||||
@@ -262,6 +309,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
byte[] encoded = payloadEncoder.encode(payload);
|
||||
if (LOG.isLoggable(INFO))
|
||||
@@ -326,13 +374,8 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
LOG.info("Got result from decoder");
|
||||
// Ignore results until the KeyAgreementTask is ready
|
||||
if (!gotLocalPayload) {
|
||||
return;
|
||||
}
|
||||
if (!gotRemotePayload) {
|
||||
gotRemotePayload = true;
|
||||
qrCodeScanned(result.getText());
|
||||
}
|
||||
if (!gotLocalPayload) return;
|
||||
if (!gotRemotePayload) qrCodeScanned(result.getText());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,16 +383,4 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
protected void finish() {
|
||||
getActivity().getSupportFragmentManager().popBackStack();
|
||||
}
|
||||
|
||||
private class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
@UiThread
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if (state == STATE_ON && waitingForBluetooth) {
|
||||
waitingForBluetooth = false;
|
||||
startListening();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.briarproject.briar.android.login;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
|
||||
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
|
||||
|
||||
@ParametersAreNonnullByDefault
|
||||
public class OpenDatabaseActivity extends BriarActivity
|
||||
implements EventListener {
|
||||
|
||||
@Inject
|
||||
LifecycleManager lifecycleManager;
|
||||
@Inject
|
||||
EventBus eventBus;
|
||||
|
||||
private TextView textView;
|
||||
private ImageView imageView;
|
||||
private boolean showingMigration = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
super.onCreate(state);
|
||||
setContentView(R.layout.activity_open_database);
|
||||
textView = findViewById(R.id.textView);
|
||||
imageView = findViewById(R.id.imageView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
LifecycleState state = lifecycleManager.getLifecycleState();
|
||||
if (state.isAfter(STARTING_SERVICES)) {
|
||||
finishAndStartApp();
|
||||
} else {
|
||||
if (state == MIGRATING_DATABASE) showMigration();
|
||||
eventBus.addListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof LifecycleEvent) {
|
||||
LifecycleState state = ((LifecycleEvent) e).getLifecycleState();
|
||||
if (state.isAfter(STARTING_SERVICES))
|
||||
runOnUiThreadUnlessDestroyed(this::finishAndStartApp);
|
||||
else if (state == MIGRATING_DATABASE)
|
||||
runOnUiThreadUnlessDestroyed(this::showMigration);
|
||||
}
|
||||
}
|
||||
|
||||
private void showMigration() {
|
||||
if (showingMigration) return;
|
||||
textView.setText(R.string.startup_migrate_database);
|
||||
imageView.setImageResource(R.drawable.startup_migration);
|
||||
showingMigration = true;
|
||||
}
|
||||
|
||||
private void finishAndStartApp() {
|
||||
startActivity(new Intent(this, NavDrawerActivity.class));
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -48,7 +47,7 @@ public class SetupActivity extends BaseActivity
|
||||
}
|
||||
|
||||
public void showApp() {
|
||||
Intent i = new Intent(this, NavDrawerActivity.class);
|
||||
Intent i = new Intent(this, OpenDatabaseActivity.class);
|
||||
i.setFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
supportFinishAfterTransition();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.android.panic;
|
||||
package org.briarproject.briar.android.logout;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.briarproject.briar.android.logout;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
|
||||
public class HideUiActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.android.fragment;
|
||||
package org.briarproject.briar.android.logout;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -7,15 +7,17 @@ import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SignOutFragment extends BaseFragment {
|
||||
|
||||
private static final String TAG = SignOutFragment.class.getName();
|
||||
public static final String TAG = SignOutFragment.class.getName();
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
public View onCreateView(@Nonnull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_sign_out, container, false);
|
||||
@@ -30,5 +32,4 @@ public class SignOutFragment extends BaseFragment {
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
// no need to inject
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
@@ -22,6 +23,7 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.plugin.BluetoothConstants;
|
||||
import org.briarproject.bramble.api.plugin.LanTcpConstants;
|
||||
import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
@@ -35,7 +37,7 @@ import org.briarproject.briar.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.fragment.SignOutFragment;
|
||||
import org.briarproject.briar.android.logout.SignOutFragment;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
|
||||
import org.briarproject.briar.android.settings.SettingsActivity;
|
||||
@@ -46,13 +48,12 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.os.Build.MANUFACTURER;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.v4.app.FragmentManager.POP_BACK_STACK_INCLUSIVE;
|
||||
import static android.support.v4.view.GravityCompat.START;
|
||||
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
|
||||
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO;
|
||||
@@ -75,6 +76,8 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
|
||||
@Inject
|
||||
NavDrawerController controller;
|
||||
@Inject
|
||||
LifecycleManager lifecycleManager;
|
||||
|
||||
private DrawerLayout drawerLayout;
|
||||
private NavigationView navigation;
|
||||
@@ -130,7 +133,9 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
initializeTransports(getLayoutInflater());
|
||||
transportsView.setAdapter(transportsAdapter);
|
||||
|
||||
if (state == null) {
|
||||
if (lifecycleManager.getLifecycleState().isAfter(RUNNING)) {
|
||||
showSignOutFragment();
|
||||
} else if (state == null) {
|
||||
startFragment(ContactListFragment.newInstance(),
|
||||
R.id.nav_btn_contacts);
|
||||
}
|
||||
@@ -214,31 +219,23 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
public void onBackPressed() {
|
||||
if (drawerLayout.isDrawerOpen(START)) {
|
||||
drawerLayout.closeDrawer(START);
|
||||
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
|
||||
getSupportFragmentManager()
|
||||
.findFragmentByTag(ContactListFragment.TAG) != null) {
|
||||
if (SDK_INT == 19 && MANUFACTURER.equalsIgnoreCase("Samsung")) {
|
||||
// workaround for #1116 causes splash screen to show again
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
Intent i = new Intent(Intent.ACTION_MAIN);
|
||||
i.addCategory(Intent.CATEGORY_HOME);
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
}
|
||||
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
|
||||
getSupportFragmentManager()
|
||||
.findFragmentByTag(ContactListFragment.TAG) == null) {
|
||||
} else {
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
if (fm.findFragmentByTag(SignOutFragment.TAG) != null) {
|
||||
finish();
|
||||
} else if (fm.getBackStackEntryCount() == 0
|
||||
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) {
|
||||
/*
|
||||
* This makes sure that the first fragment (ContactListFragment) the
|
||||
* user sees is the same as the last fragment the user sees before
|
||||
* exiting. This models the typical Google navigation behaviour such
|
||||
* as in Gmail/Inbox.
|
||||
*/
|
||||
startFragment(ContactListFragment.newInstance(),
|
||||
R.id.nav_btn_contacts);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
startFragment(ContactListFragment.newInstance(),
|
||||
R.id.nav_btn_contacts);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,10 +251,15 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
drawerToggle.onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
private void signOut() {
|
||||
private void showSignOutFragment() {
|
||||
drawerLayout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED);
|
||||
startFragment(new SignOutFragment());
|
||||
}
|
||||
|
||||
private void signOut() {
|
||||
drawerLayout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED);
|
||||
signOut(false);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void startFragment(BaseFragment fragment, int itemId) {
|
||||
|
||||
@@ -74,9 +74,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
||||
if (group.isEmpty()) {
|
||||
postCount.setVisibility(GONE);
|
||||
date.setVisibility(GONE);
|
||||
avatar.setProblem(true);
|
||||
status
|
||||
.setText(ctx.getString(R.string.groups_group_is_empty));
|
||||
status.setText(ctx.getString(R.string.groups_group_is_empty));
|
||||
status.setVisibility(VISIBLE);
|
||||
} else {
|
||||
// Message Count
|
||||
@@ -91,7 +89,6 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
||||
long lastUpdate = group.getTimestamp();
|
||||
date.setText(UiUtils.formatDate(ctx, lastUpdate));
|
||||
date.setVisibility(VISIBLE);
|
||||
avatar.setProblem(false);
|
||||
status.setVisibility(GONE);
|
||||
}
|
||||
remove.setVisibility(GONE);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.briarproject.briar.android.settings;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.preference.CheckBoxPreference;
|
||||
import android.support.v7.preference.ListPreference;
|
||||
import android.support.v7.preference.Preference;
|
||||
@@ -22,8 +23,6 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.BluetoothConstants;
|
||||
import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.plugin.event.DisableBluetoothEvent;
|
||||
import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||
@@ -46,6 +45,10 @@ import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
|
||||
import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
|
||||
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
|
||||
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
|
||||
import static android.provider.Settings.EXTRA_APP_PACKAGE;
|
||||
import static android.provider.Settings.EXTRA_CHANNEL_ID;
|
||||
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
@@ -55,6 +58,10 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_ALWAYS;
|
||||
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
|
||||
@@ -130,35 +137,14 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
"pref_key_notify_lock_screen");
|
||||
notifySound = findPreference("pref_key_notify_sound");
|
||||
|
||||
setSettingsEnabled(false);
|
||||
|
||||
enableBluetooth.setOnPreferenceChangeListener(this);
|
||||
torNetwork.setOnPreferenceChangeListener(this);
|
||||
notifyPrivateMessages.setOnPreferenceChangeListener(this);
|
||||
notifyGroupMessages.setOnPreferenceChangeListener(this);
|
||||
notifyForumPosts.setOnPreferenceChangeListener(this);
|
||||
notifyBlogPosts.setOnPreferenceChangeListener(this);
|
||||
notifyVibration.setOnPreferenceChangeListener(this);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (SDK_INT >= 21) {
|
||||
notifyLockscreen.setVisible(true);
|
||||
notifyLockscreen.setOnPreferenceChangeListener(this);
|
||||
}
|
||||
notifySound.setOnPreferenceClickListener(preference -> {
|
||||
String title = getString(R.string.choose_ringtone_title);
|
||||
Intent i = new Intent(ACTION_RINGTONE_PICKER);
|
||||
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
|
||||
i.putExtra(EXTRA_RINGTONE_TITLE, title);
|
||||
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_NOTIFICATION_URI);
|
||||
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
|
||||
Uri uri;
|
||||
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
uri = DEFAULT_NOTIFICATION_URI;
|
||||
else uri = Uri.parse(ringtoneUri);
|
||||
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
|
||||
}
|
||||
startActivityForResult(i, REQUEST_RINGTONE);
|
||||
return true;
|
||||
});
|
||||
|
||||
findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
|
||||
preference -> {
|
||||
@@ -220,39 +206,105 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
enableBluetooth.setValue(Boolean.toString(btSetting));
|
||||
torNetwork.setValue(Integer.toString(torSetting));
|
||||
|
||||
notifyPrivateMessages.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_PRIVATE, true));
|
||||
|
||||
notifyGroupMessages.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_GROUP, true));
|
||||
|
||||
notifyForumPosts.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_FORUM, true));
|
||||
|
||||
notifyBlogPosts.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_BLOG, true));
|
||||
|
||||
notifyVibration.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_VIBRATION, true));
|
||||
|
||||
notifyLockscreen.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_LOCK_SCREEN, false));
|
||||
|
||||
String text;
|
||||
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
|
||||
String ringtoneName = settings.get(PREF_NOTIFY_RINGTONE_NAME);
|
||||
if (StringUtils.isNullOrEmpty(ringtoneName)) {
|
||||
text = getString(R.string.notify_sound_setting_default);
|
||||
if (SDK_INT < 26) {
|
||||
notifyPrivateMessages.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_PRIVATE, true));
|
||||
notifyGroupMessages.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_GROUP, true));
|
||||
notifyForumPosts.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_FORUM, true));
|
||||
notifyBlogPosts.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_BLOG, true));
|
||||
notifyVibration.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_VIBRATION, true));
|
||||
notifyPrivateMessages.setOnPreferenceChangeListener(this);
|
||||
notifyGroupMessages.setOnPreferenceChangeListener(this);
|
||||
notifyForumPosts.setOnPreferenceChangeListener(this);
|
||||
notifyBlogPosts.setOnPreferenceChangeListener(this);
|
||||
notifyVibration.setOnPreferenceChangeListener(this);
|
||||
notifyLockscreen.setChecked(settings.getBoolean(
|
||||
PREF_NOTIFY_LOCK_SCREEN, false));
|
||||
notifySound.setOnPreferenceClickListener(
|
||||
pref -> onNotificationSoundClicked());
|
||||
String text;
|
||||
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
|
||||
String ringtoneName =
|
||||
settings.get(PREF_NOTIFY_RINGTONE_NAME);
|
||||
if (StringUtils.isNullOrEmpty(ringtoneName)) {
|
||||
text = getString(R.string.notify_sound_setting_default);
|
||||
} else {
|
||||
text = ringtoneName;
|
||||
}
|
||||
} else {
|
||||
text = ringtoneName;
|
||||
text = getString(R.string.notify_sound_setting_disabled);
|
||||
}
|
||||
notifySound.setSummary(text);
|
||||
} else {
|
||||
text = getString(R.string.notify_sound_setting_disabled);
|
||||
setupNotificationPreference(notifyPrivateMessages,
|
||||
CONTACT_CHANNEL_ID,
|
||||
R.string.notify_private_messages_setting_summary_26);
|
||||
setupNotificationPreference(notifyGroupMessages,
|
||||
GROUP_CHANNEL_ID,
|
||||
R.string.notify_group_messages_setting_summary_26);
|
||||
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
|
||||
R.string.notify_forum_posts_setting_summary_26);
|
||||
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
|
||||
R.string.notify_blog_posts_setting_summary_26);
|
||||
notifyVibration.setVisible(false);
|
||||
notifyLockscreen.setVisible(false);
|
||||
notifySound.setVisible(false);
|
||||
}
|
||||
notifySound.setSummary(text);
|
||||
setSettingsEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
private void setSettingsEnabled(boolean enabled) {
|
||||
enableBluetooth.setEnabled(enabled);
|
||||
torNetwork.setEnabled(enabled);
|
||||
notifyPrivateMessages.setEnabled(enabled);
|
||||
notifyGroupMessages.setEnabled(enabled);
|
||||
notifyForumPosts.setEnabled(enabled);
|
||||
notifyBlogPosts.setEnabled(enabled);
|
||||
notifyVibration.setEnabled(enabled);
|
||||
notifyLockscreen.setEnabled(enabled);
|
||||
notifySound.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
private void setupNotificationPreference(CheckBoxPreference pref,
|
||||
String channelId, @StringRes int summary) {
|
||||
pref.setWidgetLayoutResource(0);
|
||||
pref.setSummary(summary);
|
||||
pref.setOnPreferenceClickListener(clickedPref -> {
|
||||
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
.putExtra(EXTRA_APP_PACKAGE, getContext().getPackageName())
|
||||
.putExtra(EXTRA_CHANNEL_ID, channelId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onNotificationSoundClicked() {
|
||||
String title = getString(R.string.choose_ringtone_title);
|
||||
Intent i = new Intent(ACTION_RINGTONE_PICKER);
|
||||
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
|
||||
i.putExtra(EXTRA_RINGTONE_TITLE, title);
|
||||
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
|
||||
DEFAULT_NOTIFICATION_URI);
|
||||
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
|
||||
Uri uri;
|
||||
String ringtoneUri =
|
||||
settings.get(PREF_NOTIFY_RINGTONE_URI);
|
||||
if (StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
uri = DEFAULT_NOTIFICATION_URI;
|
||||
else uri = Uri.parse(ringtoneUri);
|
||||
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
|
||||
}
|
||||
startActivityForResult(i, REQUEST_RINGTONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void triggerFeedback() {
|
||||
androidExecutor.runOnBackgroundThread(() -> ACRA.getErrorReporter()
|
||||
.handleException(new UserFeedback(), false));
|
||||
@@ -262,7 +314,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
public boolean onPreferenceChange(Preference preference, Object o) {
|
||||
if (preference == enableBluetooth) {
|
||||
boolean btSetting = Boolean.valueOf((String) o);
|
||||
enableOrDisableBluetooth(btSetting);
|
||||
storeBluetoothSettings(btSetting);
|
||||
} else if (preference == torNetwork) {
|
||||
int torSetting = Integer.valueOf((String) o);
|
||||
@@ -295,11 +346,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
return true;
|
||||
}
|
||||
|
||||
private void enableOrDisableBluetooth(boolean enable) {
|
||||
if (enable) eventBus.broadcast(new EnableBluetoothEvent());
|
||||
else eventBus.broadcast(new DisableBluetoothEvent());
|
||||
}
|
||||
|
||||
private void storeTorSettings(int torSetting) {
|
||||
listener.runOnDbThread(() -> {
|
||||
try {
|
||||
|
||||
@@ -12,8 +12,8 @@ import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.controller.ConfigController;
|
||||
import org.briarproject.briar.android.login.OpenDatabaseActivity;
|
||||
import org.briarproject.briar.android.login.SetupActivity;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -43,10 +43,15 @@ public class SplashScreenActivity extends BaseActivity {
|
||||
|
||||
setContentView(R.layout.splash);
|
||||
|
||||
new Handler().postDelayed(() -> {
|
||||
startNextActivity();
|
||||
supportFinishAfterTransition();
|
||||
}, 500);
|
||||
if (configController.accountSignedIn()) {
|
||||
startActivity(new Intent(this, OpenDatabaseActivity.class));
|
||||
finish();
|
||||
} else {
|
||||
new Handler().postDelayed(() -> {
|
||||
startNextActivity();
|
||||
supportFinishAfterTransition();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,7 +65,7 @@ public class SplashScreenActivity extends BaseActivity {
|
||||
startActivity(new Intent(this, ExpiredActivity.class));
|
||||
} else {
|
||||
if (configController.accountExists()) {
|
||||
startActivity(new Intent(this, NavDrawerActivity.class));
|
||||
startActivity(new Intent(this, OpenDatabaseActivity.class));
|
||||
} else {
|
||||
configController.deleteAccount(this);
|
||||
startActivity(new Intent(this, SetupActivity.class));
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.support.annotation.ColorRes;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import static android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE;
|
||||
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
|
||||
|
||||
@@ -17,6 +19,9 @@ public class BriarNotificationBuilder extends NotificationCompat.Builder {
|
||||
// Auto-cancel does not fire the delete intent, see
|
||||
// https://issuetracker.google.com/issues/36961721
|
||||
setAutoCancel(true);
|
||||
|
||||
setLights(ContextCompat.getColor(context, R.color.briar_green_light),
|
||||
750, 500);
|
||||
}
|
||||
|
||||
public BriarNotificationBuilder setColorRes(@ColorRes int res) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -25,6 +24,8 @@ import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.view.ArticleMovementMethod;
|
||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
||||
@@ -47,6 +48,8 @@ import static android.text.format.DateUtils.WEEK_IN_MILLIS;
|
||||
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||
import static org.briarproject.briar.android.BriarApplication.EXPIRY_DATE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class UiUtils {
|
||||
|
||||
public static final long MIN_DATE_RESOLUTION = MINUTE_IN_MILLIS;
|
||||
@@ -110,7 +113,9 @@ public class UiUtils {
|
||||
return Html.fromHtml(s);
|
||||
}
|
||||
|
||||
public static void makeLinksClickable(TextView v) {
|
||||
public static void makeLinksClickable(TextView v,
|
||||
@Nullable FragmentManager fm) {
|
||||
if (fm == null) return;
|
||||
SpannableStringBuilder ssb = new SpannableStringBuilder(v.getText());
|
||||
URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class);
|
||||
for (URLSpan span : spans) {
|
||||
@@ -122,8 +127,6 @@ public class UiUtils {
|
||||
@Override
|
||||
public void onClick(View v2) {
|
||||
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
|
||||
FragmentManager fm = ((AppCompatActivity) v2.getContext())
|
||||
.getSupportFragmentManager();
|
||||
f.show(fm, f.getUniqueTag());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -26,7 +25,6 @@ public class TextAvatarView extends FrameLayout {
|
||||
private final AppCompatTextView character;
|
||||
private final CircleImageView background;
|
||||
private final TextView badge;
|
||||
private int unreadCount;
|
||||
|
||||
public TextAvatarView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -49,30 +47,14 @@ public class TextAvatarView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setUnreadCount(int count) {
|
||||
unreadCount = count;
|
||||
if (count > 0) {
|
||||
badge.setBackgroundResource(R.drawable.bubble);
|
||||
badge.setText(String.valueOf(count));
|
||||
badge.setTextColor(ContextCompat.getColor(getContext(),
|
||||
R.color.briar_text_primary_inverse));
|
||||
badge.setVisibility(VISIBLE);
|
||||
} else {
|
||||
badge.setVisibility(INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setProblem(boolean problem) {
|
||||
if (problem) {
|
||||
badge.setBackgroundResource(R.drawable.bubble_problem);
|
||||
badge.setText("!");
|
||||
badge.setTextColor(ContextCompat
|
||||
.getColor(getContext(), R.color.briar_primary));
|
||||
badge.setVisibility(VISIBLE);
|
||||
} else {
|
||||
setUnreadCount(unreadCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void setBackgroundBytes(byte[] bytes) {
|
||||
int r = getByte(bytes, 0) * 3 / 4 + 96;
|
||||
int g = getByte(bytes, 1) * 3 / 4 + 96;
|
||||
|
||||
@@ -21,6 +21,12 @@ public interface AndroidNotificationManager {
|
||||
String PREF_NOTIFY_VIBRATION = "notifyVibration";
|
||||
String PREF_NOTIFY_LOCK_SCREEN = "notifyLockScreen";
|
||||
|
||||
// Channel IDs
|
||||
String CONTACT_CHANNEL_ID = "contacts";
|
||||
String GROUP_CHANNEL_ID = "groups";
|
||||
String FORUM_CHANNEL_ID = "forums";
|
||||
String BLOG_CHANNEL_ID = "blogs";
|
||||
|
||||
// Content URIs for pending intents
|
||||
String CONTACT_URI = "content://org.briarproject.briar/contact";
|
||||
String GROUP_URI = "content://org.briarproject.briar/group";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<vector android:alpha="0.54" android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners
|
||||
android:radius="@dimen/unread_bubble_size"/>
|
||||
|
||||
<padding
|
||||
android:left="@dimen/unread_bubble_padding_horizontal"
|
||||
android:right="@dimen/unread_bubble_padding_horizontal"/>
|
||||
|
||||
<solid
|
||||
android:color="@color/briar_gold"/>
|
||||
|
||||
<stroke
|
||||
android:color="@color/briar_primary"
|
||||
android:width="@dimen/avatar_border_width"/>
|
||||
|
||||
</shape>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<vector android:height="48dp" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<vector android:height="48dp" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/>
|
||||
</vector>
|
||||
9
briar-android/src/main/res/drawable/startup_lock.xml
Normal file
9
briar-android/src/main/res/drawable/startup_lock.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1 -2.73,2.71 -2.73,7.08 0,9.79 2.73,2.71 7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29 -3.51,3.48 -9.21,3.48 -12.72,0 -3.5,-3.47 -3.53,-9.11 -0.02,-12.58 3.51,-3.47 9.14,-3.47 12.65,0L21,3v7.12zM12.5,8v4.25l3.5,2.08 -0.72,1.21L11,13V8h1.5z"/>
|
||||
</vector>
|
||||
@@ -1,79 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="0dp"
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:adjustViewBounds="true"
|
||||
android:paddingBottom="@dimen/margin_activity_vertical"
|
||||
android:paddingEnd="@dimen/margin_activity_horizontal"
|
||||
android:paddingLeft="@dimen/margin_activity_horizontal"
|
||||
android:paddingRight="@dimen/margin_activity_horizontal"
|
||||
android:paddingStart="@dimen/margin_activity_horizontal"
|
||||
android:paddingTop="@dimen/margin_activity_vertical"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/qr_code_intro"/>
|
||||
android:padding="@dimen/margin_large">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
<ImageView
|
||||
android:id="@+id/diagram"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:adjustViewBounds="true"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/qr_code_intro"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/explanationText"
|
||||
app:layout_constraintBottom_toBottomOf="@id/explanationText"/>
|
||||
|
||||
<LinearLayout
|
||||
<ImageView
|
||||
android:id="@+id/explanationImage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:adjustViewBounds="true"
|
||||
android:paddingTop="@dimen/margin_large"
|
||||
android:paddingLeft="@dimen/margin_large"
|
||||
android:paddingRight="@dimen/margin_large"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/qr_code_explanation"
|
||||
tools:ignore="ContentDescription"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/diagram"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/explanationText"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/explanationText"
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:padding="@dimen/margin_large"
|
||||
android:text="@string/face_to_face"
|
||||
app:layout_constraintTop_toBottomOf="@id/explanationImage"
|
||||
app:layout_constraintStart_toEndOf="@id/diagram"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/explanationBorder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/border_explanation"
|
||||
app:layout_constraintTop_toTopOf="@id/explanationImage"
|
||||
app:layout_constraintStart_toStartOf="@id/explanationImage"
|
||||
app:layout_constraintEnd_toEndOf="@id/explanationImage"
|
||||
app:layout_constraintBottom_toBottomOf="@id/explanationText"/>
|
||||
|
||||
<android.support.constraint.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="diagram,explanationBorder"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/continueButton"
|
||||
style="@style/BriarButton.Default"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/margin_activity_vertical"
|
||||
android:paddingEnd="@dimen/margin_activity_horizontal"
|
||||
android:paddingLeft="@dimen/margin_activity_horizontal"
|
||||
android:paddingRight="@dimen/margin_activity_horizontal"
|
||||
android:paddingStart="@dimen/margin_activity_horizontal"
|
||||
android:paddingTop="@dimen/margin_activity_vertical">
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:text="@string/continue_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/border_explanation"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/margin_large">
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:src="@drawable/qr_code_explanation"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BriarTextBody"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:text="@string/face_to_face"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/continueButton"
|
||||
style="@style/BriarButton.Default"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:text="@string/continue_button"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.briarproject.briar.android.keyagreement.CameraView
|
||||
android:id="@+id/camera_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/camera_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/status_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/background_light"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:visibility="invisible">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connect_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="@dimen/margin_large"
|
||||
tools:text="Connection failed"/>
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/qr_code_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true"
|
||||
android:contentDescription="@string/qr_code"
|
||||
android:scaleType="fitCenter"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/fullscreen_button"
|
||||
android:background="?selectableItemBackground"
|
||||
android:src="@drawable/ic_fullscreen_black_48dp"
|
||||
android:alpha="0.54"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_small"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:contentDescription="@string/show_qr_code_fullscreen"/>
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/container_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/white"
|
||||
android:visibility="invisible">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/title_progress_bar"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:gravity="center"
|
||||
android:paddingTop="@dimen/margin_large"
|
||||
tools:text="@string/waiting_for_contact_to_scan"/>
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user