Compare commits

..

52 Commits

Author SHA1 Message Date
akwizgran
d1cda4fb1e Add recently connected state to core and UI. 2020-05-05 15:39:09 +01:00
akwizgran
8fd9a40ffb Merge branch 'discover-bt-address-from-incoming-connection' into 'master'
Discover remote Bluetooth address from connection

See merge request briar/briar!1244
2020-04-29 15:31:30 +00:00
akwizgran
fb918457d4 Use constants for metadata keys. 2020-04-29 15:37:21 +01:00
akwizgran
b5fe55faf3 Validate remote address. 2020-04-29 15:28:27 +01:00
akwizgran
7320099494 Also store properties discovered from outgoing connections.
This is useful when adding a Bluetooth address is discovered while
adding a contact.
2020-04-28 17:56:01 +01:00
akwizgran
346bec94e8 Discover contacts' BT addresses from incoming connections. 2020-04-28 17:45:17 +01:00
akwizgran
856ec61759 Merge branch '1722-lastChatActivity' into 'master'
Include last private chat activity in list of contacts

Closes #1722

See merge request briar/briar!1242
2020-04-27 09:20:08 +00:00
Torsten Grote
f61e2b399e [headless] Fix unit tests by passing only timestamp into OutputContact 2020-04-20 09:44:51 -03:00
Nico Alt
6135f9152f Include last private chat activity in list of contacts
Fixes #1722.
2020-04-08 12:00:00 +00:00
Torsten Grote
84584d4d3c Merge branch 'tor-0.3.5.10' into 'master'
Upgrade Tor to version 0.3.5.10

Closes #1714

See merge request briar/briar!1241
2020-03-31 13:14:21 +00:00
akwizgran
17239810c8 Upgrade Tor to version 0.3.5.10. 2020-03-25 17:09:12 +00:00
Torsten Grote
9eee58657e Merge branch '1696-keystore-crash' into 'master'
Show a dialog instead of crashing if a hardware-backed key can't be loaded

Closes #1696

See merge request briar/briar!1233
2020-03-12 12:12:42 +00:00
Torsten Grote
76425455b8 Merge branch 'logging-for-account-bugs' into 'master'
Add logging to track down account bugs

See merge request briar/briar!1239
2020-03-11 14:18:50 +00:00
akwizgran
9ea7140a7f Add logging to track down account bugs. 2020-03-11 14:06:48 +00:00
akwizgran
bde9800c89 Add annotation for visibility. 2020-03-11 13:54:01 +00:00
Torsten Grote
4e5b6ed3e0 Merge branch '1367-db-race' into 'master'
Don't infer anything from existence of (possibly empty) DB directory

Closes #1528 and #1367

See merge request briar/briar!1238
2020-03-10 14:59:06 +00:00
akwizgran
77d037f061 Update javadocs. 2020-03-10 11:27:54 +00:00
Torsten Grote
676f5faef4 Merge branch 'fix-wifi-connectivity-misreporting' into 'master'
Fix misreporting of wifi status in LAN plugin

See merge request briar/briar!1237
2020-03-06 16:52:20 +00:00
akwizgran
8e21068465 Fix misreporting of wifi status in LAN plugin. 2020-03-06 13:35:06 +00:00
akwizgran
4a68e5347d Merge branch '1582-fix-climbing-snackbar' into 'master'
Fix climbing snackbar

Closes #1582

See merge request briar/briar!1223
2020-03-03 14:42:20 +00:00
Torsten Grote
27dd383496 Merge branch '1371-protect-code-cache-directory' into 'master'
Protect cache and code_cache directories when deleting account

Closes #1545 and #1371

See merge request briar/briar!1231
2020-02-26 14:03:39 +00:00
akwizgran
ed50582e27 Show a dialog if the DB key can't be decrypted due to a keystore error. 2020-02-25 15:00:49 +00:00
akwizgran
1546a05568 Catch exception if hardware-backed key can't be loaded. 2020-02-25 12:28:21 +00:00
akwizgran
4bdf966e67 Test that code_cache directory isn't deleted. 2020-02-25 11:23:07 +00:00
akwizgran
e1e67f3b2e Clear the cache directory but don't delete it. 2020-02-25 11:18:50 +00:00
akwizgran
1d63b16ff1 Don't delete the code_cache directory when deleting account.
This seems to avoid the disappearing account bug when installing a new
version.
2020-02-25 10:14:31 +00:00
akwizgran
618ab1f1ec Don't infer anything from existence of (possibly empty) DB directory. 2020-02-24 17:51:59 +00:00
Torsten Grote
421f0ebfa5 Merge branch 'network-prefix-length' into 'master'
Use network prefix length to determine which addresses are connectable

Closes #1178

See merge request briar/briar!1230
2020-02-19 13:11:24 +00:00
akwizgran
61db5d1b04 Make bit-twiddling code more readable. 2020-02-19 09:52:13 +00:00
akwizgran
b3d4012527 Use network prefix length to determine which addresses are connectable. 2020-02-18 11:22:29 +00:00
Torsten Grote
60172331ee Merge branch 'ipv4-link-local' into 'master'
Add support for IPv4 link-local addresses

See merge request briar/briar!1229
2020-02-17 12:42:01 +00:00
akwizgran
076debdc4b Merge branch '1328-reuse-port' into 'master'
Choose port in advance when providing wifi access point

Closes #1328

See merge request briar/briar!1228
2020-02-17 12:37:29 +00:00
akwizgran
ed13cbca6a Add support for IPv4 link-local addresses. 2020-02-17 11:42:13 +00:00
akwizgran
49cb1d0612 Choose port in advance when providing wifi access point. 2020-02-14 16:56:00 +00:00
akwizgran
eb562f8f6b Bump version numbers for 1.2.7 release. 2020-02-14 09:51:14 +00:00
Torsten Grote
d9b3ee7f77 Merge branch '1707-fragment-listeners' into 'master'
Don't overwrite listener references with null during fragment changes

Closes #1707, #1706, #1704, and #1697

See merge request briar/briar!1227
2020-02-13 17:47:09 +00:00
akwizgran
c206b46e28 Don't overwrite listener references with null during fragment changes. 2020-02-13 15:58:26 +00:00
akwizgran
62ef64db11 Bump version numbers for 1.2.6 release. 2020-02-13 11:33:18 +00:00
akwizgran
c2e83dd21d Update translations. 2020-02-13 11:32:19 +00:00
akwizgran
48048dd2fd Merge branch '1483-crash-logging' into 'master'
Log the role we find when failing to parse creator session

See merge request briar/briar!1225
2020-02-12 17:26:32 +00:00
akwizgran
17335811ec Merge branch '1699-no-browser' into 'master'
Check if browser intent resolves before starting

Closes #1699

See merge request briar/briar!1226
2020-02-12 14:48:58 +00:00
Torsten Grote
9946fe806a [android] check if browser intent resolves before starting
This prevents a crash on systems without a browser
2020-02-12 10:43:59 -03:00
Torsten Grote
748d249771 [core] log the role when failing to parse creator session 2020-02-12 09:31:16 -03:00
akwizgran
68d6b4b2ac Merge branch '1665-recyclerview-selection' into 'master'
Upgrade recyclerview and selection library to fix crashes

Closes #1665

See merge request briar/briar!1224
2020-02-12 11:24:22 +00:00
Torsten Grote
cf48efae34 [android] upgrade recyclerview and selection library 2020-02-12 08:02:25 -03:00
akwizgran
287be6aa3f Merge branch '1695-show-no-internet-snackbar-when-tor-disabled' into 'master'
Show "No Internet" snackbar when Tor plugin is not active

Closes #1695

See merge request briar/briar!1222
2020-02-11 17:28:05 +00:00
Torsten Grote
1e4ad67ffc [android] Fix climbing snackbar
Use a fresh snackbar for pending contacts each time it needs to be
shown. Don't re-use the old instance and clear it in onStop().
2020-02-11 13:25:15 -03:00
Torsten Grote
c976dd02ae [android] Show "No Internet" snackbar when Tor plugin is not active 2020-02-11 12:59:28 -03:00
Torsten Grote
c4761c3bb2 Merge branch 'ignore-ble-for-bt-discovery' into 'master'
Ignore BLE-only devices during BT discovery

See merge request briar/briar!1221
2020-02-07 13:18:53 +00:00
Torsten Grote
0ff182b5af Merge branch 'message-tree-thread-safety' into 'master'
Ensure MessageTreeImpl#contains() is thread-safe

See merge request briar/briar!1213
2020-01-23 11:02:55 +00:00
akwizgran
b904b6ea51 Ensure MessageTreeImpl#contains() is thread-safe. 2020-01-23 10:14:35 +00:00
akwizgran
bd478c5074 Ignore BLE-only devices during BT discovery. 2019-12-12 17:24:09 +00:00
127 changed files with 1900 additions and 1089 deletions

View File

@@ -11,8 +11,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 10205
versionName "1.2.5"
versionCode 10207
versionName "1.2.7"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -38,7 +38,7 @@ configurations {
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.9@zip'
tor 'org.briarproject:tor-android:0.3.5.10@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.identity.IdentityManager;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
@@ -20,6 +21,7 @@ import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.util.IoUtils.deleteFileOrDir;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
@@ -30,6 +32,12 @@ class AndroidAccountManager extends AccountManagerImpl
private static final Logger LOG =
Logger.getLogger(AndroidAccountManager.class.getName());
/**
* Directories that shouldn't be deleted when deleting the user's account.
*/
private static final List<String> PROTECTED_DIR_NAMES =
asList("cache", "code_cache", "lib", "shared_prefs");
protected final Context appContext;
private final SharedPreferences prefs;
@@ -81,7 +89,7 @@ class AndroidAccountManager extends AccountManagerImpl
if (!prefs.edit().clear().commit())
LOG.warning("Could not clear shared preferences");
}
// Delete files, except lib and shared_prefs directories
// Delete files, except protected directories
Set<File> files = new HashSet<>();
File dataDir = getDataDir();
@Nullable
@@ -90,14 +98,12 @@ class AndroidAccountManager extends AccountManagerImpl
LOG.warning("Could not list files in app data dir");
} else {
for (File file : fileArray) {
String name = file.getName();
if (!name.equals("lib") && !name.equals("shared_prefs")) {
if (!PROTECTED_DIR_NAMES.contains(file.getName())) {
files.add(file);
}
}
}
files.add(appContext.getFilesDir());
files.add(appContext.getCacheDir());
addIfNotNull(files, appContext.getExternalCacheDir());
if (SDK_INT >= 19) {
for (File file : appContext.getExternalCacheDirs()) {
@@ -109,12 +115,16 @@ class AndroidAccountManager extends AccountManagerImpl
addIfNotNull(files, file);
}
}
// Clear the cache directory but don't delete it
File cacheDir = appContext.getCacheDir();
File[] children = cacheDir.listFiles();
if (children != null) files.addAll(asList(children));
for (File file : files) {
if (LOG.isLoggable(INFO)) {
LOG.info("Deleting " + file.getAbsolutePath());
}
deleteFileOrDir(file);
}
// Recreate the cache dir as some OpenGL drivers expect it to exist
if (!new File(dataDir, "cache").mkdirs())
LOG.warning("Could not recreate cache dir");
}
private File getDataDir() {

View File

@@ -32,6 +32,7 @@ import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -76,9 +77,9 @@ class AndroidNetworkManager implements NetworkManager, Service {
filter.addAction(ACTION_SCREEN_ON);
filter.addAction(ACTION_SCREEN_OFF);
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
appContext.registerReceiver(networkStateReceiver, filter);
}
@Override
@@ -136,7 +137,8 @@ class AndroidNetworkManager implements NetworkManager, Service {
}
private boolean isApEvent(@Nullable String action) {
return WIFI_AP_STATE_CHANGED_ACTION.equals(action);
return WIFI_AP_STATE_CHANGED_ACTION.equals(action) ||
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action);
}
}
}

View File

@@ -24,7 +24,6 @@ import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
@@ -47,7 +46,10 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothDevice.ACTION_FOUND;
import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE;
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Collections.shuffle;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -240,11 +242,15 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
break;
} else if (ACTION_FOUND.equals(action)) {
BluetoothDevice d = i.getParcelableExtra(EXTRA_DEVICE);
String address = d.getAddress();
if (LOG.isLoggable(INFO))
LOG.info("Discovered " + scrubMacAddress(address));
if (!addresses.contains(address))
addresses.add(address);
// Ignore Bluetooth LE devices
if (SDK_INT < 18 || d.getType() != DEVICE_TYPE_LE) {
String address = d.getAddress();
if (LOG.isLoggable(INFO))
LOG.info("Discovered " +
scrubMacAddress(address));
if (!addresses.contains(address))
addresses.add(address);
}
}
now = clock.currentTimeMillis();
}
@@ -260,7 +266,7 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
appContext.unregisterReceiver(receiver);
}
// Shuffle the addresses so we don't always try the same one first
Collections.shuffle(addresses);
shuffle(addresses);
return addresses;
}

View File

@@ -10,6 +10,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.util.AndroidUtils.isValidBluetoothAddress;
@NotNullByDefault
class AndroidBluetoothTransportConnection
extends AbstractDuplexTransportConnection {
@@ -23,6 +26,8 @@ class AndroidBluetoothTransportConnection
super(plugin);
this.connectionManager = connectionManager;
this.socket = socket;
String address = socket.getRemoteDevice().getAddress();
if (isValidBluetoothAddress(address)) remote.put(PROP_ADDRESS, address);
}
@Override

View File

@@ -19,7 +19,7 @@ import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -40,19 +40,6 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
private static final Logger LOG =
getLogger(AndroidLanTcpPlugin.class.getName());
private static final byte[] WIFI_AP_ADDRESS_BYTES =
{(byte) 192, (byte) 168, 43, 1};
private static final InetAddress WIFI_AP_ADDRESS;
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 Executor connectionStatusExecutor;
private final ConnectivityManager connectivityManager;
@Nullable
@@ -62,8 +49,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
AndroidLanTcpPlugin(Executor ioExecutor, Context appContext,
Backoff backoff, PluginCallback callback, int maxLatency,
int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
int maxIdleTime, int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
// Don't execute more than one connection status check at a time
connectionStatusExecutor =
new PoliteExecutor("AndroidLanTcpPlugin", ioExecutor, 1);
@@ -79,6 +67,7 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
@Override
public void start() {
if (used.getAndSet(true)) throw new IllegalStateException();
initialisePortProperty();
running = true;
updateConnectionStatus();
}
@@ -95,16 +84,19 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
}
@Override
protected Collection<InetAddress> getLocalIpAddresses() {
protected List<InetAddress> getUsableLocalInetAddresses() {
// 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
// If we're connected to a wifi network, return its address
WifiInfo info = wifiManager.getConnectionInfo();
if (info != null && info.getIpAddress() != 0)
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);
for (InetAddress addr : getLocalInetAddresses()) {
if (addr.equals(WIFI_AP_ADDRESS)) return singletonList(addr);
if (addr.equals(WIFI_DIRECT_AP_ADDRESS)) return singletonList(addr);
}
// No suitable addresses
return emptyList();
}
@@ -144,8 +136,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
private void updateConnectionStatus() {
connectionStatusExecutor.execute(() -> {
if (!running) return;
Collection<InetAddress> addrs = getLocalIpAddresses();
if (addrs.contains(WIFI_AP_ADDRESS)) {
List<InetAddress> addrs = getUsableLocalInetAddresses();
if (addrs.contains(WIFI_AP_ADDRESS)
|| addrs.contains(WIFI_DIRECT_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

View File

@@ -21,10 +21,11 @@ import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
@NotNullByDefault
public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
private static final int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int CONNECTION_TIMEOUT = 3_000; // 3 seconds
private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
@@ -55,7 +56,8 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
AndroidLanTcpPlugin plugin = new AndroidLanTcpPlugin(ioExecutor,
appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME);
appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME,
CONNECTION_TIMEOUT);
eventBus.addListener(plugin);
return plugin;
}

View File

@@ -71,7 +71,7 @@ public class AndroidUtils {
return new Pair<>("", "");
}
private static boolean isValidBluetoothAddress(@Nullable String address) {
public static boolean isValidBluetoothAddress(@Nullable String address) {
return !StringUtils.isNullOrEmpty(address)
&& BluetoothAdapter.checkBluetoothAddress(address)
&& !address.equals(FAKE_BLUETOOTH_ADDRESS);

View File

@@ -72,7 +72,9 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
@Test
public void testDeleteAccountClearsSharedPrefsAndDeletesFiles()
throws Exception {
// Directories 'lib' and 'shared_prefs' should be spared
// Directories 'code_cache', 'lib' and 'shared_prefs' should be spared
File codeCacheDir = new File(testDir, "code_cache");
File codeCacheFile = new File(codeCacheDir, "file");
File libDir = new File(testDir, "lib");
File libFile = new File(libDir, "file");
File sharedPrefsDir = new File(testDir, "shared_prefs");
@@ -111,6 +113,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertTrue(dbDir.mkdirs());
assertTrue(keyDir.mkdirs());
assertTrue(codeCacheDir.mkdirs());
assertTrue(codeCacheFile.createNewFile());
assertTrue(libDir.mkdirs());
assertTrue(libFile.createNewFile());
assertTrue(sharedPrefsDir.mkdirs());
@@ -126,6 +130,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
assertFalse(dbDir.exists());
assertFalse(keyDir.exists());
assertTrue(codeCacheDir.exists());
assertTrue(codeCacheFile.exists());
assertTrue(libDir.exists());
assertTrue(libFile.exists());
assertTrue(sharedPrefsDir.exists());

View File

@@ -70,7 +70,7 @@ dependencyVerification {
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
'org.briarproject:tor-android:0.3.5.9:tor-android-0.3.5.9.zip:853b0440feccd6904bd03e6b2de53a62ebcde1d58068beeadc447a7dff950bc8',
'org.briarproject:tor-android:0.3.5.10:tor-android-0.3.5.10.zip:edd83bf557fcff2105eaa0bdb3f607a6852ebe7360920929ae3039dd5f4774c5',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.groovy:groovy-all:2.4.15:groovy-all-2.4.15.jar:51d6c4e71782e85674239189499854359d380fb75e1a703756e3aaa5b98a5af0',

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.api.account;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -13,7 +14,8 @@ public interface AccountManager {
* Returns true if the manager has the database key. This will be false
* before {@link #createAccount(String, String)} or {@link #signIn(String)}
* has been called, and true after {@link #createAccount(String, String)}
* or {@link #signIn(String)} has returned true, until the process exits.
* or {@link #signIn(String)} has returned true, until
* {@link #deleteAccount()} is called or the process exits.
*/
boolean hasDatabaseKey();
@@ -22,25 +24,22 @@ public interface AccountManager {
* before {@link #createAccount(String, String)} or {@link #signIn(String)}
* has been called, and non-null after
* {@link #createAccount(String, String)} or {@link #signIn(String)} has
* returned true, until the process exits.
* returned true, until {@link #deleteAccount()} is called or the process
* exits.
*/
@Nullable
SecretKey getDatabaseKey();
/**
* Returns true if the encrypted database key can be loaded from disk, and
* the database directory exists and is a directory.
* Returns true if the encrypted database key can be loaded from disk.
*/
boolean accountExists();
/**
* Creates an identity with the given name and registers it with the
* {@link IdentityManager}. Creates a database key, encrypts it with the
* given password and stores it on disk.
* <p/>
* This method does not create the database directory, so
* {@link #accountExists()} will continue to return false until the
* database directory is created.
* given password and stores it on disk. {@link #accountExists()} will
* return true after this method returns true.
*/
boolean createAccount(String name, String password);
@@ -54,17 +53,19 @@ public interface AccountManager {
* Loads the encrypted database key from disk and decrypts it with the
* given password.
*
* @return true if the database key was successfully loaded and decrypted.
* @throws DecryptionException If the database key could not be loaded and
* decrypted.
*/
boolean signIn(String password);
void signIn(String password) throws DecryptionException;
/**
* Loads the encrypted database key from disk, decrypts it with the old
* password, encrypts it with the new password, and stores it on disk,
* replacing the old key.
*
* @return true if the database key was successfully loaded, re-encrypted
* and stored.
* @throws DecryptionException If the database key could not be loaded and
* decrypted.
*/
boolean changePassword(String oldPassword, String newPassword);
void changePassword(String oldPassword, String newPassword)
throws DecryptionException;
}

View File

@@ -142,16 +142,17 @@ public interface CryptoComponent {
/**
* Decrypts and authenticates the given ciphertext that has been read from
* storage. The encryption and authentication keys are derived from the
* given password. Returns null if the ciphertext cannot be decrypted and
* authenticated (for example, if the password is wrong).
* given password.
*
* @param keyStrengthener Used to strengthen the password-based key. If
* null, or if strengthening was not used when encrypting the ciphertext,
* the password-based key will not be strengthened
* @throws DecryptionException If the ciphertext cannot be decrypted and
* authenticated (for example, if the password is wrong).
*/
@Nullable
byte[] decryptWithPassword(byte[] ciphertext, String password,
@Nullable KeyStrengthener keyStrengthener);
@Nullable KeyStrengthener keyStrengthener)
throws DecryptionException;
/**
* Returns true if the given ciphertext was encrypted using a strengthened

View File

@@ -0,0 +1,17 @@
package org.briarproject.bramble.api.crypto;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class DecryptionException extends Exception {
private final DecryptionResult result;
public DecryptionException(DecryptionResult result) {
this.result = result;
}
public DecryptionResult getDecryptionResult() {
return result;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.bramble.api.crypto;
/**
* The result of a password-based decryption operation.
*/
public enum DecryptionResult {
/**
* Decryption succeeded.
*/
SUCCESS,
/**
* Decryption failed because the format of the ciphertext was invalid.
*/
INVALID_CIPHERTEXT,
/**
* Decryption failed because the {@link KeyStrengthener} used for
* encryption was not available for decryption.
*/
KEY_STRENGTHENER_ERROR,
/**
* Decryption failed because the password used for decryption did not match
* the password used for encryption.
*/
INVALID_PASSWORD
}

View File

@@ -5,8 +5,7 @@ import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionStatusChangedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
@@ -21,15 +20,15 @@ public interface ConnectionRegistry {
/**
* Registers a connection with the given contact over the given transport.
* Broadcasts {@link ConnectionOpenedEvent}. Also broadcasts
* {@link ContactConnectedEvent} if this is the only connection with the
* contact.
* {@link ConnectionStatusChangedEvent} if this is the only connection with
* the contact.
*/
void registerConnection(ContactId c, TransportId t, boolean incoming);
/**
* Unregisters a connection with the given contact over the given transport.
* Broadcasts {@link ConnectionClosedEvent}. Also broadcasts
* {@link ContactDisconnectedEvent} if this is the only connection with
* {@link ConnectionStatusChangedEvent} if this is the only connection with
* the contact.
*/
void unregisterConnection(ContactId c, TransportId t, boolean incoming);
@@ -45,9 +44,9 @@ public interface ConnectionRegistry {
boolean isConnected(ContactId c, TransportId t);
/**
* Returns true if the given contact is connected via any transport.
* Returns the connection status of the given contact via all transports.
*/
boolean isConnected(ContactId c);
ConnectionStatus getConnectionStatus(ContactId c);
/**
* Registers a connection with the given pending contact. Broadcasts

View File

@@ -0,0 +1,5 @@
package org.briarproject.bramble.api.plugin;
public enum ConnectionStatus {
CONNECTED, RECENTLY_CONNECTED, DISCONNECTED
}

View File

@@ -4,10 +4,10 @@ public interface LanTcpConstants {
TransportId ID = new TransportId("org.briarproject.bramble.lan");
// a transport property (shared with contacts)
// Transport properties (shared with contacts)
String PROP_IP_PORTS = "ipPorts";
String PROP_PORT = "port";
// a local setting
// A local setting
String PREF_LAN_IP_PORTS = "ipPorts";
}

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException;
import java.io.InputStream;
@@ -14,6 +15,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AbstractDuplexTransportConnection
implements DuplexTransportConnection {
protected final TransportProperties remote = new TransportProperties();
private final Plugin plugin;
private final Reader reader;
private final Writer writer;
@@ -44,6 +47,11 @@ public abstract class AbstractDuplexTransportConnection
return writer;
}
@Override
public TransportProperties getRemoteProperties() {
return remote;
}
private class Reader implements TransportConnectionReader {
@Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
/**
* An interface for reading and writing data over a duplex transport. The
@@ -23,4 +24,10 @@ public interface DuplexTransportConnection {
* for writing to the connection.
*/
TransportConnectionWriter getWriter();
/**
* Returns a possibly empty set of {@link TransportProperties} describing
* the remote peer.
*/
TransportProperties getRemoteProperties();
}

View File

@@ -3,24 +3,31 @@ package org.briarproject.bramble.api.plugin.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a contact connects that was not previously
* connected via any transport.
* An event that is broadcast when a contact's connection status changes.
*/
@Immutable
@NotNullByDefault
public class ContactConnectedEvent extends Event {
public class ConnectionStatusChangedEvent extends Event {
private final ContactId contactId;
private final ConnectionStatus status;
public ContactConnectedEvent(ContactId contactId) {
public ConnectionStatusChangedEvent(ContactId contactId,
ConnectionStatus status) {
this.contactId = contactId;
this.status = status;
}
public ContactId getContactId() {
return contactId;
}
public ConnectionStatus getConnectionStatus() {
return status;
}
}

View File

@@ -1,26 +0,0 @@
package org.briarproject.bramble.api.plugin.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a contact disconnects and is no longer
* connected via any transport.
*/
@Immutable
@NotNullByDefault
public class ContactDisconnectedEvent extends Event {
private final ContactId contactId;
public ContactDisconnectedEvent(ContactId contactId) {
this.contactId = contactId;
}
public ContactId getContactId() {
return contactId;
}
}

View File

@@ -11,4 +11,28 @@ public interface TransportPropertyConstants {
* The maximum length of a property's key or value in UTF-8 bytes.
*/
int MAX_PROPERTY_LENGTH = 100;
/**
* Message metadata key for the transport ID of a local or remote update,
* as a BDF string.
*/
String MSG_KEY_TRANSPORT_ID = "transportId";
/**
* Message metadata key for the version number of a local or remote update,
* as a BDF long.
*/
String MSG_KEY_VERSION = "version";
/**
* Message metadata key for whether an update is local or remote, as a BDF
* boolean.
*/
String MSG_KEY_LOCAL = "local";
/**
* Group metadata key for any discovered transport properties of the
* contact, as a BDF dictionary.
*/
String GROUP_KEY_DISCOVERED = "discovered";
}

View File

@@ -34,6 +34,14 @@ public interface TransportPropertyManager {
void addRemoteProperties(Transaction txn, ContactId c,
Map<TransportId, TransportProperties> props) throws DbException;
/**
* Stores the given properties discovered from an incoming transport
* connection. They will be overridden by any properties received while
* adding the contact or synced from the contact.
*/
void addRemotePropertiesFromConnection(ContactId c, TransportId t,
TransportProperties props) throws DbException;
/**
* Returns the local transport properties for all transports.
*/

View File

@@ -117,4 +117,10 @@ public class IoUtils {
throw new IOException(e);
}
}
public static boolean isNonEmptyDirectory(File f) {
if (!f.isDirectory()) return false;
File[] children = f.listFiles();
return children != null && children.length > 0;
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.bramble.account;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
@@ -17,6 +18,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -24,6 +26,7 @@ import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.fromHexString;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -95,7 +98,7 @@ class AccountManagerImpl implements AccountManager {
}
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8"));
new FileInputStream(f), Charset.forName("UTF-8")));
String key = reader.readLine();
reader.close();
return key;
@@ -147,7 +150,7 @@ class AccountManagerImpl implements AccountManager {
@GuardedBy("stateChangeLock")
private void writeDbKeyToFile(String key, File f) throws IOException {
FileOutputStream out = new FileOutputStream(f);
out.write(key.getBytes("UTF-8"));
out.write(key.getBytes(Charset.forName("UTF-8")));
out.flush();
out.close();
}
@@ -155,8 +158,7 @@ class AccountManagerImpl implements AccountManager {
@Override
public boolean accountExists() {
synchronized (stateChangeLock) {
return loadEncryptedDatabaseKey() != null
&& databaseConfig.getDatabaseDirectory().isDirectory();
return loadEncryptedDatabaseKey() != null;
}
}
@@ -193,31 +195,24 @@ class AccountManagerImpl implements AccountManager {
}
@Override
public boolean signIn(String password) {
public void signIn(String password) throws DecryptionException {
synchronized (stateChangeLock) {
SecretKey key = loadAndDecryptDatabaseKey(password);
if (key == null) return false;
databaseKey = key;
return true;
databaseKey = loadAndDecryptDatabaseKey(password);
}
}
@GuardedBy("stateChangeLock")
@Nullable
private SecretKey loadAndDecryptDatabaseKey(String password) {
private SecretKey loadAndDecryptDatabaseKey(String password)
throws DecryptionException {
String hex = loadEncryptedDatabaseKey();
if (hex == null) {
LOG.warning("Failed to load encrypted database key");
return null;
throw new DecryptionException(INVALID_CIPHERTEXT);
}
byte[] ciphertext = fromHexString(hex);
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
keyStrengthener);
if (plaintext == null) {
LOG.info("Failed to decrypt database key");
return null;
}
SecretKey key = new SecretKey(plaintext);
// If the DB key was encrypted with a weak key and a key strengthener
// is now available, re-encrypt the DB key with a strengthened key
@@ -230,10 +225,11 @@ class AccountManagerImpl implements AccountManager {
}
@Override
public boolean changePassword(String oldPassword, String newPassword) {
public void changePassword(String oldPassword, String newPassword)
throws DecryptionException {
synchronized (stateChangeLock) {
SecretKey key = loadAndDecryptDatabaseKey(oldPassword);
return key != null && encryptAndStoreDatabaseKey(key, newPassword);
encryptAndStoreDatabaseKey(key, newPassword);
}
}
}

View File

@@ -7,6 +7,7 @@ import net.i2p.crypto.eddsa.KeyPairGenerator;
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
@@ -39,6 +40,9 @@ import static java.lang.System.arraycopy;
import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -359,16 +363,17 @@ class CryptoComponentImpl implements CryptoComponent {
}
@Override
@Nullable
public byte[] decryptWithPassword(byte[] input, String password,
@Nullable KeyStrengthener keyStrengthener) {
@Nullable KeyStrengthener keyStrengthener)
throws DecryptionException {
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();
int macBytes = cipher.getMacBytes();
// The input contains the format version, salt, cost parameter, IV,
// ciphertext and MAC
if (input.length < 1 + PBKDF_SALT_BYTES + INT_32_BYTES
+ STORAGE_IV_BYTES + macBytes)
return null; // Invalid input
+ STORAGE_IV_BYTES + macBytes) {
throw new DecryptionException(INVALID_CIPHERTEXT);
}
int inputOff = 0;
// Format version
byte formatVersion = input[inputOff];
@@ -376,7 +381,7 @@ class CryptoComponentImpl implements CryptoComponent {
// Check whether we support this format version
if (formatVersion != PBKDF_FORMAT_SCRYPT &&
formatVersion != PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
return null;
throw new DecryptionException(INVALID_CIPHERTEXT);
}
// Salt
byte[] salt = new byte[PBKDF_SALT_BYTES];
@@ -385,8 +390,9 @@ class CryptoComponentImpl implements CryptoComponent {
// Cost parameter
long cost = ByteUtils.readUint32(input, inputOff);
inputOff += INT_32_BYTES;
if (cost < 2 || cost > Integer.MAX_VALUE)
return null; // Invalid cost parameter
if (cost < 2 || cost > Integer.MAX_VALUE) {
throw new DecryptionException(INVALID_CIPHERTEXT);
}
// IV
byte[] iv = new byte[STORAGE_IV_BYTES];
arraycopy(input, inputOff, iv, 0, iv.length);
@@ -394,8 +400,10 @@ class CryptoComponentImpl implements CryptoComponent {
// Derive the decryption key from the password
SecretKey key = passwordBasedKdf.deriveKey(password, salt, (int) cost);
if (formatVersion == PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
if (keyStrengthener == null || !keyStrengthener.isInitialised())
return null; // Can't derive the same strengthened key
if (keyStrengthener == null || !keyStrengthener.isInitialised()) {
// Can't derive the same strengthened key
throw new DecryptionException(KEY_STRENGTHENER_ERROR);
}
key = keyStrengthener.strengthenKey(key);
}
// Initialise the cipher
@@ -411,7 +419,7 @@ class CryptoComponentImpl implements CryptoComponent {
cipher.process(input, inputOff, inputLen, output, 0);
return output;
} catch (GeneralSecurityException e) {
return null; // Invalid ciphertext
throw new DecryptionException(INVALID_PASSWORD);
}
}

View File

@@ -25,6 +25,7 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
import static org.briarproject.bramble.util.IoUtils.isNonEmptyDirectory;
import static org.briarproject.bramble.util.LogUtils.logFileOrDir;
/**
@@ -69,8 +70,9 @@ class H2Database extends JdbcDatabase {
LOG.info("Contents of account directory before opening DB:");
logFileOrDir(LOG, INFO, dir.getParentFile());
}
boolean reopen = !dir.mkdirs();
boolean reopen = isNonEmptyDirectory(dir);
if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen);
if (!reopen && dir.mkdirs()) LOG.info("Created database directory");
super.open("org.h2.Driver", reopen, key, listener);
if (LOG.isLoggable(INFO)) {
LOG.info("Contents of account directory after opening DB:");

View File

@@ -20,9 +20,11 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
import static org.briarproject.bramble.util.IoUtils.isNonEmptyDirectory;
/**
* Contains all the HSQLDB-specific code for the database.
@@ -64,7 +66,10 @@ class HyperSqlDatabase extends JdbcDatabase {
public boolean open(SecretKey key, @Nullable MigrationListener listener)
throws DbException {
this.key = key;
boolean reopen = !config.getDatabaseDirectory().mkdirs();
File dir = config.getDatabaseDirectory();
boolean reopen = isNonEmptyDirectory(dir);
if (LOG.isLoggable(INFO)) LOG.info("Reopening DB: " + reopen);
if (!reopen && dir.mkdirs()) LOG.info("Created database directory");
super.open("org.hsqldb.jdbc.JDBCDriver", reopen, key, listener);
return reopen;
}

View File

@@ -15,6 +15,8 @@ import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.sync.SyncSession;
import org.briarproject.bramble.api.sync.SyncSessionFactory;
import org.briarproject.bramble.api.transport.KeyManager;
@@ -52,6 +54,7 @@ class ConnectionManagerImpl implements ConnectionManager {
private final HandshakeManager handshakeManager;
private final ContactExchangeManager contactExchangeManager;
private final ConnectionRegistry connectionRegistry;
private final TransportPropertyManager transportPropertyManager;
@Inject
ConnectionManagerImpl(@IoExecutor Executor ioExecutor,
@@ -60,7 +63,8 @@ class ConnectionManagerImpl implements ConnectionManager {
SyncSessionFactory syncSessionFactory,
HandshakeManager handshakeManager,
ContactExchangeManager contactExchangeManager,
ConnectionRegistry connectionRegistry) {
ConnectionRegistry connectionRegistry,
TransportPropertyManager transportPropertyManager) {
this.ioExecutor = ioExecutor;
this.keyManager = keyManager;
this.streamReaderFactory = streamReaderFactory;
@@ -69,6 +73,7 @@ class ConnectionManagerImpl implements ConnectionManager {
this.handshakeManager = handshakeManager;
this.contactExchangeManager = contactExchangeManager;
this.connectionRegistry = connectionRegistry;
this.transportPropertyManager = transportPropertyManager;
}
@Override
@@ -269,6 +274,7 @@ class ConnectionManagerImpl implements ConnectionManager {
private final TransportId transportId;
private final TransportConnectionReader reader;
private final TransportConnectionWriter writer;
private final TransportProperties remote;
@Nullable
private volatile SyncSession outgoingSession = null;
@@ -278,6 +284,7 @@ class ConnectionManagerImpl implements ConnectionManager {
this.transportId = transportId;
reader = connection.getReader();
writer = connection.getWriter();
remote = connection.getRemoteProperties();
}
@Override
@@ -313,13 +320,16 @@ class ConnectionManagerImpl implements ConnectionManager {
// Start the outgoing session on another thread
ioExecutor.execute(() -> runOutgoingSession(contactId));
try {
// Store any transport properties discovered from the connection
transportPropertyManager.addRemotePropertiesFromConnection(
contactId, transportId, remote);
// Create and run the incoming session
createIncomingSession(ctx, reader).run();
reader.dispose(false, true);
// Interrupt the outgoing session so it finishes cleanly
SyncSession out = outgoingSession;
if (out != null) out.interrupt();
} catch (IOException e) {
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
onReadError(true);
} finally {
@@ -375,6 +385,7 @@ class ConnectionManagerImpl implements ConnectionManager {
private final TransportId transportId;
private final TransportConnectionReader reader;
private final TransportConnectionWriter writer;
private final TransportProperties remote;
@Nullable
private volatile SyncSession outgoingSession = null;
@@ -385,6 +396,7 @@ class ConnectionManagerImpl implements ConnectionManager {
this.transportId = transportId;
reader = connection.getReader();
writer = connection.getWriter();
remote = connection.getRemoteProperties();
}
@Override
@@ -461,13 +473,16 @@ class ConnectionManagerImpl implements ConnectionManager {
connectionRegistry.registerConnection(contactId, transportId,
false);
try {
// Store any transport properties discovered from the connection
transportPropertyManager.addRemotePropertiesFromConnection(
contactId, transportId, remote);
// Create and run the incoming session
createIncomingSession(ctx, reader).run();
reader.dispose(false, true);
// Interrupt the outgoing session so it finishes cleanly
SyncSession out = outgoingSession;
if (out != null) out.interrupt();
} catch (IOException e) {
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
onReadError();
} finally {

View File

@@ -6,30 +6,41 @@ import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionStatusChangedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.Scheduler;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.Collections.emptyList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.CONNECTED;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.DISCONNECTED;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.RECENTLY_CONNECTED;
@ThreadSafe
@NotNullByDefault
@@ -38,22 +49,30 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
private static final Logger LOG =
getLogger(ConnectionRegistryImpl.class.getName());
private static final long RECENTLY_CONNECTED_MS = MINUTES.toMillis(1);
private static final long EXPIRY_INTERVAL_MS = SECONDS.toMillis(10);
private final EventBus eventBus;
private final Clock clock;
private final Object lock = new Object();
@GuardedBy("lock")
private final Map<TransportId, Multiset<ContactId>> contactConnections;
@GuardedBy("lock")
private final Multiset<ContactId> contactCounts;
private final Map<ContactId, Counter> contactCounts;
@GuardedBy("lock")
private final Set<PendingContactId> connectedPendingContacts;
@Inject
ConnectionRegistryImpl(EventBus eventBus) {
ConnectionRegistryImpl(EventBus eventBus, Clock clock,
@Scheduler ScheduledExecutorService scheduler) {
this.eventBus = eventBus;
this.clock = clock;
contactConnections = new HashMap<>();
contactCounts = new Multiset<>();
contactCounts = new HashMap<>();
connectedPendingContacts = new HashSet<>();
scheduler.scheduleWithFixedDelay(this::expireRecentConnections,
EXPIRY_INTERVAL_MS, EXPIRY_INTERVAL_MS, MILLISECONDS);
}
@Override
@@ -71,12 +90,22 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
contactConnections.put(t, m);
}
m.add(c);
if (contactCounts.add(c) == 1) firstConnection = true;
Counter counter = contactCounts.get(c);
if (counter == null) {
counter = new Counter();
contactCounts.put(c, counter);
}
if (counter.connections == 0) {
counter.disconnectedTime = 0;
firstConnection = true;
}
counter.connections++;
}
eventBus.broadcast(new ConnectionOpenedEvent(c, t, incoming));
if (firstConnection) {
LOG.info("Contact connected");
eventBus.broadcast(new ContactConnectedEvent(c));
eventBus.broadcast(new ConnectionStatusChangedEvent(c, CONNECTED));
}
}
@@ -93,12 +122,22 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
if (m == null || !m.contains(c))
throw new IllegalArgumentException();
m.remove(c);
if (contactCounts.remove(c) == 0) lastConnection = true;
Counter counter = contactCounts.get(c);
if (counter == null || counter.connections == 0) {
throw new IllegalArgumentException();
}
counter.connections--;
if (counter.connections == 0) {
counter.disconnectedTime = clock.currentTimeMillis();
lastConnection = true;
}
}
eventBus.broadcast(new ConnectionClosedEvent(c, t, incoming));
if (lastConnection) {
LOG.info("Contact disconnected");
eventBus.broadcast(new ContactDisconnectedEvent(c));
eventBus.broadcast(
new ConnectionStatusChangedEvent(c, RECENTLY_CONNECTED));
}
}
@@ -106,7 +145,7 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
public Collection<ContactId> getConnectedContacts(TransportId t) {
synchronized (lock) {
Multiset<ContactId> m = contactConnections.get(t);
if (m == null) return Collections.emptyList();
if (m == null) return emptyList();
List<ContactId> ids = new ArrayList<>(m.keySet());
if (LOG.isLoggable(INFO))
LOG.info(ids.size() + " contacts connected: " + t);
@@ -123,9 +162,11 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
}
@Override
public boolean isConnected(ContactId c) {
public ConnectionStatus getConnectionStatus(ContactId c) {
synchronized (lock) {
return contactCounts.contains(c);
Counter counter = contactCounts.get(c);
if (counter == null) return DISCONNECTED;
return counter.connections > 0 ? CONNECTED : RECENTLY_CONNECTED;
}
}
@@ -147,4 +188,36 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
}
eventBus.broadcast(new RendezvousConnectionClosedEvent(p, success));
}
@Scheduler
private void expireRecentConnections() {
long now = clock.currentTimeMillis();
List<ContactId> disconnected = new ArrayList<>();
synchronized (lock) {
Iterator<Entry<ContactId, Counter>> it =
contactCounts.entrySet().iterator();
while (it.hasNext()) {
Entry<ContactId, Counter> e = it.next();
if (e.getValue().isExpired(now)) {
disconnected.add(e.getKey());
it.remove();
}
}
}
for (ContactId c : disconnected) {
eventBus.broadcast(
new ConnectionStatusChangedEvent(c, DISCONNECTED));
}
}
private static class Counter {
private int connections = 0;
private long disconnectedTime = 0;
private boolean isExpired(long now) {
return connections == 0 &&
now - disconnectedTime > RECENTLY_CONNECTED_MS;
}
}
}

View File

@@ -17,18 +17,20 @@ import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.lang.Integer.parseInt;
import static java.util.Collections.addAll;
import static java.util.Collections.emptyList;
import static java.util.Collections.sort;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -37,6 +39,7 @@ import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TR
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_LAN_IP_PORTS;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IP_PORTS;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_PORT;
import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED;
import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -47,15 +50,36 @@ class LanTcpPlugin extends TcpPlugin {
private static final Logger LOG = getLogger(LanTcpPlugin.class.getName());
private static final LanAddressComparator ADDRESS_COMPARATOR =
new LanAddressComparator();
private static final int MAX_ADDRESSES = 4;
private static final String SEPARATOR = ",";
/**
* The IP address of an Android device providing a wifi access point.
*/
protected static final InetAddress WIFI_AP_ADDRESS;
/**
* The IP address of an Android device providing a wifi direct
* legacy mode access point.
*/
protected static final InetAddress WIFI_DIRECT_AP_ADDRESS;
static {
try {
WIFI_AP_ADDRESS = InetAddress.getByAddress(
new byte[] {(byte) 192, (byte) 168, 43, 1});
WIFI_DIRECT_AP_ADDRESS = InetAddress.getByAddress(
new byte[] {(byte) 192, (byte) 168, 49, 1});
} catch (UnknownHostException e) {
// Should only be thrown if the address has an illegal length
throw new AssertionError(e);
}
}
LanTcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback,
int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
int maxLatency, int maxIdleTime, int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
}
@Override
@@ -64,37 +88,81 @@ class LanTcpPlugin extends TcpPlugin {
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses() {
// Use the same address and port as last time if available
public void start() {
if (used.getAndSet(true)) throw new IllegalStateException();
initialisePortProperty();
running = true;
bind();
}
protected void initialisePortProperty() {
TransportProperties p = callback.getLocalProperties();
if (isNullOrEmpty(p.get(PROP_PORT))) {
int port = new Random().nextInt(32768) + 32768;
p.put(PROP_PORT, String.valueOf(port));
callback.mergeLocalProperties(p);
}
}
@Override
protected List<InetSocketAddress> getLocalSocketAddresses() {
TransportProperties p = callback.getLocalProperties();
int preferredPort = parsePortProperty(p.get(PROP_PORT));
String oldIpPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts);
List<InetSocketAddress> locals = new ArrayList<>();
for (InetAddress local : getLocalIpAddresses()) {
if (isAcceptableAddress(local)) {
// If this is the old address, try to use the same port
for (InetSocketAddress old : olds) {
if (old.getAddress().equals(local))
locals.add(new InetSocketAddress(local, old.getPort()));
List<InetSocketAddress> fallbacks = new ArrayList<>();
for (InetAddress local : getUsableLocalInetAddresses()) {
// If we've used this address before, try to use the same port
int port = preferredPort;
for (InetSocketAddress old : olds) {
if (old.getAddress().equals(local)) {
port = old.getPort();
break;
}
locals.add(new InetSocketAddress(local, 0));
}
locals.add(new InetSocketAddress(local, port));
// Fall back to any available port
fallbacks.add(new InetSocketAddress(local, 0));
}
sort(locals, ADDRESS_COMPARATOR);
locals.addAll(fallbacks);
return locals;
}
private int parsePortProperty(@Nullable String portProperty) {
if (isNullOrEmpty(portProperty)) return 0;
try {
return parseInt(portProperty);
} catch (NumberFormatException e) {
return 0;
}
}
private List<InetSocketAddress> parseSocketAddresses(String ipPorts) {
if (isNullOrEmpty(ipPorts)) return emptyList();
String[] split = ipPorts.split(SEPARATOR);
List<InetSocketAddress> addresses = new ArrayList<>();
for (String ipPort : split) {
if (isNullOrEmpty(ipPorts)) return addresses;
for (String ipPort : ipPorts.split(SEPARATOR)) {
InetSocketAddress a = parseSocketAddress(ipPort);
if (a != null) addresses.add(a);
}
return addresses;
}
protected List<InetAddress> getUsableLocalInetAddresses() {
List<InterfaceAddress> ifAddrs =
new ArrayList<>(getLocalInterfaceAddresses());
// Prefer longer network prefixes
sort(ifAddrs, (a, b) ->
b.getNetworkPrefixLength() - a.getNetworkPrefixLength());
List<InetAddress> addrs = new ArrayList<>();
for (InterfaceAddress ifAddr : ifAddrs) {
InetAddress addr = ifAddr.getAddress();
if (isAcceptableAddress(addr)) addrs.add(addr);
}
return addrs;
}
@Override
protected void setLocalSocketAddress(InetSocketAddress a) {
String ipPort = getIpPortString(a);
@@ -132,7 +200,20 @@ class LanTcpPlugin extends TcpPlugin {
@Override
protected List<InetSocketAddress> getRemoteSocketAddresses(
TransportProperties p) {
return parseSocketAddresses(p.get(PROP_IP_PORTS));
String ipPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> remotes = parseSocketAddresses(ipPorts);
int port = parsePortProperty(p.get(PROP_PORT));
// If the contact has a preferred port, we can guess their IP:port when
// they're providing a wifi access point
if (port != 0) {
InetSocketAddress wifiAp =
new InetSocketAddress(WIFI_AP_ADDRESS, port);
if (!remotes.contains(wifiAp)) remotes.add(wifiAp);
InetSocketAddress wifiDirectAp =
new InetSocketAddress(WIFI_DIRECT_AP_ADDRESS, port);
if (!remotes.contains(wifiDirectAp)) remotes.add(wifiDirectAp);
}
return remotes;
}
private boolean isAcceptableAddress(InetAddress a) {
@@ -145,52 +226,33 @@ class LanTcpPlugin extends TcpPlugin {
}
@Override
protected boolean isConnectable(InetSocketAddress remote) {
protected boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote) {
if (remote.getPort() == 0) return false;
if (!isAcceptableAddress(remote.getAddress())) return false;
// Try to determine whether the address is on the same LAN as us
if (socket == null) return false;
byte[] localIp = socket.getInetAddress().getAddress();
byte[] localIp = local.getAddress().getAddress();
byte[] remoteIp = remote.getAddress().getAddress();
return addressesAreOnSameLan(localIp, remoteIp);
int prefixLength = local.getNetworkPrefixLength();
return areAddressesInSameNetwork(localIp, remoteIp, prefixLength);
}
// Package access for testing
boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) {
// 10.0.0.0/8
if (isPrefix10(localIp)) return isPrefix10(remoteIp);
// 172.16.0.0/12
if (isPrefix172(localIp)) return isPrefix172(remoteIp);
// 192.168.0.0/16
if (isPrefix192(localIp)) return isPrefix192(remoteIp);
// Unrecognised prefix - may be compatible
static boolean areAddressesInSameNetwork(byte[] localIp, byte[] remoteIp,
int prefixLength) {
if (localIp.length != remoteIp.length) return false;
// Compare the first prefixLength bits of the addresses
for (int i = 0; i < prefixLength; i++) {
int byteIndex = i >> 3;
int bitIndex = i & 7; // 0 to 7
int mask = 128 >> bitIndex; // Select the bit at bitIndex
if ((localIp[byteIndex] & mask) != (remoteIp[byteIndex] & mask)) {
return false; // Addresses differ at bit i
}
}
return true;
}
private static boolean isPrefix10(byte[] ipv4) {
return ipv4[0] == 10;
}
private static boolean isPrefix172(byte[] ipv4) {
return ipv4[0] == (byte) 172 && (ipv4[1] & 0xF0) == 16;
}
private static boolean isPrefix192(byte[] ipv4) {
return ipv4[0] == (byte) 192 && ipv4[1] == (byte) 168;
}
// Returns the prefix length for an RFC 1918 address, or 0 for any other
// address
private static int getRfc1918PrefixLength(InetAddress addr) {
if (!(addr instanceof Inet4Address)) return 0;
if (!addr.isSiteLocalAddress()) return 0;
byte[] ipv4 = addr.getAddress();
if (isPrefix10(ipv4)) return 8;
if (isPrefix172(ipv4)) return 12;
if (isPrefix192(ipv4)) return 16;
return 0;
}
@Override
public boolean supportsKeyAgreement() {
return true;
@@ -229,6 +291,12 @@ class LanTcpPlugin extends TcpPlugin {
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (!isRunning()) return null;
ServerSocket ss = socket;
InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress());
if (local == null) {
LOG.warning("No interface for key agreement server socket");
return null;
}
InetSocketAddress remote;
try {
remote = parseSocketAddress(descriptor);
@@ -236,12 +304,11 @@ class LanTcpPlugin extends TcpPlugin {
LOG.info("Invalid IP/port in key agreement descriptor");
return null;
}
if (!isConnectable(remote)) {
if (!isConnectable(local, remote)) {
if (LOG.isLoggable(INFO)) {
SocketAddress local = socket.getLocalSocketAddress();
LOG.info(scrubSocketAddress(remote) +
" is not connectable from " +
scrubSocketAddress(local));
scrubSocketAddress(ss.getLocalSocketAddress()));
}
return null;
}
@@ -249,8 +316,8 @@ class LanTcpPlugin extends TcpPlugin {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.bind(new InetSocketAddress(ss.getInetAddress(), 0));
s.connect(remote, connectionTimeout);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubSocketAddress(remote));
@@ -299,19 +366,4 @@ class LanTcpPlugin extends TcpPlugin {
IoUtils.tryToClose(ss, LOG, WARNING);
}
}
static class LanAddressComparator implements Comparator<InetSocketAddress> {
@Override
public int compare(InetSocketAddress a, InetSocketAddress b) {
// Prefer addresses with non-zero ports
int aPort = a.getPort(), bPort = b.getPort();
if (aPort > 0 && bPort == 0) return -1;
if (aPort == 0 && bPort > 0) return 1;
// Prefer addresses with longer RFC 1918 prefixes
int aPrefix = getRfc1918PrefixLength(a.getAddress());
int bPrefix = getRfc1918PrefixLength(b.getAddress());
return bPrefix - aPrefix;
}
}
}

View File

@@ -18,10 +18,11 @@ import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
@NotNullByDefault
public class LanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
private static final int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int CONNECTION_TIMEOUT = 3_000; // 3 seconds
private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
@@ -48,6 +49,6 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY,
MAX_IDLE_TIME);
MAX_IDLE_TIME, CONNECTION_TIMEOUT);
}
}

View File

@@ -19,10 +19,10 @@ import org.briarproject.bramble.util.IoUtils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
@@ -36,7 +36,6 @@ import java.util.regex.Pattern;
import javax.annotation.Nullable;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
import static java.util.logging.Level.INFO;
@@ -58,7 +57,8 @@ abstract class TcpPlugin implements DuplexPlugin {
protected final Executor ioExecutor, bindExecutor;
protected final Backoff backoff;
protected final PluginCallback callback;
protected final int maxLatency, maxIdleTime, socketTimeout;
protected final int maxLatency, maxIdleTime;
protected final int connectionTimeout, socketTimeout;
protected final AtomicBoolean used = new AtomicBoolean(false);
protected volatile boolean running = false;
@@ -86,15 +86,18 @@ abstract class TcpPlugin implements DuplexPlugin {
/**
* Returns true if connections to the given address can be attempted.
*/
protected abstract boolean isConnectable(InetSocketAddress remote);
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
protected abstract boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote);
TcpPlugin(Executor ioExecutor, Backoff backoff, PluginCallback callback,
int maxLatency, int maxIdleTime) {
int maxLatency, int maxIdleTime, int connectionTimeout) {
this.ioExecutor = ioExecutor;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
this.connectionTimeout = connectionTimeout;
if (maxIdleTime > Integer.MAX_VALUE / 2)
socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2;
@@ -230,13 +233,23 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (!isRunning()) return null;
ServerSocket ss = socket;
InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress());
if (local == null) {
LOG.warning("No interface for server socket");
return null;
}
for (InetSocketAddress remote : getRemoteSocketAddresses(p)) {
if (!isConnectable(remote)) {
// Don't try to connect to our own address
if (!canConnectToOwnAddress() &&
remote.getAddress().equals(ss.getInetAddress())) {
continue;
}
if (!isConnectable(local, remote)) {
if (LOG.isLoggable(INFO)) {
SocketAddress local = socket.getLocalSocketAddress();
LOG.info(scrubSocketAddress(remote) +
" is not connectable from " +
scrubSocketAddress(local));
scrubSocketAddress(ss.getLocalSocketAddress()));
}
continue;
}
@@ -244,8 +257,8 @@ abstract class TcpPlugin implements DuplexPlugin {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubSocketAddress(remote));
Socket s = createSocket();
s.bind(new InetSocketAddress(socket.getInetAddress(), 0));
s.connect(remote);
s.bind(new InetSocketAddress(ss.getInetAddress(), 0));
s.connect(remote, connectionTimeout);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubSocketAddress(remote));
@@ -259,6 +272,19 @@ abstract class TcpPlugin implements DuplexPlugin {
return null;
}
@Nullable
InterfaceAddress getLocalInterfaceAddress(InetAddress a) {
for (InterfaceAddress ifAddr : getLocalInterfaceAddresses()) {
if (ifAddr.getAddress().equals(a)) return ifAddr;
}
return null;
}
// Override for testing
protected boolean canConnectToOwnAddress() {
return false;
}
protected Socket createSocket() throws IOException {
return new Socket();
}
@@ -314,14 +340,27 @@ abstract class TcpPlugin implements DuplexPlugin {
throw new UnsupportedOperationException();
}
Collection<InetAddress> getLocalIpAddresses() {
List<InterfaceAddress> getLocalInterfaceAddresses() {
List<InterfaceAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : getNetworkInterfaces()) {
addrs.addAll(iface.getInterfaceAddresses());
}
return addrs;
}
List<InetAddress> getLocalInetAddresses() {
List<InetAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : getNetworkInterfaces()) {
addrs.addAll(list(iface.getInetAddresses()));
}
return addrs;
}
private List<NetworkInterface> getNetworkInterfaces() {
try {
Enumeration<NetworkInterface> ifaces = getNetworkInterfaces();
if (ifaces == null) return emptyList();
List<InetAddress> addrs = new ArrayList<>();
for (NetworkInterface iface : list(ifaces))
addrs.addAll(list(iface.getInetAddresses()));
return addrs;
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
logException(LOG, WARNING, e);
return emptyList();

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.properties.TransportProperties;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -29,8 +30,10 @@ class WanTcpPlugin extends TcpPlugin {
private volatile MappingResult mappingResult;
WanTcpPlugin(Executor ioExecutor, Backoff backoff, PortMapper portMapper,
PluginCallback callback, int maxLatency, int maxIdleTime) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
PluginCallback callback, int maxLatency, int maxIdleTime,
int connectionTimeout) {
super(ioExecutor, backoff, callback, maxLatency, maxIdleTime,
connectionTimeout);
this.portMapper = portMapper;
}
@@ -45,7 +48,7 @@ class WanTcpPlugin extends TcpPlugin {
TransportProperties p = callback.getLocalProperties();
InetSocketAddress old = parseSocketAddress(p.get(PROP_IP_PORT));
List<InetSocketAddress> addrs = new LinkedList<>();
for (InetAddress a : getLocalIpAddresses()) {
for (InetAddress a : getLocalInetAddresses()) {
if (isAcceptableAddress(a)) {
// If this is the old address, try to use the same port
if (old != null && old.getAddress().equals(a))
@@ -86,7 +89,8 @@ class WanTcpPlugin extends TcpPlugin {
}
@Override
protected boolean isConnectable(InetSocketAddress remote) {
protected boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote) {
if (remote.getPort() == 0) return false;
return isAcceptableAddress(remote.getAddress());
}

View File

@@ -19,10 +19,11 @@ import static org.briarproject.bramble.api.plugin.WanTcpConstants.ID;
@NotNullByDefault
public class WanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins
private static final int MAX_LATENCY = 30_000; // 30 seconds
private static final int MAX_IDLE_TIME = 30_000; // 30 seconds
private static final int CONNECTION_TIMEOUT = 30_000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60_000; // 1 minute
private static final int MAX_POLLING_INTERVAL = 600_000; // 10 mins
private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor;
@@ -52,6 +53,6 @@ public class WanTcpPluginFactory implements DuplexPluginFactory {
MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new WanTcpPlugin(ioExecutor, backoff,
new PortMapperImpl(shutdownManager), callback, MAX_LATENCY,
MAX_IDLE_TIME);
MAX_IDLE_TIME, CONNECTION_TIMEOUT);
}
}

View File

@@ -37,6 +37,11 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.GROUP_KEY_DISCOVERED;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION;
@Immutable
@NotNullByDefault
class TransportPropertyManagerImpl implements TransportPropertyManager,
@@ -111,10 +116,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
try {
// Find the latest update for this transport, if any
BdfDictionary d = metadataParser.parse(meta);
TransportId t = new TransportId(d.getString("transportId"));
TransportId t = new TransportId(d.getString(MSG_KEY_TRANSPORT_ID));
LatestUpdate latest = findLatest(txn, m.getGroupId(), t, false);
if (latest != null) {
if (d.getLong("version") > latest.version) {
if (d.getLong(MSG_KEY_VERSION) > latest.version) {
// This update is newer - delete the previous update
db.deleteMessage(txn, latest.messageId);
db.deleteMessageMetadata(txn, latest.messageId);
@@ -140,6 +145,27 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
}
}
@Override
public void addRemotePropertiesFromConnection(ContactId c, TransportId t,
TransportProperties props) throws DbException {
if (props.isEmpty()) return;
try {
db.transaction(false, txn -> {
Group g = getContactGroup(db.getContact(txn, c));
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(
txn, g.getId());
BdfDictionary discovered =
meta.getOptionalDictionary(GROUP_KEY_DISCOVERED);
if (discovered == null) discovered = new BdfDictionary();
discovered.putAll(props);
meta.put(GROUP_KEY_DISCOVERED, discovered);
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
});
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public Map<TransportId, TransportProperties> getLocalProperties()
throws DbException {
@@ -203,12 +229,26 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
Group g = getContactGroup(c);
try {
// Find the latest remote update
TransportProperties remote;
LatestUpdate latest = findLatest(txn, g.getId(), t, false);
if (latest == null) return new TransportProperties();
// Retrieve and parse the latest remote properties
BdfList message =
clientHelper.getMessageAsList(txn, latest.messageId);
return parseProperties(message);
if (latest == null) {
remote = new TransportProperties();
} else {
// Retrieve and parse the latest remote properties
BdfList message =
clientHelper.getMessageAsList(txn, latest.messageId);
remote = parseProperties(message);
}
// Merge in any discovered properties
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g.getId());
BdfDictionary d = meta.getOptionalDictionary(GROUP_KEY_DISCOVERED);
if (d == null) return remote;
TransportProperties merged =
clientHelper.parseAndValidateTransportProperties(d);
// Received properties override discovered properties
merged.putAll(remote);
return merged;
} catch (FormatException e) {
throw new DbException(e);
}
@@ -281,9 +321,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
long now = clock.currentTimeMillis();
Message m = clientHelper.createMessage(g, now, body);
BdfDictionary meta = new BdfDictionary();
meta.put("transportId", t.getString());
meta.put("version", version);
meta.put("local", local);
meta.put(MSG_KEY_TRANSPORT_ID, t.getString());
meta.put(MSG_KEY_VERSION, version);
meta.put(MSG_KEY_LOCAL, local);
clientHelper.addLocalMessage(txn, m, meta, shared, false);
} catch (FormatException e) {
throw new RuntimeException(e);
@@ -302,8 +342,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
.getMessageMetadataAsDictionary(txn, localGroup.getId());
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue();
TransportId t = new TransportId(meta.getString("transportId"));
long version = meta.getLong("version");
TransportId t =
new TransportId(meta.getString(MSG_KEY_TRANSPORT_ID));
long version = meta.getLong(MSG_KEY_VERSION);
latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
}
return latestUpdates;
@@ -316,9 +357,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
clientHelper.getMessageMetadataAsDictionary(txn, g);
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
BdfDictionary meta = e.getValue();
if (meta.getString("transportId").equals(t.getString())
&& meta.getBoolean("local") == local) {
return new LatestUpdate(e.getKey(), meta.getLong("version"));
if (meta.getString(MSG_KEY_TRANSPORT_ID).equals(t.getString())
&& meta.getBoolean(MSG_KEY_LOCAL) == local) {
return new LatestUpdate(e.getKey(),
meta.getLong(MSG_KEY_VERSION));
}
}
return null;

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.account;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
@@ -19,12 +20,15 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.annotation.Nullable;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getIdentity;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
@@ -35,6 +39,7 @@ import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
public class AccountManagerImplTest extends BrambleMockTestCase {
@@ -83,8 +88,13 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testSignInReturnsFalseIfDbKeyCannotBeLoaded() {
assertFalse(accountManager.signIn(password));
public void testSignInThrowsExceptionIfDbKeyCannotBeLoaded() {
try {
accountManager.signIn(password);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
assertFalse(accountManager.hasDatabaseKey());
assertFalse(keyFile.exists());
@@ -92,11 +102,11 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testSignInReturnsFalseIfPasswordIsWrong() throws Exception {
public void testSignInThrowsExceptionIfPasswordIsWrong() throws Exception {
context.checking(new Expectations() {{
oneOf(crypto).decryptWithPassword(encryptedKey, password,
keyStrengthener);
will(returnValue(null));
will(throwException(new DecryptionException(INVALID_PASSWORD)));
}});
storeDatabaseKey(keyFile, encryptedKeyHex);
@@ -105,7 +115,12 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertFalse(accountManager.signIn(password));
try {
accountManager.signIn(password);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
assertFalse(accountManager.hasDatabaseKey());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
@@ -128,7 +143,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(accountManager.signIn(password));
accountManager.signIn(password);
assertTrue(accountManager.hasDatabaseKey());
SecretKey decrypted = accountManager.getDatabaseKey();
assertNotNull(decrypted);
@@ -157,7 +172,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(accountManager.signIn(password));
accountManager.signIn(password);
assertTrue(accountManager.hasDatabaseKey());
SecretKey decrypted = accountManager.getDatabaseKey();
assertNotNull(decrypted);
@@ -239,55 +254,6 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
assertFalse(keyBackupFile.exists());
}
@Test
public void testAccountExistsReturnsFalseIfDbDirectoryDoesNotExist()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(dbDir.exists());
assertFalse(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertFalse(dbDir.exists());
}
@Test
public void testAccountExistsReturnsFalseIfDbDirectoryIsNotDirectory()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(dbDir.createNewFile());
assertFalse(dbDir.isDirectory());
assertFalse(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(dbDir.exists());
assertFalse(dbDir.isDirectory());
}
@Test
public void testAccountExistsReturnsTrueIfDbDirectoryIsDirectory()
throws Exception {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(dbDir.mkdirs());
assertTrue(dbDir.isDirectory());
assertTrue(accountManager.accountExists());
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
assertTrue(dbDir.exists());
assertTrue(dbDir.isDirectory());
}
@Test
public void testCreateAccountStoresDbKey() throws Exception {
context.checking(new Expectations() {{
@@ -315,26 +281,36 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testChangePasswordReturnsFalseIfDbKeyCannotBeLoaded() {
assertFalse(accountManager.changePassword(password, newPassword));
public void testChangePasswordThrowsExceptionIfDbKeyCannotBeLoaded() {
try {
accountManager.changePassword(password, newPassword);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
assertFalse(keyFile.exists());
assertFalse(keyBackupFile.exists());
}
@Test
public void testChangePasswordReturnsFalseIfPasswordIsWrong()
public void testChangePasswordThrowsExceptionIfPasswordIsWrong()
throws Exception {
context.checking(new Expectations() {{
oneOf(crypto).decryptWithPassword(encryptedKey, password,
keyStrengthener);
will(returnValue(null));
will(throwException(new DecryptionException(INVALID_PASSWORD)));
}});
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertFalse(accountManager.changePassword(password, newPassword));
try {
accountManager.changePassword(password, newPassword);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
@@ -357,7 +333,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
storeDatabaseKey(keyFile, encryptedKeyHex);
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
assertTrue(accountManager.changePassword(password, newPassword));
accountManager.changePassword(password, newPassword);
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyFile));
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyBackupFile));
@@ -366,7 +342,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
private void storeDatabaseKey(File f, String hex) throws IOException {
f.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(f);
out.write(hex.getBytes("UTF-8"));
out.write(hex.getBytes(Charset.forName("UTF-8")));
out.flush();
out.close();
}
@@ -374,7 +350,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
@Nullable
private String loadDatabaseKey(File f) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(f), "UTF-8"));
new FileInputStream(f), Charset.forName("UTF-8")));
String hex = reader.readLine();
reader.close();
return hex;

View File

@@ -1,25 +1,35 @@
package org.briarproject.bramble.crypto;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.KeyStrengthener;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestSecureRandomProvider;
import org.briarproject.bramble.test.TestUtils;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.Random;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class PasswordBasedEncryptionTest extends BrambleTestCase {
public class PasswordBasedEncryptionTest extends BrambleMockTestCase {
private final KeyStrengthener keyStrengthener =
context.mock(KeyStrengthener.class);
private final CryptoComponentImpl crypto =
new CryptoComponentImpl(new TestSecureRandomProvider(),
new ScryptKdf(new SystemClock()));
@Test
public void testEncryptionAndDecryption() {
byte[] input = TestUtils.getRandomBytes(1234);
public void testEncryptionAndDecryption() throws Exception {
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
@@ -27,14 +37,80 @@ public class PasswordBasedEncryptionTest extends BrambleTestCase {
}
@Test
public void testInvalidCiphertextReturnsNull() {
byte[] input = TestUtils.getRandomBytes(1234);
public void testInvalidFormatVersionThrowsException() {
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
// Modify the ciphertext
int position = new Random().nextInt(ciphertext.length);
ciphertext[position] = (byte) (ciphertext[position] ^ 0xFF);
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
assertNull(output);
// Modify the format version
ciphertext[0] ^= (byte) 0xFF;
try {
crypto.decryptWithPassword(ciphertext, password, null);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
}
}
@Test
public void testInvalidPasswordThrowsException() {
byte[] input = getRandomBytes(1234);
byte[] ciphertext = crypto.encryptWithPassword(input, "password", null);
// Try to decrypt with the wrong password
try {
crypto.decryptWithPassword(ciphertext, "wrong", null);
fail();
} catch (DecryptionException expected) {
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
}
}
@Test
public void testMissingKeyStrengthenerThrowsException() {
SecretKey strengthened = getSecretKey();
context.checking(new Expectations() {{
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
will(returnValue(strengthened));
}});
// Use the key strengthener during encryption
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext =
crypto.encryptWithPassword(input, password, keyStrengthener);
// The key strengthener is missing during decryption
try {
crypto.decryptWithPassword(ciphertext, password, null);
fail();
} catch (DecryptionException expected) {
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
}
}
@Test
public void testKeyStrengthenerFailureThrowsException() {
SecretKey strengthened = getSecretKey();
context.checking(new Expectations() {{
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
will(returnValue(strengthened));
oneOf(keyStrengthener).isInitialised();
will(returnValue(false));
}});
// Use the key strengthener during encryption
byte[] input = getRandomBytes(1234);
String password = "password";
byte[] ciphertext =
crypto.encryptWithPassword(input, password, keyStrengthener);
// The key strengthener fails during decryption
try {
crypto.decryptWithPassword(ciphertext, password, keyStrengthener);
fail();
} catch (DecryptionException expected) {
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
}
}
}

View File

@@ -7,18 +7,20 @@ import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionStatusChangedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent;
import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getContactId;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getTransportId;
@@ -30,6 +32,9 @@ import static org.junit.Assert.fail;
public class ConnectionRegistryImplTest extends BrambleMockTestCase {
private final EventBus eventBus = context.mock(EventBus.class);
private final Clock clock = context.mock(Clock.class);
private final ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
private final ContactId contactId = getContactId();
private final ContactId contactId1 = getContactId();
@@ -40,17 +45,25 @@ public class ConnectionRegistryImplTest extends BrambleMockTestCase {
@Test
public void testRegisterAndUnregister() {
ConnectionRegistry c = new ConnectionRegistryImpl(eventBus);
context.checking(new Expectations() {{
oneOf(scheduler).scheduleWithFixedDelay(with(any(Runnable.class)),
with(10_000L), with(10_000L), with(MILLISECONDS));
}});
ConnectionRegistry c = new ConnectionRegistryImpl(eventBus, clock,
scheduler);
context.assertIsSatisfied();
// The registry should be empty
assertEquals(emptyList(), c.getConnectedContacts(transportId));
assertEquals(emptyList(), c.getConnectedContacts(transportId1));
// Check that a registered connection shows up - this should
// broadcast a ConnectionOpenedEvent and a ContactConnectedEvent
// broadcast a ConnectionOpenedEvent and a ConnectionStatusChangedEvent
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(ConnectionOpenedEvent.class)));
oneOf(eventBus).broadcast(with(any(ContactConnectedEvent.class)));
oneOf(eventBus).broadcast(with(any(
ConnectionStatusChangedEvent.class)));
}});
c.registerConnection(contactId, transportId, true);
assertEquals(singletonList(contactId),
@@ -81,11 +94,13 @@ public class ConnectionRegistryImplTest extends BrambleMockTestCase {
context.assertIsSatisfied();
// Unregister the other connection - this should broadcast a
// ConnectionClosedEvent and a ContactDisconnectedEvent
// ConnectionClosedEvent and a ConnectionStatusChangedEvent
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(System.currentTimeMillis()));
oneOf(eventBus).broadcast(with(any(ConnectionClosedEvent.class)));
oneOf(eventBus).broadcast(with(any(
ContactDisconnectedEvent.class)));
ConnectionStatusChangedEvent.class)));
}});
c.unregisterConnection(contactId, transportId, true);
assertEquals(emptyList(), c.getConnectedContacts(transportId));
@@ -102,12 +117,12 @@ public class ConnectionRegistryImplTest extends BrambleMockTestCase {
// Register both contacts with one transport, one contact with both -
// this should broadcast three ConnectionOpenedEvents and two
// ContactConnectedEvents
// ConnectionStatusChangedEvents
context.checking(new Expectations() {{
exactly(3).of(eventBus).broadcast(with(any(
ConnectionOpenedEvent.class)));
exactly(2).of(eventBus).broadcast(with(any(
ContactConnectedEvent.class)));
ConnectionStatusChangedEvent.class)));
}});
c.registerConnection(contactId, transportId, true);
c.registerConnection(contactId1, transportId, true);
@@ -122,7 +137,14 @@ public class ConnectionRegistryImplTest extends BrambleMockTestCase {
@Test
public void testRegisterAndUnregisterPendingContacts() {
ConnectionRegistry c = new ConnectionRegistryImpl(eventBus);
context.checking(new Expectations() {{
oneOf(scheduler).scheduleWithFixedDelay(with(any(Runnable.class)),
with(10_000L), with(10_000L), with(MILLISECONDS));
}});
ConnectionRegistry c = new ConnectionRegistryImpl(eventBus, clock,
scheduler);
context.assertIsSatisfied();
context.checking(new Expectations() {{
oneOf(eventBus).broadcast(with(any(

View File

@@ -7,12 +7,11 @@ import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.plugin.tcp.LanTcpPlugin.LanAddressComparator;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
@@ -22,7 +21,6 @@ import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Comparator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -33,56 +31,89 @@ import static java.util.concurrent.Executors.newCachedThreadPool;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN;
import static org.briarproject.bramble.plugin.tcp.LanTcpPlugin.areAddressesInSameNetwork;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
public class LanTcpPluginTest extends BrambleTestCase {
private final Backoff backoff = new TestBackoff();
private final ExecutorService ioExecutor = newCachedThreadPool();
private Callback callback = null;
private LanTcpPlugin plugin = null;
@Before
public void setUp() {
callback = new Callback();
plugin = new LanTcpPlugin(ioExecutor, backoff, callback, 0, 0, 1000) {
@Override
protected boolean canConnectToOwnAddress() {
return true;
}
};
}
@Test
public void testAddressesAreOnSameLan() {
Callback callback = new Callback();
LanTcpPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
// Local and remote in 10.0.0.0/8 should return true
assertTrue(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
makeAddress(10, 255, 255, 255)));
// Local and remote in 172.16.0.0/12 should return true
assertTrue(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0),
makeAddress(172, 31, 255, 255)));
// Local and remote in 192.168.0.0/16 should return true
assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
makeAddress(192, 168, 255, 255)));
// Local and remote in 169.254.0.0/16 (link-local) should return true
assertTrue(plugin.addressesAreOnSameLan(makeAddress(169, 254, 0, 0),
makeAddress(169, 254, 255, 255)));
// Local and remote in different recognised prefixes should return false
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
makeAddress(172, 31, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
makeAddress(192, 168, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0),
makeAddress(10, 255, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0),
makeAddress(192, 168, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
makeAddress(10, 255, 255, 255)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
makeAddress(172, 31, 255, 255)));
// Remote prefix unrecognised should return false
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
makeAddress(1, 2, 3, 4)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0),
makeAddress(1, 2, 3, 4)));
assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
makeAddress(1, 2, 3, 4)));
// Both prefixes unrecognised should return true (could be link-local)
assertTrue(plugin.addressesAreOnSameLan(makeAddress(1, 2, 3, 4),
makeAddress(5, 6, 7, 8)));
public void testAreAddressesInSameNetwork() {
// Local and remote in 10.0.0.0/8
assertTrue(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
makeAddress(10, 255, 255, 255), 8));
assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
makeAddress(10, 255, 255, 255), 9));
// Local and remote in 172.16.0.0/12
assertTrue(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(172, 31, 255, 255), 12));
assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(172, 31, 255, 255), 13));
// Local and remote in 192.168.0.0/16
assertTrue(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(192, 168, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(192, 168, 255, 255), 17));
// Local and remote in 169.254.0.0/16
assertTrue(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(169, 254, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(169, 254, 255, 255), 17));
// Local in 10.0.0.0/8, remote in a different network
assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
makeAddress(172, 31, 255, 255), 8));
assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
makeAddress(192, 168, 255, 255), 8));
assertFalse(areAddressesInSameNetwork(makeAddress(10, 0, 0, 0),
makeAddress(169, 254, 255, 255), 8));
// Local in 172.16.0.0/12, remote in a different network
assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(10, 255, 255, 255), 12));
assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(192, 168, 255, 255), 12));
assertFalse(areAddressesInSameNetwork(makeAddress(172, 16, 0, 0),
makeAddress(169, 254, 255, 255), 12));
// Local in 192.168.0.0/16, remote in a different network
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(10, 255, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(172, 31, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(192, 168, 0, 0),
makeAddress(169, 254, 255, 255), 16));
// Local in 169.254.0.0/16, remote in a different network
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(10, 255, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(172, 31, 255, 255), 16));
assertFalse(areAddressesInSameNetwork(makeAddress(169, 254, 0, 0),
makeAddress(192, 168, 255, 255), 16));
}
private byte[] makeAddress(int... parts) {
@@ -93,13 +124,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test
public void testIncomingConnection() throws Exception {
if (!systemHasLocalIpv4Address()) {
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
assumeTrue(systemHasLocalIpv4Address());
plugin.start();
// The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -128,13 +153,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test
public void testOutgoingConnection() throws Exception {
if (!systemHasLocalIpv4Address()) {
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
assumeTrue(systemHasLocalIpv4Address());
plugin.start();
// The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -177,13 +196,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test
public void testIncomingKeyAgreementConnection() throws Exception {
if (!systemHasLocalIpv4Address()) {
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
assumeTrue(systemHasLocalIpv4Address());
plugin.start();
assertTrue(callback.propertiesLatch.await(5, SECONDS));
KeyAgreementListener kal =
@@ -225,13 +238,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
@Test
public void testOutgoingKeyAgreementConnection() throws Exception {
if (!systemHasLocalIpv4Address()) {
System.err.println("WARNING: Skipping test, no local IPv4 address");
return;
}
Callback callback = new Callback();
DuplexPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback,
0, 0);
assumeTrue(systemHasLocalIpv4Address());
plugin.start();
// The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -276,62 +283,12 @@ public class LanTcpPluginTest extends BrambleTestCase {
plugin.stop();
}
@Test
public void testComparatorPrefersNonZeroPorts() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress nonZero = new InetSocketAddress("1.2.3.4", 1234);
InetSocketAddress zero = new InetSocketAddress("1.2.3.4", 0);
assertEquals(0, comparator.compare(nonZero, nonZero));
assertTrue(comparator.compare(nonZero, zero) < 0);
assertTrue(comparator.compare(zero, nonZero) > 0);
assertEquals(0, comparator.compare(zero, zero));
}
@Test
public void testComparatorPrefersLongerPrefixes() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
assertEquals(0, comparator.compare(prefix192, prefix192));
assertTrue(comparator.compare(prefix192, prefix172) < 0);
assertTrue(comparator.compare(prefix192, prefix10) < 0);
assertTrue(comparator.compare(prefix172, prefix192) > 0);
assertEquals(0, comparator.compare(prefix172, prefix172));
assertTrue(comparator.compare(prefix172, prefix10) < 0);
assertTrue(comparator.compare(prefix10, prefix192) > 0);
assertTrue(comparator.compare(prefix10, prefix172) > 0);
assertEquals(0, comparator.compare(prefix10, prefix10));
}
@Test
public void testComparatorPrefersSiteLocalToLinkLocal() {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
InetSocketAddress linkLocal = new InetSocketAddress("169.254.0.1", 0);
assertTrue(comparator.compare(prefix192, linkLocal) < 0);
assertTrue(comparator.compare(prefix172, linkLocal) < 0);
assertTrue(comparator.compare(prefix10, linkLocal) < 0);
assertTrue(comparator.compare(linkLocal, prefix192) > 0);
assertTrue(comparator.compare(linkLocal, prefix172) > 0);
assertTrue(comparator.compare(linkLocal, prefix10) > 0);
assertEquals(0, comparator.compare(linkLocal, linkLocal));
}
private boolean systemHasLocalIpv4Address() throws Exception {
for (NetworkInterface i : list(getNetworkInterfaces())) {
for (InetAddress a : list(i.getInetAddresses())) {
if (a instanceof Inet4Address)
if (a instanceof Inet4Address) {
return a.isLinkLocalAddress() || a.isSiteLocalAddress();
}
}
}
return false;
@@ -340,7 +297,9 @@ public class LanTcpPluginTest extends BrambleTestCase {
@NotNullByDefault
private static class Callback implements PluginCallback {
private final CountDownLatch propertiesLatch = new CountDownLatch(1);
// Properties will be stored twice: the preferred port at startup,
// and the IP:port when the server socket is bound
private final CountDownLatch propertiesLatch = new CountDownLatch(2);
private final CountDownLatch connectionsLatch = new CountDownLatch(1);
private final TransportProperties local = new TransportProperties();

View File

@@ -24,14 +24,18 @@ import org.briarproject.bramble.test.DbExpectations;
import org.jmock.Expectations;
import org.junit.Test;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.GROUP_KEY_DISCOVERED;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.CLIENT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyManager.MAJOR_VERSION;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -186,25 +190,25 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Message message = getMessage(contactGroupId);
Metadata meta = new Metadata();
BdfDictionary metaDictionary = BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 2),
new BdfEntry("local", false)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 2),
new BdfEntry(MSG_KEY_LOCAL, false)
);
Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>();
// A remote update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"),
new BdfEntry("version", 1),
new BdfEntry("local", false)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, false)
));
// A local update for the same transport should be ignored
MessageId localUpdateId = new MessageId(getRandomId());
messageMetadata.put(localUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
));
context.checking(new Expectations() {{
@@ -228,18 +232,18 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Metadata meta = new Metadata();
// Version 4 is being delivered
BdfDictionary metaDictionary = BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 4),
new BdfEntry("local", false)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 4),
new BdfEntry(MSG_KEY_LOCAL, false)
);
Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>();
// An older remote update for the same transport should be deleted
MessageId fooVersion3 = new MessageId(getRandomId());
messageMetadata.put(fooVersion3, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 3),
new BdfEntry("local", false)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 3),
new BdfEntry(MSG_KEY_LOCAL, false)
));
context.checking(new Expectations() {{
@@ -265,18 +269,18 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Metadata meta = new Metadata();
// Version 3 is being delivered
BdfDictionary metaDictionary = BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 3),
new BdfEntry("local", false)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 3),
new BdfEntry(MSG_KEY_LOCAL, false)
);
Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>();
// A newer remote update for the same transport should not be deleted
MessageId fooVersion4 = new MessageId(getRandomId());
messageMetadata.put(fooVersion4, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 4),
new BdfEntry("local", false)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 4),
new BdfEntry(MSG_KEY_LOCAL, false)
));
context.checking(new Expectations() {{
@@ -342,9 +346,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// A local update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
));
context.checking(new DbExpectations() {{
@@ -366,16 +370,16 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// A local update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
));
// A local update for the right transport should be returned
MessageId fooUpdateId = new MessageId(getRandomId());
messageMetadata.put(fooUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
));
BdfList fooUpdate = BdfList.of("foo", 1, fooPropertiesDict);
@@ -405,28 +409,28 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
List<Contact> contacts = asList(contact1, contact2);
Group contactGroup1 = getGroup(CLIENT_ID, MAJOR_VERSION);
Group contactGroup2 = getGroup(CLIENT_ID, MAJOR_VERSION);
Map<MessageId, BdfDictionary> messageMetadata2 =
Map<MessageId, BdfDictionary> messageMetadata =
new LinkedHashMap<>();
// A remote update for another transport should be ignored
MessageId barUpdateId = new MessageId(getRandomId());
messageMetadata2.put(barUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "bar"),
new BdfEntry("version", 1),
new BdfEntry("local", false)
messageMetadata.put(barUpdateId, BdfDictionary.of(
new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, false)
));
// A local update for the right transport should be ignored
MessageId localUpdateId = new MessageId(getRandomId());
messageMetadata2.put(localUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
messageMetadata.put(localUpdateId, BdfDictionary.of(
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
));
// A remote update for the right transport should be returned
MessageId fooUpdateId = new MessageId(getRandomId());
messageMetadata2.put(fooUpdateId, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
new BdfEntry("local", false)
messageMetadata.put(fooUpdateId, BdfDictionary.of(
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, false)
));
BdfList fooUpdate = BdfList.of("foo", 1, fooPropertiesDict);
@@ -440,19 +444,25 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(contactGroup1));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup1.getId());
will(returnValue(Collections.emptyMap()));
will(returnValue(emptyMap()));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup1.getId());
will(returnValue(new BdfDictionary()));
// Second contact: returns an update
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact2);
will(returnValue(contactGroup2));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup2.getId());
will(returnValue(messageMetadata2));
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup2.getId());
will(returnValue(new BdfDictionary()));
}});
TransportPropertyManagerImpl t = createInstance();
@@ -463,6 +473,62 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
assertEquals(fooProperties, properties.get(contact2.getId()));
}
@Test
public void testReceivePropertiesOverrideDiscoveredProperties()
throws Exception {
Transaction txn = new Transaction(null, true);
Contact contact = getContact();
List<Contact> contacts = singletonList(contact);
Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
MessageId updateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> messageMetadata = singletonMap(updateId,
BdfDictionary.of(
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, false)
));
BdfList update = BdfList.of("foo", 1, fooPropertiesDict);
TransportProperties discovered = new TransportProperties();
discovered.put("fooKey1", "overridden");
discovered.put("fooKey3", "fooValue3");
BdfDictionary discoveredDict = new BdfDictionary(discovered);
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_DISCOVERED, discoveredDict)
);
TransportProperties merged = new TransportProperties();
merged.putAll(fooProperties);
merged.put("fooKey3", "fooValue3");
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getContacts(txn);
will(returnValue(contacts));
// One update
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
MAJOR_VERSION, contact);
will(returnValue(contactGroup));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(messageMetadata));
oneOf(clientHelper).getMessageAsList(txn, updateId);
will(returnValue(update));
oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict);
will(returnValue(fooProperties));
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(groupMeta));
oneOf(clientHelper).parseAndValidateTransportProperties(
discoveredDict);
will(returnValue(discovered));
}});
TransportPropertyManagerImpl t = createInstance();
Map<ContactId, TransportProperties> properties =
t.getRemoteProperties(new TransportId("foo"));
assertEquals(merged, properties.get(contact.getId()));
}
@Test
public void testMergingUnchangedPropertiesDoesNotCreateUpdate()
throws Exception {
@@ -470,9 +536,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
MessageId updateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> messageMetadata = singletonMap(updateId,
BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
));
BdfList update = BdfList.of("foo", 1, fooPropertiesDict);
@@ -505,7 +571,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// There are no existing properties to merge with
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
localGroup.getId());
will(returnValue(Collections.emptyMap()));
will(returnValue(emptyMap()));
// Store the new properties in the local group, version 1
expectStoreMessage(txn, localGroup.getId(), "foo",
fooPropertiesDict, 1, true, false);
@@ -517,7 +583,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(contactGroup));
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(Collections.emptyMap()));
will(returnValue(emptyMap()));
expectStoreMessage(txn, contactGroup.getId(), "foo",
fooPropertiesDict, 1, true, true);
}});
@@ -532,9 +598,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact = getContact();
Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
BdfDictionary oldMetadata = BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 1),
new BdfEntry("local", true)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 1),
new BdfEntry(MSG_KEY_LOCAL, true)
);
MessageId localGroupUpdateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> localGroupMessageMetadata =
@@ -589,14 +655,14 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
// The latest update for transport "foo" should be returned
MessageId fooVersion999 = new MessageId(getRandomId());
messageMetadata.put(fooVersion999, BdfDictionary.of(
new BdfEntry("transportId", "foo"),
new BdfEntry("version", 999)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "foo"),
new BdfEntry(MSG_KEY_VERSION, 999)
));
// The latest update for transport "bar" should be returned
MessageId barVersion3 = new MessageId(getRandomId());
messageMetadata.put(barVersion3, BdfDictionary.of(
new BdfEntry("transportId", "bar"),
new BdfEntry("version", 3)
new BdfEntry(MSG_KEY_TRANSPORT_ID, "bar"),
new BdfEntry(MSG_KEY_VERSION, 3)
));
BdfList fooUpdate = BdfList.of("foo", 999, fooPropertiesDict);
BdfList barUpdate = BdfList.of("bar", 3, barPropertiesDict);
@@ -627,9 +693,9 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Message message = getMessage(g);
long timestamp = message.getTimestamp();
BdfDictionary meta = BdfDictionary.of(
new BdfEntry("transportId", transportId),
new BdfEntry("version", version),
new BdfEntry("local", local)
new BdfEntry(MSG_KEY_TRANSPORT_ID, transportId),
new BdfEntry(MSG_KEY_VERSION, version),
new BdfEntry(MSG_KEY_LOCAL, local)
);
context.checking(new Expectations() {{

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException;
import java.io.InputStream;
@@ -37,6 +38,11 @@ public class TestDuplexTransportConnection
return writer;
}
@Override
public TransportProperties getRemoteProperties() {
return new TransportProperties();
}
/**
* Creates and returns a pair of TestDuplexTransportConnections that are
* connected to each other.

View File

@@ -16,7 +16,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: '*.jar')
implementation 'net.java.dev.jna:jna:4.5.2'
implementation 'net.java.dev.jna:jna-platform:4.5.2'
tor 'org.briarproject:tor:0.3.5.9@zip'
tor 'org.briarproject:tor:0.3.5.10@zip'
tor 'org.briarproject:obfs4proxy:0.0.7@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'

View File

@@ -24,7 +24,7 @@ dependencyVerification {
'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8',
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.briarproject:obfs4proxy:0.0.7:obfs4proxy-0.0.7.zip:5b2f693262ce43a7e130f7cc7d5d1617925330640a2eb6d71085e95df8ee0642',
'org.briarproject:tor:0.3.5.9:tor-0.3.5.9.zip:6c3994b129db019cc23caaf50d6b4383903c40d05fbc47fc94211170a3e5d38c',
'org.briarproject:tor:0.3.5.10:tor-0.3.5.10.zip:7b387d3523ae8af289c23be59dc4c64ec5d3721385d7825a09705095e3318d5c',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',

View File

@@ -22,8 +22,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 10205
versionName "1.2.5"
versionCode 10207
versionName "1.2.7"
applicationId "org.briarproject.briar.android"
buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
@@ -98,7 +98,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01'
implementation 'ch.acra:acra:4.11'
implementation 'info.guardianproject.panic:panic:1.0'

View File

@@ -28,7 +28,9 @@ import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@RequiresApi(23)
@NotNullByDefault
@@ -79,7 +81,10 @@ class AndroidKeyStrengthener implements KeyStrengthener {
return true;
}
return false;
} catch (GeneralSecurityException | IOException e) {
} catch (GeneralSecurityException e) {
logException(LOG, WARNING, e);
return false;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

View File

@@ -36,6 +36,7 @@ import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog;
@@ -64,7 +65,11 @@ import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONIO
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = {ContactExchangeModule.class, ViewModelModule.class})
@Module(includes = {
ContactExchangeModule.class,
LoginModule.class,
ViewModelModule.class
})
public class AppModule {
static class EagerSingletons {

View File

@@ -8,8 +8,6 @@ import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.BriarControllerImpl;
import org.briarproject.briar.android.controller.DbController;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.login.ChangePasswordController;
import org.briarproject.briar.android.login.ChangePasswordControllerImpl;
import org.briarproject.briar.android.navdrawer.NavDrawerController;
import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl;
@@ -46,13 +44,6 @@ public class ActivityModule {
return setupController;
}
@ActivityScope
@Provides
ChangePasswordController providePasswordController(
ChangePasswordControllerImpl passwordController) {
return passwordController;
}
@ActivityScope
@Provides
protected BriarController provideBriarController(
@@ -80,5 +71,4 @@ public class ActivityModule {
BriarServiceConnection provideBriarServiceConnection() {
return new BriarServiceConnection();
}
}

View File

@@ -92,6 +92,9 @@ public abstract class BaseActivity extends AppCompatActivity
.build();
injectActivity(activityComponent);
super.onCreate(state);
if (LOG.isLoggable(INFO)) {
LOG.info("Creating " + getClass().getSimpleName());
}
// WARNING: When removing this or making it possible to turn it off,
// we need a solution for the app lock feature.
@@ -127,8 +130,9 @@ public abstract class BaseActivity extends AppCompatActivity
@Override
protected void onStart() {
super.onStart();
if (LOG.isLoggable(INFO))
LOG.info("Starting " + this.getClass().getSimpleName());
if (LOG.isLoggable(INFO)) {
LOG.info("Starting " + getClass().getSimpleName());
}
for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityStart();
}
@@ -144,11 +148,28 @@ public abstract class BaseActivity extends AppCompatActivity
return (ScreenFilterDialogFragment) f;
}
@Override
protected void onResume() {
super.onResume();
if (LOG.isLoggable(INFO)) {
LOG.info("Resuming " + getClass().getSimpleName());
}
}
@Override
protected void onPause() {
super.onPause();
if (LOG.isLoggable(INFO)) {
LOG.info("Pausing " + getClass().getSimpleName());
}
}
@Override
protected void onStop() {
super.onStop();
if (LOG.isLoggable(INFO))
LOG.info("Stopping " + this.getClass().getSimpleName());
if (LOG.isLoggable(INFO)) {
LOG.info("Stopping " + getClass().getSimpleName());
}
for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityStop();
}
@@ -203,6 +224,9 @@ public abstract class BaseActivity extends AppCompatActivity
@Override
protected void onDestroy() {
super.onDestroy();
if (LOG.isLoggable(INFO)) {
LOG.info("Destroying " + getClass().getSimpleName());
}
destroyed = true;
for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityDestroy();

View File

@@ -95,12 +95,14 @@ public abstract class BriarActivity extends BaseActivity {
// Also check that the activity isn't finishing already.
// This is possible if we finished in onActivityResult().
// Launching another StartupActivity would cause a loop.
LOG.info("Not signed in, launching StartupActivity");
Intent i = new Intent(this, StartupActivity.class);
startActivityForResult(i, REQUEST_PASSWORD);
} else if (lockManager.isLocked() && !isFinishing()) {
// Also check that the activity isn't finishing already.
// This is possible if we finished in onActivityResult().
// Launching another UnlockActivity would cause a loop.
LOG.info("Locked, launching UnlockActivity");
Intent i = new Intent(this, UnlockActivity.class);
startActivityForResult(i, REQUEST_UNLOCK);
} else if (SDK_INT >= 23) {

View File

@@ -9,7 +9,6 @@ import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import java.util.Collection;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
@@ -18,7 +17,10 @@ public interface BlogController extends BaseController {
void setGroupId(GroupId g);
@UiThread
void setBlogSharingListener(@Nullable BlogSharingListener listener);
void setBlogSharingListener(BlogSharingListener listener);
@UiThread
void unsetBlogSharingListener(BlogSharingListener listener);
void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);

View File

@@ -96,10 +96,15 @@ class BlogControllerImpl extends BaseControllerImpl
}
@Override
public void setBlogSharingListener(@Nullable BlogSharingListener listener) {
public void setBlogSharingListener(BlogSharingListener listener) {
this.listener = listener;
}
@Override
public void unsetBlogSharingListener(BlogSharingListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void eventOccurred(Event e) {
if (groupId == null || listener == null)

View File

@@ -141,7 +141,8 @@ public class BlogFragment extends BaseFragment
@Override
public void onDestroy() {
super.onDestroy();
blogController.setBlogSharingListener(null);
blogController.unsetBlogSharingListener(this);
sharingController.unsetSharingListener(this);
}
@Override

View File

@@ -7,7 +7,6 @@ import org.briarproject.briar.api.blog.Blog;
import java.util.Collection;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
@@ -19,7 +18,10 @@ public interface FeedController extends BaseController {
void loadPersonalBlog(ResultExceptionHandler<Blog, DbException> handler);
@UiThread
void setFeedListener(@Nullable FeedListener listener);
void setFeedListener(FeedListener listener);
@UiThread
void unsetFeedListener(FeedListener listener);
@NotNullByDefault
interface FeedListener extends BlogListener {

View File

@@ -69,10 +69,15 @@ class FeedControllerImpl extends BaseControllerImpl implements FeedController {
}
@Override
public void setFeedListener(@Nullable FeedListener listener) {
public void setFeedListener(FeedListener listener) {
this.listener = listener;
}
@Override
public void unsetFeedListener(FeedListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void eventOccurred(Event e) {
if (listener == null) throw new IllegalStateException();

View File

@@ -134,7 +134,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onDestroy() {
super.onDestroy();
feedController.setFeedListener(null);
feedController.unsetFeedListener(this);
}
@Override

View File

@@ -2,35 +2,38 @@ package org.briarproject.briar.android.contact;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import javax.annotation.concurrent.NotThreadSafe;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.DISCONNECTED;
@NotThreadSafe
@NotNullByDefault
public class ContactItem {
private final Contact contact;
private boolean connected;
private ConnectionStatus status;
public ContactItem(Contact contact) {
this(contact, false);
this(contact, DISCONNECTED);
}
public ContactItem(Contact contact, boolean connected) {
public ContactItem(Contact contact, ConnectionStatus status) {
this.contact = contact;
this.connected = connected;
this.status = status;
}
public Contact getContact() {
return contact;
}
boolean isConnected() {
return connected;
ConnectionStatus getConnectionStatus() {
return status;
}
void setConnected(boolean connected) {
this.connected = connected;
void setConnectionStatus(ConnectionStatus status) {
this.status = status;
}
}

View File

@@ -7,6 +7,7 @@ import android.widget.TextView;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
@@ -16,6 +17,8 @@ import androidx.annotation.UiThread;
import androidx.recyclerview.widget.RecyclerView;
import im.delight.android.identicons.IdenticonDrawable;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.CONNECTED;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.RECENTLY_CONNECTED;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
@UiThread
@@ -27,7 +30,7 @@ public class ContactItemViewHolder<I extends ContactItem>
protected final ImageView avatar;
protected final TextView name;
@Nullable
protected final ImageView bulb;
private final ImageView bulb;
public ContactItemViewHolder(View v) {
super(v);
@@ -47,10 +50,13 @@ public class ContactItemViewHolder<I extends ContactItem>
if (bulb != null) {
// online/offline
if (item.isConnected()) {
bulb.setImageResource(R.drawable.contact_connected);
ConnectionStatus status = item.getConnectionStatus();
if (status == CONNECTED) {
bulb.setImageResource(R.drawable.ic_connected);
} else if (status == RECENTLY_CONNECTED) {
bulb.setImageResource(R.drawable.ic_recently_connected);
} else {
bulb.setImageResource(R.drawable.contact_disconnected);
bulb.setImageResource(R.drawable.ic_disconnected);
}
}

View File

@@ -39,7 +39,7 @@ public class ContactListAdapter extends
if (c1.getTimestamp() != c2.getTimestamp()) {
return false;
}
return c1.isConnected() == c2.isConnected();
return c1.getConnectionStatus() == c2.getConnectionStatus();
}
@Override

View File

@@ -25,8 +25,8 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.bramble.api.plugin.event.ConnectionStatusChangedEvent;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
@@ -53,7 +53,6 @@ import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.LinearLayoutManager;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListener;
@@ -61,7 +60,7 @@ import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListen
import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
import static androidx.core.view.ViewCompat.getTransitionName;
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -87,7 +86,12 @@ public class ContactListFragment extends BaseFragment implements EventListener,
private ContactListAdapter adapter;
private BriarRecyclerView list;
private Snackbar snackbar;
/**
* The Snackbar is non-null when shown and null otherwise.
* Use {@link #showSnackBar()} and {@link #dismissSnackBar()} to interact.
*/
@Nullable
private Snackbar snackbar = null;
// Fields that are accessed from background threads must be volatile
@Inject
@@ -132,26 +136,15 @@ public class ContactListFragment extends BaseFragment implements EventListener,
ContactId contactId = item.getContact().getId();
i.putExtra(CONTACT_ID, contactId.getInt());
Bundle options = null;
// work-around for android bug #224270
if (SDK_INT >= 23 && !isSamsung7()) {
ContactListItemViewHolder holder =
(ContactListItemViewHolder) list
.getRecyclerView()
.findViewHolderForAdapterPosition(
adapter.findItemPosition(item));
Pair<View, String> avatar =
Pair.create(holder.avatar,
getTransitionName(holder.avatar));
Pair<View, String> bulb =
Pair.create(holder.bulb,
getTransitionName(holder.bulb));
ActivityOptionsCompat options =
makeSceneTransitionAnimation(getActivity(),
avatar, bulb);
ActivityCompat.startActivity(getActivity(), i,
options.toBundle());
} else {
// work-around for android bug #224270
options = makeTransitionOptions(view);
}
if (options == null) {
startActivity(i);
} else {
ActivityCompat.startActivity(getActivity(), i, options);
}
};
adapter = new ContactListAdapter(requireContext(),
@@ -163,16 +156,18 @@ public class ContactListFragment extends BaseFragment implements EventListener,
list.setEmptyText(getString(R.string.no_contacts));
list.setEmptyAction(getString(R.string.no_contacts_action));
snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, v ->
startActivity(new Intent(getContext(),
PendingContactListActivity.class)))
.make(contentView, R.string.pending_contact_requests_snackbar,
LENGTH_INDEFINITE);
return contentView;
}
@Nullable
private Bundle makeTransitionOptions(View view) {
View avatar = view.findViewById(R.id.avatarView);
String name = requireNonNull(getTransitionName(avatar));
ActivityOptionsCompat options = makeSceneTransitionAnimation(
requireActivity(), view, name);
return options.toBundle();
}
@Override
public void onMenuItemClick(FloatingActionButton fab, @Nullable TextView v,
int itemId) {
@@ -203,9 +198,9 @@ public class ContactListFragment extends BaseFragment implements EventListener,
listener.runOnDbThread(() -> {
try {
if (contactManager.getPendingContacts().isEmpty()) {
runOnUiThreadUnlessDestroyed(() -> snackbar.dismiss());
runOnUiThreadUnlessDestroyed(this::dismissSnackBar);
} else {
runOnUiThreadUnlessDestroyed(() -> snackbar.show());
runOnUiThreadUnlessDestroyed(this::showSnackBar);
}
} catch (DbException e) {
logException(LOG, WARNING, e);
@@ -220,6 +215,7 @@ public class ContactListFragment extends BaseFragment implements EventListener,
adapter.clear();
list.showProgressBar();
list.stopPeriodicUpdate();
dismissSnackBar();
}
private void loadContacts() {
@@ -233,9 +229,9 @@ public class ContactListFragment extends BaseFragment implements EventListener,
ContactId id = c.getId();
GroupCount count =
conversationManager.getGroupCount(id);
boolean connected =
connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, connected, count));
ConnectionStatus status = connectionRegistry
.getConnectionStatus(c.getId());
contacts.add(new ContactListItem(c, status, count));
} catch (NoSuchContactException e) {
// Continue
}
@@ -266,10 +262,9 @@ public class ContactListFragment extends BaseFragment implements EventListener,
if (e instanceof ContactAddedEvent) {
LOG.info("Contact added, reloading");
loadContacts();
} else if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId(), true);
} else if (e instanceof ContactDisconnectedEvent) {
setConnected(((ContactDisconnectedEvent) e).getContactId(), false);
} else if (e instanceof ConnectionStatusChangedEvent) {
ConnectionStatusChangedEvent c = (ConnectionStatusChangedEvent) e;
setConnectionStatus(c.getContactId(), c.getConnectionStatus());
} else if (e instanceof ContactRemovedEvent) {
LOG.info("Contact removed, removing item");
removeItem(((ContactRemovedEvent) e).getContactId());
@@ -305,14 +300,37 @@ public class ContactListFragment extends BaseFragment implements EventListener,
}
@UiThread
private void setConnected(ContactId c, boolean connected) {
private void setConnectionStatus(ContactId c, ConnectionStatus status) {
adapter.incrementRevision();
int position = adapter.findItemPosition(c);
ContactListItem item = adapter.getItemAt(position);
if (item != null) {
item.setConnected(connected);
item.setConnectionStatus(status);
adapter.updateItemAt(position, item);
}
}
@UiThread
private void showSnackBar() {
if (snackbar != null) return;
View v = requireNonNull(getView());
int stringRes = R.string.pending_contact_requests_snackbar;
snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, view -> showPendingContactList())
.make(v, stringRes, LENGTH_INDEFINITE);
snackbar.show();
}
@UiThread
private void dismissSnackBar() {
if (snackbar == null) return;
snackbar.dismiss();
snackbar = null;
}
private void showPendingContactList() {
Intent i = new Intent(getContext(), PendingContactListActivity.class);
startActivity(i);
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.contact;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
@@ -15,9 +16,9 @@ public class ContactListItem extends ContactItem {
private long timestamp;
private int unread;
public ContactListItem(Contact contact, boolean connected,
public ContactListItem(Contact contact, ConnectionStatus status,
GroupCount count) {
super(contact, connected);
super(contact, status);
this.empty = count.getMsgCount() == 0;
this.unread = count.getUnreadCount();
this.timestamp = count.getLatestMsgTime();

View File

@@ -7,7 +7,6 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import org.briarproject.briar.android.util.UiUtils;
import java.util.Locale;
@@ -15,8 +14,11 @@ import javax.annotation.Nullable;
import androidx.annotation.UiThread;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.core.view.ViewCompat.setTransitionName;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
@UiThread
@NotNullByDefault
@@ -39,10 +41,11 @@ class ContactListItemViewHolder extends ContactItemViewHolder<ContactListItem> {
// unread count
int unreadCount = item.getUnreadCount();
if (unreadCount > 0) {
unread.setText(String.format(Locale.getDefault(), "%d", unreadCount));
unread.setVisibility(View.VISIBLE);
unread.setText(String.format(Locale.getDefault(), "%d",
unreadCount));
unread.setVisibility(VISIBLE);
} else {
unread.setVisibility(View.INVISIBLE);
unread.setVisibility(INVISIBLE);
}
// date of last message
@@ -54,8 +57,7 @@ class ContactListItemViewHolder extends ContactItemViewHolder<ContactListItem> {
}
ContactId c = item.getContact().getId();
setTransitionName(avatar, UiUtils.getAvatarTransitionName(c));
setTransitionName(bulb, UiUtils.getBulbTransitionName(c));
setTransitionName(avatar, getAvatarTransitionName(c));
}
}

View File

@@ -92,7 +92,7 @@ public class PendingContactListViewModel extends AndroidViewModel
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = items.isEmpty();
boolean online = pairs.isEmpty();
for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();

View File

@@ -54,8 +54,6 @@ class PendingContactViewHolder extends ViewHolder {
status.setText(R.string.waiting_for_contact_to_come_online);
break;
case OFFLINE:
color = ContextCompat
.getColor(status.getContext(), R.color.briar_yellow);
status.setText("");
break;
case CONNECTING:

View File

@@ -16,6 +16,12 @@ public interface SharingController {
@UiThread
void setSharingListener(SharingListener listener);
/**
* Unsets the listener.
*/
@UiThread
void unsetSharingListener(SharingListener listener);
/**
* Call this when your lifecycle starts,
* so the listener will be called when information changes.

View File

@@ -6,8 +6,7 @@ import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionStatusChangedEvent;
import java.util.Collection;
import java.util.HashSet;
@@ -18,6 +17,8 @@ import javax.inject.Inject;
import androidx.annotation.UiThread;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.CONNECTED;
@NotNullByDefault
public class SharingControllerImpl implements SharingController, EventListener {
@@ -43,6 +44,11 @@ public class SharingControllerImpl implements SharingController, EventListener {
this.listener = listener;
}
@Override
public void unsetSharingListener(SharingListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void onStart() {
eventBus.addListener(this);
@@ -55,15 +61,14 @@ public class SharingControllerImpl implements SharingController, EventListener {
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId());
} else if (e instanceof ContactDisconnectedEvent) {
setConnected(((ContactDisconnectedEvent) e).getContactId());
if (e instanceof ConnectionStatusChangedEvent) {
ConnectionStatusChangedEvent c = (ConnectionStatusChangedEvent) e;
setConnectionStatus(c.getContactId());
}
}
@UiThread
private void setConnected(ContactId c) {
private void setConnectionStatus(ContactId c) {
if (listener == null) throw new IllegalStateException();
if (contacts.contains(c)) {
int online = getOnlineCount();
@@ -90,7 +95,9 @@ public class SharingControllerImpl implements SharingController, EventListener {
public int getOnlineCount() {
int online = 0;
for (ContactId c : contacts) {
if (connectionRegistry.isConnected(c)) online++;
if (connectionRegistry.getConnectionStatus(c) == CONNECTED) {
online++;
}
}
return online;
}

View File

@@ -13,7 +13,6 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -34,8 +33,8 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.bramble.api.plugin.event.ConnectionStatusChangedEvent;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
@@ -125,6 +124,8 @@ import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.CONNECTED;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.RECENTLY_CONNECTED;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -138,7 +139,6 @@ import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHME
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
@@ -198,8 +198,8 @@ public class ConversationActivity extends BriarActivity
private ConversationAdapter adapter;
private Toolbar toolbar;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private TextView toolbarStatus;
private BriarRecyclerView list;
private LinearLayoutManager layoutManager;
private TextInputView textInputView;
@@ -237,8 +237,8 @@ public class ConversationActivity extends BriarActivity
// Custom Toolbar
toolbar = requireNonNull(setUpCustomToolbar(true));
toolbarAvatar = toolbar.findViewById(R.id.contactAvatar);
toolbarStatus = toolbar.findViewById(R.id.contactStatus);
toolbarTitle = toolbar.findViewById(R.id.contactName);
toolbarStatus = toolbar.findViewById(R.id.contactStatus);
observeOnce(viewModel.getContactAuthorId(), this, authorId -> {
requireNonNull(authorId);
@@ -257,7 +257,6 @@ public class ConversationActivity extends BriarActivity
this::onAddedPrivateMessage);
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
visitor = new ConversationVisitor(this, this, this,
viewModel.getContactDisplayName());
@@ -499,14 +498,14 @@ public class ConversationActivity extends BriarActivity
@UiThread
private void displayContactOnlineStatus() {
if (connectionRegistry.isConnected(contactId)) {
toolbarStatus.setImageDrawable(ContextCompat.getDrawable(
ConversationActivity.this, R.drawable.contact_online));
toolbarStatus.setContentDescription(getString(R.string.online));
ConnectionStatus status =
connectionRegistry.getConnectionStatus(contactId);
if (status == CONNECTED) {
toolbarStatus.setText(R.string.online);
} else if (status == RECENTLY_CONNECTED) {
toolbarStatus.setText(R.string.recently_online);
} else {
toolbarStatus.setImageDrawable(ContextCompat.getDrawable(
ConversationActivity.this, R.drawable.contact_offline));
toolbarStatus.setContentDescription(getString(R.string.offline));
toolbarStatus.setText(R.string.offline);
}
}
@@ -729,16 +728,10 @@ public class ConversationActivity extends BriarActivity
LOG.info("Messages acked");
markMessages(m.getMessageIds(), true, true);
}
} else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e;
} else if (e instanceof ConnectionStatusChangedEvent) {
ConnectionStatusChangedEvent c = (ConnectionStatusChangedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact connected");
displayContactOnlineStatus();
}
} else if (e instanceof ContactDisconnectedEvent) {
ContactDisconnectedEvent c = (ContactDisconnectedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact disconnected");
LOG.info("Connection status changed");
displayContactOnlineStatus();
}
} else if (e instanceof ClientVersionUpdatedEvent) {

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
@@ -73,7 +74,8 @@ public class ContactChooserFragment extends BaseFragment {
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View contentView = inflater.inflate(R.layout.list, container, false);
@@ -127,9 +129,9 @@ public class ContactChooserFragment extends BaseFragment {
ContactId id = c.getId();
GroupCount count =
conversationManager.getGroupCount(id);
boolean connected =
connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, connected, count));
ConnectionStatus status = connectionRegistry
.getConnectionStatus(c.getId());
contacts.add(new ContactListItem(c, status, count));
}
}
displayContacts(contacts);

View File

@@ -15,27 +15,33 @@ import android.widget.Toast;
import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.util.UiUtils;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
import static org.briarproject.briar.android.login.LoginUtils.createKeyStrengthenerErrorDialog;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
public class ChangePasswordActivity extends BriarActivity
implements OnClickListener, OnEditorActionListener {
@Inject
protected ChangePasswordController passwordController;
ViewModelProvider.Factory viewModelFactory;
private TextInputLayout currentPasswordEntryWrapper;
private TextInputLayout newPasswordEntryWrapper;
@@ -47,11 +53,17 @@ public class ChangePasswordActivity extends BriarActivity
private Button changePasswordButton;
private ProgressBar progress;
@VisibleForTesting
ChangePasswordViewModel viewModel;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_change_password);
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ChangePasswordViewModel.class);
currentPasswordEntryWrapper =
findViewById(R.id.current_password_entry_wrapper);
newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper);
@@ -102,13 +114,12 @@ public class ChangePasswordActivity extends BriarActivity
String firstPassword = newPassword.getText().toString();
String secondPassword = newPasswordConfirmation.getText().toString();
boolean passwordsMatch = firstPassword.equals(secondPassword);
float strength =
passwordController.estimatePasswordStrength(firstPassword);
float strength = viewModel.estimatePasswordStrength(firstPassword);
strengthMeter.setStrength(strength);
UiUtils.setError(newPasswordEntryWrapper,
setError(newPasswordEntryWrapper,
getString(R.string.password_too_weak),
firstPassword.length() > 0 && strength < QUITE_WEAK);
UiUtils.setError(newPasswordConfirmationWrapper,
setError(newPasswordConfirmationWrapper,
getString(R.string.passwords_do_not_match),
secondPassword.length() > 0 && !passwordsMatch);
changePasswordButton.setEnabled(
@@ -127,32 +138,34 @@ public class ChangePasswordActivity extends BriarActivity
// Replace the button with a progress bar
changePasswordButton.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE);
passwordController.changePassword(currentPassword.getText().toString(),
newPassword.getText().toString(),
new UiResultHandler<Boolean>(this) {
@Override
public void onResultUi(@NonNull Boolean result) {
if (result) {
Toast.makeText(ChangePasswordActivity.this,
R.string.password_changed,
Toast.LENGTH_LONG).show();
setResult(RESULT_OK);
supportFinishAfterTransition();
} else {
tryAgain();
}
String curPwd = currentPassword.getText().toString();
String newPwd = newPassword.getText().toString();
viewModel.changePassword(curPwd, newPwd).observeEvent(this, result -> {
if (result == SUCCESS) {
Toast.makeText(ChangePasswordActivity.this,
R.string.password_changed,
LENGTH_LONG).show();
setResult(RESULT_OK);
supportFinishAfterTransition();
} else {
tryAgain(result);
}
});
}
);
}
private void tryAgain() {
UiUtils.setError(currentPasswordEntryWrapper,
getString(R.string.try_again), true);
private void tryAgain(DecryptionResult result) {
changePasswordButton.setVisibility(VISIBLE);
progress.setVisibility(INVISIBLE);
currentPassword.setText("");
// show the keyboard again
showSoftKeyboard(currentPassword);
if (result == KEY_STRENGTHENER_ERROR) {
createKeyStrengthenerErrorDialog(this).show();
} else {
setError(currentPasswordEntryWrapper,
getString(R.string.try_again), true);
currentPassword.setText("");
// show the keyboard again
showSoftKeyboard(currentPassword);
}
}
}

View File

@@ -1,43 +0,0 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultHandler;
import java.util.concurrent.Executor;
import javax.inject.Inject;
@NotNullByDefault
public class ChangePasswordControllerImpl implements ChangePasswordController {
protected final AccountManager accountManager;
protected final Executor ioExecutor;
private final PasswordStrengthEstimator strengthEstimator;
@Inject
ChangePasswordControllerImpl(AccountManager accountManager,
@IoExecutor Executor ioExecutor,
PasswordStrengthEstimator strengthEstimator) {
this.accountManager = accountManager;
this.ioExecutor = ioExecutor;
this.strengthEstimator = strengthEstimator;
}
@Override
public float estimatePasswordStrength(String password) {
return strengthEstimator.estimateStrength(password);
}
@Override
public void changePassword(String oldPassword, String newPassword,
ResultHandler<Boolean> resultHandler) {
ioExecutor.execute(() -> {
boolean changed =
accountManager.changePassword(oldPassword, newPassword);
resultHandler.onResult(changed);
});
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import androidx.lifecycle.ViewModel;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
@NotNullByDefault
public class ChangePasswordViewModel extends ViewModel {
private final AccountManager accountManager;
private final Executor ioExecutor;
private final PasswordStrengthEstimator strengthEstimator;
@Inject
ChangePasswordViewModel(AccountManager accountManager,
@IoExecutor Executor ioExecutor,
PasswordStrengthEstimator strengthEstimator) {
this.accountManager = accountManager;
this.ioExecutor = ioExecutor;
this.strengthEstimator = strengthEstimator;
}
float estimatePasswordStrength(String password) {
return strengthEstimator.estimateStrength(password);
}
LiveEvent<DecryptionResult> changePassword(String oldPassword,
String newPassword) {
MutableLiveEvent<DecryptionResult> result = new MutableLiveEvent<>();
ioExecutor.execute(() -> {
try {
accountManager.changePassword(oldPassword, newPassword);
result.postEvent(SUCCESS);
} catch (DecryptionException e) {
result.postEvent(e.getDecryptionResult());
}
});
return result;
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.android.login;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
public abstract class LoginModule {
@Binds
@IntoMap
@ViewModelKey(StartupViewModel.class)
abstract ViewModel bindStartupViewModel(StartupViewModel viewModel);
@Binds
@IntoMap
@ViewModelKey(ChangePasswordViewModel.class)
abstract ViewModel bindChangePasswordViewModel(
ChangePasswordViewModel viewModel);
}

View File

@@ -0,0 +1,30 @@
package org.briarproject.briar.android.login;
import android.content.Context;
import android.graphics.drawable.Drawable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import androidx.appcompat.app.AlertDialog;
import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.content.ContextCompat.getDrawable;
import static androidx.core.graphics.drawable.DrawableCompat.setTint;
import static java.util.Objects.requireNonNull;
@NotNullByDefault
class LoginUtils {
static AlertDialog createKeyStrengthenerErrorDialog(Context ctx) {
AlertDialog.Builder builder =
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error);
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
builder.setIcon(icon);
builder.setTitle(R.string.dialog_title_cannot_check_password);
builder.setMessage(R.string.dialog_message_cannot_check_password);
builder.setPositiveButton(R.string.ok, null);
return builder.create();
}
}

View File

@@ -12,6 +12,7 @@ import android.widget.ProgressBar;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
@@ -28,6 +29,9 @@ import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.briar.android.login.LoginUtils.createKeyStrengthenerErrorDialog;
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.setError;
@@ -58,12 +62,13 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_password, container,
false);
false);
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
.get(StartupViewModel.class);
viewModel.getPasswordValidated().observeEvent(this, valid -> {
if (!valid) onPasswordInvalid();
viewModel.getPasswordValidated().observeEvent(this, result -> {
if (result != SUCCESS) onPasswordInvalid(result);
});
signInButton = v.findViewById(R.id.btn_sign_in);
@@ -107,18 +112,20 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
viewModel.validatePassword(password.getText().toString());
}
private void onPasswordInvalid() {
setError(input, getString(R.string.try_again), true);
private void onPasswordInvalid(DecryptionResult result) {
signInButton.setVisibility(VISIBLE);
progress.setVisibility(INVISIBLE);
password.setText(null);
// show the keyboard again
showSoftKeyboard(password);
if (result == KEY_STRENGTHENER_ERROR) {
createKeyStrengthenerErrorDialog(requireContext()).show();
} else {
setError(input, getString(R.string.try_again), true);
password.setText(null);
// show the keyboard again
showSoftKeyboard(password);
}
}
public void onForgottenPasswordClick() {
// TODO Encapsulate the dialog in a re-usable fragment
private void onForgottenPasswordClick() {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
R.style.BriarDialogTheme);
builder.setTitle(R.string.dialog_title_lost_password);

View File

@@ -3,6 +3,8 @@ package org.briarproject.briar.android.login;
import android.app.Application;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.crypto.DecryptionException;
import org.briarproject.bramble.api.crypto.DecryptionResult;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
@@ -24,6 +26,7 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
@@ -46,7 +49,7 @@ public class StartupViewModel extends AndroidViewModel
@IoExecutor
private final Executor ioExecutor;
private final MutableLiveEvent<Boolean> passwordValidated =
private final MutableLiveEvent<DecryptionResult> passwordValidated =
new MutableLiveEvent<>();
private final MutableLiveEvent<Boolean> accountDeleted =
new MutableLiveEvent<>();
@@ -105,13 +108,17 @@ public class StartupViewModel extends AndroidViewModel
void validatePassword(String password) {
ioExecutor.execute(() -> {
boolean signedIn = accountManager.signIn(password);
passwordValidated.postEvent(signedIn);
if (signedIn) state.postValue(SIGNED_IN);
try {
accountManager.signIn(password);
passwordValidated.postEvent(SUCCESS);
state.postValue(SIGNED_IN);
} catch (DecryptionException e) {
passwordValidated.postEvent(e.getDecryptionResult());
}
});
}
LiveEvent<Boolean> getPasswordValidated() {
LiveEvent<DecryptionResult> getPasswordValidated() {
return passwordValidated;
}

View File

@@ -10,8 +10,6 @@ import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
@@ -21,7 +19,10 @@ interface GroupListController extends DbController {
* The listener must be set right after the controller was injected
*/
@UiThread
void setGroupListListener(@Nullable GroupListListener listener);
void setGroupListListener(GroupListListener listener);
@UiThread
void unsetGroupListListener(GroupListListener listener);
@UiThread
void onStart();

View File

@@ -80,10 +80,15 @@ class GroupListControllerImpl extends DbControllerImpl
}
@Override
public void setGroupListListener(@Nullable GroupListListener listener) {
public void setGroupListListener(GroupListListener listener) {
this.listener = listener;
}
@Override
public void unsetGroupListListener(GroupListListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
@CallSuper
public void onStart() {

View File

@@ -112,7 +112,7 @@ public class GroupListFragment extends BaseFragment implements
@Override
public void onDestroy() {
super.onDestroy();
controller.setGroupListListener(null);
controller.unsetGroupListListener(this);
}
@Override

View File

@@ -99,7 +99,7 @@ public class GroupMemberListActivity extends BriarActivity
supportFinishAfterTransition();
}
}
// TODO ContactConnectedEvent and ContactDisconnectedEvent
// TODO ConnectionStatusChangedEvent
}
@Override

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
@@ -19,6 +20,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.DISCONNECTED;
import static org.briarproject.bramble.util.LogUtils.logException;
class GroupMemberListControllerImpl extends DbControllerImpl
@@ -50,10 +52,11 @@ class GroupMemberListControllerImpl extends DbControllerImpl
privateGroupManager.getMembers(groupId);
for (GroupMember m : members) {
ContactId c = m.getContactId();
boolean online = false;
if (c != null)
online = connectionRegistry.isConnected(c);
items.add(new MemberListItem(m, online));
ConnectionStatus status = DISCONNECTED;
if (c != null) {
status = connectionRegistry.getConnectionStatus(c);
}
items.add(new MemberListItem(m, status));
}
handler.onResult(items);
} catch (DbException e) {

View File

@@ -45,7 +45,7 @@ class MemberListAdapter extends
@Override
public boolean areContentsTheSame(MemberListItem m1, MemberListItem m2) {
if (m1.isOnline() != m2.isOnline()) return false;
if (m1.getConnectionStatus() != m2.getConnectionStatus()) return false;
if (m1.getContactId() != m2.getContactId()) return false;
if (m1.getStatus() != m2.getStatus()) return false;
return true;

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorInfo;
import org.briarproject.bramble.api.identity.AuthorInfo.Status;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.briar.api.privategroup.GroupMember;
import javax.annotation.Nullable;
@@ -15,11 +16,11 @@ import javax.annotation.concurrent.NotThreadSafe;
class MemberListItem {
private final GroupMember groupMember;
private boolean online;
private ConnectionStatus status;
MemberListItem(GroupMember groupMember, boolean online) {
MemberListItem(GroupMember groupMember, ConnectionStatus status) {
this.groupMember = groupMember;
this.online = online;
this.status = status;
}
Author getMember() {
@@ -43,12 +44,12 @@ class MemberListItem {
return groupMember.getContactId();
}
boolean isOnline() {
return online;
ConnectionStatus getConnectionStatus() {
return status;
}
void setOnline(boolean online) {
this.online = online;
void setConnectionStatus(ConnectionStatus status) {
this.status = status;
}
}

View File

@@ -5,6 +5,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.briar.R;
import org.briarproject.briar.android.view.AuthorView;
@@ -14,6 +15,8 @@ import androidx.recyclerview.widget.RecyclerView;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.CONNECTED;
import static org.briarproject.bramble.api.plugin.ConnectionStatus.RECENTLY_CONNECTED;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
@UiThread
@@ -38,10 +41,13 @@ class MemberListItemHolder extends RecyclerView.ViewHolder {
// online status of visible contacts
if (item.getContactId() != null) {
bulb.setVisibility(VISIBLE);
if (item.isOnline()) {
bulb.setImageResource(R.drawable.contact_connected);
ConnectionStatus status = item.getConnectionStatus();
if (status == CONNECTED) {
bulb.setImageResource(R.drawable.ic_connected);
} else if (status == RECENTLY_CONNECTED) {
bulb.setImageResource(R.drawable.ic_recently_connected);
} else {
bulb.setImageResource(R.drawable.contact_disconnected);
bulb.setImageResource(R.drawable.ic_disconnected);
}
} else {
bulb.setVisibility(GONE);

View File

@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.ConnectionStatus;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.R;
@@ -104,7 +105,7 @@ abstract class SharingStatusActivity extends BriarActivity
supportFinishAfterTransition();
}
}
// TODO ContactConnectedEvent and ContactDisconnectedEvent
// TODO ConnectionStatusChangedEvent
}
@Override
@@ -134,8 +135,9 @@ abstract class SharingStatusActivity extends BriarActivity
try {
List<ContactItem> contactItems = new ArrayList<>();
for (Contact c : getSharedWith()) {
boolean online = connectionRegistry.isConnected(c.getId());
ContactItem item = new ContactItem(c, online);
ConnectionStatus status =
connectionRegistry.getConnectionStatus(c.getId());
ContactItem item = new ContactItem(c, status);
contactItems.add(item);
}
displaySharedWith(contactItems);

View File

@@ -238,10 +238,6 @@ public class UiUtils {
return "avatar" + c.getInt();
}
public static String getBulbTransitionName(ContactId c) {
return "bulb" + c.getInt();
}
public static OnClickListener getGoToSettingsListener(Context context) {
return (dialog, which) -> {
Intent i = new Intent();
@@ -381,7 +377,7 @@ public class UiUtils {
/**
* Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)},
* but without a {@link LifecycleOwner}.
*
* <p>
* Warning: Do NOT call from objects that have a lifecycle.
*/
@UiThread
@@ -401,5 +397,4 @@ public class UiUtils {
return ctx.getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL;
}
}

View File

@@ -4,7 +4,6 @@ import org.briarproject.briar.android.contact.add.remote.AddContactViewModel;
import org.briarproject.briar.android.contact.add.remote.PendingContactListViewModel;
import org.briarproject.briar.android.conversation.ConversationViewModel;
import org.briarproject.briar.android.conversation.ImageViewModel;
import org.briarproject.briar.android.login.StartupViewModel;
import javax.inject.Singleton;
@@ -17,11 +16,6 @@ import dagger.multibindings.IntoMap;
@Module
public abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(StartupViewModel.class)
abstract ViewModel bindStartupViewModel(StartupViewModel startupViewModel);
@Binds
@IntoMap
@ViewModelKey(ConversationViewModel.class)

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.widget;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -10,6 +11,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -22,6 +24,7 @@ import androidx.fragment.app.DialogFragment;
import static android.content.Intent.ACTION_VIEW;
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@@ -64,18 +67,23 @@ public class LinkDialogFragment extends DialogFragment {
urlView.setText(url);
// prepare normal intent or intent chooser
Context ctx = requireContext();
Intent i = new Intent(ACTION_VIEW, Uri.parse(url));
PackageManager packageManager =
requireNonNull(getContext()).getPackageManager();
List activities = packageManager.queryIntentActivities(i,
MATCH_DEFAULT_ONLY);
PackageManager packageManager = ctx.getPackageManager();
List activities =
packageManager.queryIntentActivities(i, MATCH_DEFAULT_ONLY);
boolean choice = activities.size() > 1;
Intent intent = choice ? Intent.createChooser(i,
getString(R.string.link_warning_open_link)) : i;
Button openButton = v.findViewById(R.id.openButton);
openButton.setOnClickListener(v1 -> {
startActivity(intent);
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
getDialog().dismiss();
});

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#abffffff"
android:pathData="M12,2 C6.48,2,2,6.48,2,12 S6.48,22,12,22 S22,17.52,22,12 S17.52,2,12,2 Z M12,20
C7.58,20,4,16.42,4,12 S7.58,4,12,4 S20,7.58,20,12 S16.42,20,12,20 Z"/>
<path
android:fillColor="#95d220"
android:pathData="M10.8972,19.9503 C6.5514,19.3493,3.43091,15.2154,4.0625,10.896
C4.55452,7.53099,7.09451,4.8236,10.394,4.14714
C14.2569,3.35517,18.1698,5.54347,19.5236,9.25295
C20.0698,10.7495,20.1616,12.4612,19.777,13.9758
C19.5457,14.8864,18.8106,16.3388,18.2072,17.0771
C16.4904,19.1779,13.581,20.3215,10.8973,19.9503 Z"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="0.76779664"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#abffffff"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,18c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeAlpha="1"
android:strokeWidth="1.5"
android:fillColor="#95d220"
android:strokeColor="#abffffff"
android:fillAlpha="1"/>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeAlpha="1"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#abffffff"
android:fillType="nonZero"
android:fillAlpha="1"/>
</vector>

View File

@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0"
android:strokeAlpha="1"
android:strokeWidth="4.5"
android:fillColor="#00000000"
android:strokeColor="#95d220"
android:fillAlpha="1"/>
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeAlpha="1"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#abffffff"
android:fillAlpha="1"/>
</vector>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.56"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2 C6.48,2,2,6.48,2,12 S6.48,22,12,22 S22,17.52,22,12 S17.52,2,12,2 Z M12,20
C7.58,20,4,16.42,4,12 S7.58,4,12,4 S20,7.58,20,12 S16.42,20,12,20 Z"/>
<path
android:pathData="M0,0 L24,0 L24,24 L0,24 Z"/>
<path
android:fillColor="#95d220"
android:pathData="M10.8972,19.9503 C6.5514,19.3493,3.43091,15.2154,4.0625,10.896
C4.55452,7.53099,7.09451,4.8236,10.394,4.14714
C14.2569,3.35517,18.1698,5.54347,19.5236,9.25295
C20.0698,10.7495,20.1616,12.4612,19.777,13.9758
C19.5457,14.8864,18.8106,16.3388,18.2072,17.0771
C16.4904,19.1779,13.581,20.3215,10.8973,19.9503 Z"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="0.76779664"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.56"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,18c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeAlpha="1"
android:strokeWidth="1.5"
android:fillColor="#95d220"
android:strokeColor="#000000"
android:fillAlpha="0.56"/>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeAlpha="1"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillType="nonZero"
android:fillAlpha="1"/>
</vector>

View File

@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0"
android:strokeAlpha="0.56"
android:strokeWidth="4.5"
android:fillColor="#ffffff"
android:strokeColor="#95d220"
android:fillAlpha="1"/>
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeAlpha="1"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillAlpha="1"/>
</vector>

View File

@@ -18,27 +18,50 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/contact_avatar_status" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/contactAvatar"
style="@style/BriarAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_margin="4dp"
app:civ_border_color="@color/action_bar_text"
tools:src="@mipmap/ic_launcher_round" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/contactName"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="@dimen/margin_medium"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toEndOf="@id/contactAvatar"
android:layout_toRightOf="@id/contactAvatar"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="@color/action_bar_text"
tools:text="Contact Name of someone who chose a long name" />
</LinearLayout>
<TextView
android:id="@+id/contactStatus"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/contactName"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_toEndOf="@id/contactAvatar"
android:layout_toRightOf="@id/contactAvatar"
android:textColor="@color/briar_text_secondary_inverse"
tools:text="Recently online" />
</RelativeLayout>
</androidx.appcompat.widget.Toolbar>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="32dp"
android:layout_height="32dp"
tools:showIn="@layout/activity_conversation">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/contactAvatar"
style="@style/BriarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
app:civ_border_color="@color/action_bar_text"
tools:src="@mipmap/ic_launcher_round" />
<ImageView
android:id="@+id/contactStatus"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_gravity="bottom|end|right"
android:scaleType="fitCenter"
tools:ignore="ContentDescription"
tools:src="@drawable/contact_online" />
</FrameLayout>

View File

@@ -90,7 +90,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/contact_connected" />
tools:src="@drawable/ic_recently_connected" />
<View
android:id="@+id/divider"

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