mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 02:39:05 +01:00
Compare commits
186 Commits
variable-l
...
control-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5751958eaf | ||
|
|
ea006d21f9 | ||
|
|
0237df937f | ||
|
|
4c1fd94c67 | ||
|
|
295e97c2c7 | ||
|
|
39f65fcdaf | ||
|
|
ce04f89f8b | ||
|
|
51f1cb5e9e | ||
|
|
419f37a4a9 | ||
|
|
3d94ffb714 | ||
|
|
bc8bb08853 | ||
|
|
cc67a8fcdd | ||
|
|
f8cf88e6cd | ||
|
|
bc58c47a22 | ||
|
|
aa6879c48e | ||
|
|
4d26628f2a | ||
|
|
abaa70da99 | ||
|
|
6435c3520c | ||
|
|
b5c4c7ae61 | ||
|
|
5d96da3547 | ||
|
|
ed842f781a | ||
|
|
5e30e5e1de | ||
|
|
ce52a36db1 | ||
|
|
f5ef87b34b | ||
|
|
4c6f68c255 | ||
|
|
ae09b4c607 | ||
|
|
880d77922e | ||
|
|
1c227e81e4 | ||
|
|
541acad29a | ||
|
|
60f71648f3 | ||
|
|
270b8af39f | ||
|
|
31d3324701 | ||
|
|
dbe46d60fd | ||
|
|
d10ab96955 | ||
|
|
b2841e245a | ||
|
|
9ccd8d1602 | ||
|
|
ac3942975e | ||
|
|
b6455d40a7 | ||
|
|
2815ad042d | ||
|
|
2055961534 | ||
|
|
741eae34e9 | ||
|
|
50bd4cce6b | ||
|
|
0a5a8310fc | ||
|
|
cc43d5982a | ||
|
|
50675473ce | ||
|
|
de852b2a9f | ||
|
|
b7c712116b | ||
|
|
7dd4897c8c | ||
|
|
7469c0f5e3 | ||
|
|
144ea0c2fc | ||
|
|
a917ebdc76 | ||
|
|
2a389c74dc | ||
|
|
ef16d096f1 | ||
|
|
679455888b | ||
|
|
d4372ddae7 | ||
|
|
c3ef990a94 | ||
|
|
8ae9b7f5a2 | ||
|
|
106d80ef76 | ||
|
|
9422ba2718 | ||
|
|
8343f5c2db | ||
|
|
371c7efb04 | ||
|
|
92d67645ab | ||
|
|
a20e868970 | ||
|
|
dd853f6718 | ||
|
|
16a8ad996a | ||
|
|
e27885f0c8 | ||
|
|
f6ef48bf90 | ||
|
|
e282ca763d | ||
|
|
71016382dc | ||
|
|
d004933fae | ||
|
|
37512c50d8 | ||
|
|
0b61a5d40a | ||
|
|
5dd320f282 | ||
|
|
2a21db5fb6 | ||
|
|
b023593a2c | ||
|
|
5ccf2cae1f | ||
|
|
c2cb89ab73 | ||
|
|
b342759e06 | ||
|
|
93d99b0111 | ||
|
|
61e8d576d2 | ||
|
|
75c37a258e | ||
|
|
e964dae64b | ||
|
|
986d884b40 | ||
|
|
9557afabc6 | ||
|
|
ebe6b0d4c0 | ||
|
|
6e83fb7aef | ||
|
|
7a5ec2af12 | ||
|
|
ce1fde496c | ||
|
|
4b62c51fbf | ||
|
|
226ed3dd73 | ||
|
|
ab07dfb32c | ||
|
|
20c51c1aa4 | ||
|
|
232c2129a7 | ||
|
|
3620edbfc9 | ||
|
|
ad71d69149 | ||
|
|
f73f8ca7e7 | ||
|
|
16c701a71a | ||
|
|
8183b7b26a | ||
|
|
bd48c97eab | ||
|
|
925dc29a1f | ||
|
|
91777fd942 | ||
|
|
fbce8f81c7 | ||
|
|
d7c72c4d68 | ||
|
|
4faf535801 | ||
|
|
526ef7c6d8 | ||
|
|
798dff1a03 | ||
|
|
a4336776c9 | ||
|
|
418451cbd9 | ||
|
|
045fcfc5fa | ||
|
|
ef998577db | ||
|
|
a53345a3c9 | ||
|
|
ed8c09282d | ||
|
|
42197b5b5c | ||
|
|
374fc7035b | ||
|
|
9b796c7cc3 | ||
|
|
532edff642 | ||
|
|
6857252471 | ||
|
|
c229e19452 | ||
|
|
42bca09d16 | ||
|
|
9eacbfa659 | ||
|
|
f14e546dc6 | ||
|
|
684c64a1d9 | ||
|
|
6fdab959b1 | ||
|
|
c8487483ff | ||
|
|
a159b23dc0 | ||
|
|
5070a27a83 | ||
|
|
9ce73a6840 | ||
|
|
6e9928f20f | ||
|
|
b31d61afc5 | ||
|
|
5a99cb93cc | ||
|
|
d0bbebd25e | ||
|
|
4307d26606 | ||
|
|
0089c1ac6d | ||
|
|
2a7aac4930 | ||
|
|
a37b6d81ed | ||
|
|
1d09a6708a | ||
|
|
d3b6f484c8 | ||
|
|
039c6edb66 | ||
|
|
8b9f89eab2 | ||
|
|
1e2c17b170 | ||
|
|
a994966095 | ||
|
|
2bea581654 | ||
|
|
87377666aa | ||
|
|
9d07b2e141 | ||
|
|
5c312b49e2 | ||
|
|
f56efe45cd | ||
|
|
2332a58681 | ||
|
|
8c6dfaa196 | ||
|
|
3cfb04b60d | ||
|
|
e85fbfb952 | ||
|
|
80ee35d926 | ||
|
|
4796902b9c | ||
|
|
149e67c0f7 | ||
|
|
1d5214117f | ||
|
|
b8f248ca9c | ||
|
|
dfb71a03a5 | ||
|
|
961fdc8e72 | ||
|
|
c3d44663cd | ||
|
|
0081472489 | ||
|
|
cdf4f3a24b | ||
|
|
fb1d8e860f | ||
|
|
a3c526ec9a | ||
|
|
dee488d06d | ||
|
|
b29c7d8022 | ||
|
|
0725d207ec | ||
|
|
5a7599a88d | ||
|
|
59cd98db81 | ||
|
|
768488eb04 | ||
|
|
a6b1ad48c3 | ||
|
|
77299a68ed | ||
|
|
5e5705c73b | ||
|
|
e6229a3a13 | ||
|
|
5fbacb4ee4 | ||
|
|
c7f4e976ed | ||
|
|
419f2d966a | ||
|
|
d6c18db9e9 | ||
|
|
8fe49d9961 | ||
|
|
f536cfdab8 | ||
|
|
4d594acad5 | ||
|
|
800dfed5c1 | ||
|
|
54b823e401 | ||
|
|
52ec56d690 | ||
|
|
7b3afcca99 | ||
|
|
a22d03d028 | ||
|
|
d857338ad0 | ||
|
|
dcd5e34c6b |
@@ -11,8 +11,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 26
|
||||
versionCode 10105
|
||||
versionName "1.1.5"
|
||||
versionCode 10106
|
||||
versionName "1.1.6"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
@@ -30,8 +30,8 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':bramble-core', configuration: 'default')
|
||||
tor 'org.briarproject:tor-android:0.3.4.8@zip'
|
||||
tor 'org.briarproject:obfs4proxy-android:0.0.7@zip'
|
||||
tor 'org.briarproject:tor-android:0.3.5.8@zip'
|
||||
tor 'org.briarproject:obfs4proxy-android:0.0.9@zip'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
|
||||
|
||||
|
||||
@@ -12,11 +12,15 @@ import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.util.IoUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
class AndroidAccountManager extends AccountManagerImpl
|
||||
implements AccountManager {
|
||||
|
||||
@@ -89,20 +93,42 @@ class AndroidAccountManager extends AccountManagerImpl
|
||||
LOG.warning("Could not clear shared preferences");
|
||||
}
|
||||
// Delete files, except lib and shared_prefs directories
|
||||
Set<File> files = new HashSet<>();
|
||||
File dataDir = new File(appContext.getApplicationInfo().dataDir);
|
||||
File[] children = dataDir.listFiles();
|
||||
if (children == null) {
|
||||
@Nullable
|
||||
File[] fileArray = dataDir.listFiles();
|
||||
if (fileArray == null) {
|
||||
LOG.warning("Could not list files in app data dir");
|
||||
} else {
|
||||
for (File child : children) {
|
||||
String name = child.getName();
|
||||
for (File file : fileArray) {
|
||||
String name = file.getName();
|
||||
if (!name.equals("lib") && !name.equals("shared_prefs")) {
|
||||
IoUtils.deleteFileOrDir(child);
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.add(appContext.getFilesDir());
|
||||
files.add(appContext.getCacheDir());
|
||||
addIfNotNull(files, appContext.getExternalCacheDir());
|
||||
if (SDK_INT >= 19) {
|
||||
for (File file : appContext.getExternalCacheDirs()) {
|
||||
addIfNotNull(files, file);
|
||||
}
|
||||
}
|
||||
if (SDK_INT >= 21) {
|
||||
for (File file : appContext.getExternalMediaDirs()) {
|
||||
addIfNotNull(files, file);
|
||||
}
|
||||
}
|
||||
for (File file : files) {
|
||||
IoUtils.deleteFileOrDir(file);
|
||||
}
|
||||
// Recreate the cache dir as some OpenGL drivers expect it to exist
|
||||
if (!new File(dataDir, "cache").mkdir())
|
||||
if (!new File(dataDir, "cache").mkdirs())
|
||||
LOG.warning("Could not recreate cache dir");
|
||||
}
|
||||
|
||||
private void addIfNotNull(Set<File> files, @Nullable File file) {
|
||||
if (file != null) files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@ import javax.inject.Inject;
|
||||
import static android.content.Intent.ACTION_BATTERY_CHANGED;
|
||||
import static android.content.Intent.ACTION_POWER_CONNECTED;
|
||||
import static android.content.Intent.ACTION_POWER_DISCONNECTED;
|
||||
import static android.os.BatteryManager.BATTERY_STATUS_CHARGING;
|
||||
import static android.os.BatteryManager.BATTERY_STATUS_FULL;
|
||||
import static android.os.BatteryManager.EXTRA_STATUS;
|
||||
import static android.os.BatteryManager.EXTRA_PLUGGED;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@@ -48,9 +46,8 @@ class AndroidBatteryManager implements BatteryManager, Service {
|
||||
IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
|
||||
Intent i = appContext.registerReceiver(null, filter);
|
||||
if (i == null) return false;
|
||||
int status = i.getIntExtra(EXTRA_STATUS, -1);
|
||||
return status == BATTERY_STATUS_CHARGING ||
|
||||
status == BATTERY_STATUS_FULL;
|
||||
int status = i.getIntExtra(EXTRA_PLUGGED, 0);
|
||||
return status != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -112,6 +112,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
|
||||
// Other directories should be deleted
|
||||
File potatoDir = new File(testDir, ".potato");
|
||||
File potatoFile = new File(potatoDir, "file");
|
||||
File filesDir = new File(testDir, "filesDir");
|
||||
File externalCacheDir = new File(testDir, "externalCacheDir");
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(prefs).edit();
|
||||
@@ -128,6 +130,12 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
|
||||
will(returnValue(true));
|
||||
oneOf(app).getApplicationInfo();
|
||||
will(returnValue(applicationInfo));
|
||||
oneOf(app).getFilesDir();
|
||||
will(returnValue(filesDir));
|
||||
oneOf(app).getCacheDir();
|
||||
will(returnValue(cacheDir));
|
||||
oneOf(app).getExternalCacheDir();
|
||||
will(returnValue(externalCacheDir));
|
||||
}});
|
||||
|
||||
assertTrue(dbDir.mkdirs());
|
||||
@@ -140,6 +148,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
|
||||
assertTrue(cacheFile.createNewFile());
|
||||
assertTrue(potatoDir.mkdirs());
|
||||
assertTrue(potatoFile.createNewFile());
|
||||
assertTrue(filesDir.mkdirs());
|
||||
assertTrue(externalCacheDir.mkdirs());
|
||||
|
||||
accountManager.deleteAccount();
|
||||
|
||||
@@ -153,6 +163,8 @@ public class AndroidAccountManagerTest extends BrambleMockTestCase {
|
||||
assertFalse(cacheFile.exists());
|
||||
assertFalse(potatoDir.exists());
|
||||
assertFalse(potatoFile.exists());
|
||||
assertFalse(filesDir.exists());
|
||||
assertFalse(externalCacheDir.exists());
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -1,46 +1,44 @@
|
||||
dependencyVerification {
|
||||
verify = [
|
||||
'cglib:cglib:3.2.0:cglib-3.2.0.jar:adb13bab79712ad6bdf1bd59f2a3918018a8016e722e8a357065afb9e6690861',
|
||||
'com.android.tools.analytics-library:protos:26.2.1:protos-26.2.1.jar:2f371f5b1f551e85ab08be4d6a2873471b3d44afd1ebf6aa3298f3b796bf691f',
|
||||
'com.android.tools.analytics-library:shared:26.2.1:shared-26.2.1.jar:4c1e4e705fa4d45f23aaea230557f6508155012d9c296337787c1d7b26a97f5a',
|
||||
'com.android.tools.analytics-library:tracker:26.2.1:tracker-26.2.1.jar:4a624ecc976539f755ddb0bb8dfc2dd3d08326cfec59a098dbd70f701ca7fb75',
|
||||
'com.android.tools.build:aapt2:3.2.1-4818971:aapt2-3.2.1-4818971-linux.jar:f431b6f96c91a2c155144b091a9c97d9805c589fe8efc9c930b6cd346cb60a1e',
|
||||
'com.android.tools.build:apksig:3.2.1:apksig-3.2.1.jar:2b46f2feffea66037aab29e4261b2433c190194a6ef97b958511eb157f2ccba5',
|
||||
'com.android.tools.build:apkzlib:3.2.1:apkzlib-3.2.1.jar:c39ad0313905932431fe81c8899c2cf39a4d92ad6c4edcaa4b25432f461452aa',
|
||||
'com.android.tools.build:builder-model:3.2.1:builder-model-3.2.1.jar:a9f68e6abcec122f9cb5ad352d3f05a3eb03acbcdca95e4d25c16310c2c965ff',
|
||||
'com.android.tools.build:builder-test-api:3.2.1:builder-test-api-3.2.1.jar:533ac6c2b5884bb54967a33791f2628dfdfac7981af39417a333b43d4379b6be',
|
||||
'com.android.tools.build:builder:3.2.1:builder-3.2.1.jar:aedcbfd115dbe91d09b4113e66ef50589b558d0aa3b2f133b1d867c9b87fae83',
|
||||
'com.android.tools.build:gradle-api:3.2.1:gradle-api-3.2.1.jar:57cf0ac5ac1dca8afdb3f62b94265e776e7dcfa641cc3844fb53a05193de208d',
|
||||
'com.android.tools.build:manifest-merger:26.2.1:manifest-merger-26.2.1.jar:8830573263361035d38cfdcb51e2db94029c93865b21334f5fbf8a27984281a6',
|
||||
'com.android.tools.ddms:ddmlib:26.2.1:ddmlib-26.2.1.jar:a4bf0a29a19980bf27269465cc782064656750b77c26728f82f9e148b705218b',
|
||||
'com.android.tools.external.com-intellij:intellij-core:26.2.1:intellij-core-26.2.1.jar:4925ad1892c2687cb1a63427d440ef519c8c59215fefe0dc5d541d5d411fcafe',
|
||||
'com.android.tools.external.com-intellij:kotlin-compiler:26.2.1:kotlin-compiler-26.2.1.jar:daa064fd708f340ee25fb9823c4c74104ac77f1370b76d907eb9ae6daec0a2ae',
|
||||
'com.android.tools.external.org-jetbrains:uast:26.2.1:uast-26.2.1.jar:f10f7258d2ab9189562cc0f9ad838c0378fdba439229173390a99de02ebac75b',
|
||||
'com.android.tools.layoutlib:layoutlib-api:26.2.1:layoutlib-api-26.2.1.jar:ddbf4fca123733fa011595b1cc1f4ac2937ed327b60990711fafc33c775c2ade',
|
||||
'com.android.tools.lint:lint-api:26.2.1:lint-api-26.2.1.jar:3b57e739de567b98bc9ab56c2c0ee66fc026b4adf5843e8f9804ca0666a6f66e',
|
||||
'com.android.tools.lint:lint-checks:26.2.1:lint-checks-26.2.1.jar:c86f4cc9aaee722ee4ad70062f7b5af91e9b041914af27adc09f545ab0fb3bc6',
|
||||
'com.android.tools.lint:lint-gradle-api:26.2.1:lint-gradle-api-26.2.1.jar:2283e7af32e301565f2a797e531f0fc8c648077d457afb3ffdddbee638976c2f',
|
||||
'com.android.tools.lint:lint-gradle:26.2.1:lint-gradle-26.2.1.jar:8fd90b2f3ec788cbb9801c07ab3e1ea2255aa31a6093157d7ea0ff13d0315ecb',
|
||||
'com.android.tools.lint:lint-kotlin:26.2.1:lint-kotlin-26.2.1.jar:7a6a5d2b18f69cf1b900d857c2632b4c683713c533295933b8b759f8cab4a877',
|
||||
'com.android.tools.lint:lint:26.2.1:lint-26.2.1.jar:7848b82ae988b90dee259ae7c7e86e05cbf52db6cd21c8bbd38ce7df08f3f8c5',
|
||||
'com.android.tools:annotations:26.2.1:annotations-26.2.1.jar:7391c6a1e080174b96e64ceb078dadd31ce4d8a2d2fee0ec65be202126f90f24',
|
||||
'com.android.tools:common:26.2.1:common-26.2.1.jar:a50aab2d6411ff68f4004a87c7e93d87d8e980a0ec3b352246549897ea2d78e5',
|
||||
'com.android.tools:dvlib:26.2.1:dvlib-26.2.1.jar:72a83bf2839b1df9b1fbf67ba45d1bfb9f966cd774da4320c762b2be8f1688aa',
|
||||
'com.android.tools:repository:26.2.1:repository-26.2.1.jar:fa74dae09103faef703df38550ad8fa244c5b6d1bf90d6198be932292b3d9cc1',
|
||||
'com.android.tools:sdk-common:26.2.1:sdk-common-26.2.1.jar:759d4b292ca69a35cf961fca377b54158fc6c88108978006999442e80a011cf4',
|
||||
'com.android.tools:sdklib:26.2.1:sdklib-26.2.1.jar:248df7ad5eac4aeb6f96c394c76760de4b7b89ac056e54d0c21a739368b91b45',
|
||||
'com.android.tools.analytics-library:protos:26.3.2:protos-26.3.2.jar:50238fb4298b297217b184b9cd93c14f83536fcee829eb0ca850bdb5b53251f0',
|
||||
'com.android.tools.analytics-library:shared:26.3.2:shared-26.3.2.jar:ddd80dcf21905018b7c0583ba72b7282f446084d4952422609a09fbf8237ef71',
|
||||
'com.android.tools.analytics-library:tracker:26.3.2:tracker-26.3.2.jar:28c575d2d1af003e96d7b375a668ee10b2673a2dd0f6438750aa8c3b42e7d0ad',
|
||||
'com.android.tools.build:apksig:3.3.2:apksig-3.3.2.jar:84c4aaa20127c6c1fe6bdd334b3f5df71f54ad080be9029c8a10f43b6a908acd',
|
||||
'com.android.tools.build:apkzlib:3.3.2:apkzlib-3.3.2.jar:d34e523278e5dff565eba3ef3c089d515b2b5cc7b47dc77e2f3465e5e47176ac',
|
||||
'com.android.tools.build:builder-model:3.3.2:builder-model-3.3.2.jar:055e3db0ecee9e06b9f024034999a29cd92cb1885207b37542126bd8bcc57f46',
|
||||
'com.android.tools.build:builder-test-api:3.3.2:builder-test-api-3.3.2.jar:0b2e4cd7615bbcad14a3c91fe45ae26693508d06e40ba06c5968b8bc24416618',
|
||||
'com.android.tools.build:builder:3.3.2:builder-3.3.2.jar:65649704da7aef0487235fd326f0f2e99ed5cf958e80f204496e6e08a42bd9f5',
|
||||
'com.android.tools.build:gradle-api:3.3.2:gradle-api-3.3.2.jar:3cbd47e41bb70330dd72ec2c9fe51e6173554b484a03829b5a2de9e00841e040',
|
||||
'com.android.tools.build:manifest-merger:26.3.2:manifest-merger-26.3.2.jar:05c4a6d8b02fb9f08744876477d0a68547c03a8a9069b1f086684fa04af97c33',
|
||||
'com.android.tools.ddms:ddmlib:26.3.2:ddmlib-26.3.2.jar:d248da8a563d6e46d2c7ebbf371a4877e00510f4ca763c0bb272d5a281bf8b85',
|
||||
'com.android.tools.external.com-intellij:intellij-core:26.3.2:intellij-core-26.3.2.jar:6c5ecc968230e9f4dcd0fef28885379feace1f0cd8130de6f61d649c86139bf3',
|
||||
'com.android.tools.external.com-intellij:kotlin-compiler:26.3.2:kotlin-compiler-26.3.2.jar:1007d9b07ccb49cd8eaf30fda10ed4681d4714f2f9ab2ecda39b4e539cc51bbe',
|
||||
'com.android.tools.external.org-jetbrains:uast:26.3.2:uast-26.3.2.jar:5d1833e562ea4f38a89708dfde695f0a162cbd39d003d3dde818c3fdc2b05317',
|
||||
'com.android.tools.layoutlib:layoutlib-api:26.3.2:layoutlib-api-26.3.2.jar:d7e61e874ab95f5c350dd38b6a95b5c9dbe0083a02001884264cdb390cb255b8',
|
||||
'com.android.tools.lint:lint-api:26.3.2:lint-api-26.3.2.jar:5867dfd7fb4a4e161a816a5d29d045f9b542d34594c00a1efec46fb4cd0e1033',
|
||||
'com.android.tools.lint:lint-checks:26.3.2:lint-checks-26.3.2.jar:4b163b9c93790d2771e92ba8de58a0d9e0671ffcf2ccef3cf496efd442e27517',
|
||||
'com.android.tools.lint:lint-gradle-api:26.3.2:lint-gradle-api-26.3.2.jar:54cb282e0c054f9bed3f51302ce08b003c8ab7961dfd5a4f6de26c23cc23062f',
|
||||
'com.android.tools.lint:lint-gradle:26.3.2:lint-gradle-26.3.2.jar:bb139615f4ce97d42cc394b9389b49b76a6eb85be6785a5d272991543b519013',
|
||||
'com.android.tools.lint:lint:26.3.2:lint-26.3.2.jar:ef7b369f8a56a92ccb0f4c1c357666b9339e4a711a9d84747d446441746cfe4e',
|
||||
'com.android.tools:annotations:26.3.2:annotations-26.3.2.jar:5bcce8e98b6a2f5ccf13ebcefd8f734e0b35f8b19e456575665631442ce1f7a1',
|
||||
'com.android.tools:common:26.3.2:common-26.3.2.jar:d9f8e7f0669e9a701568e3db6a87c89cf12d8fa6811c9991e969f950215ecfac',
|
||||
'com.android.tools:dvlib:26.3.2:dvlib-26.3.2.jar:d84aad56161c7773579303d69714ded6897c64c6ddfd7d456e453231e4dfe811',
|
||||
'com.android.tools:repository:26.3.2:repository-26.3.2.jar:da611eeb06e9ab8750d25b9e2901e10db8e5ec6304eb4c8b7103d39e0921ea40',
|
||||
'com.android.tools:sdk-common:26.3.2:sdk-common-26.3.2.jar:82823a3bf25e64fac33a286490f0cf5ac50c2cdb3c540149b030896bb44bf96c',
|
||||
'com.android.tools:sdklib:26.3.2:sdklib-26.3.2.jar:424d15492af67321900963238646d27495ab60de2a5b19e6a416963bc5d6932b',
|
||||
'com.google.code.findbugs:jsr305:1.3.9:jsr305-1.3.9.jar:905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed',
|
||||
'com.google.code.findbugs:jsr305:3.0.2:jsr305-3.0.2.jar:766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7',
|
||||
'com.google.code.gson:gson:2.8.0:gson-2.8.0.jar:c6221763bd79c4f1c3dc7f750b5f29a0bb38b367b81314c4f71896e340c40825',
|
||||
'com.google.dagger:dagger-compiler:2.19:dagger-compiler-2.19.jar:27a4b202a2de908182edb261f8c0a264e08e5e4733d7514bc7fbf0d31da5c0fc',
|
||||
'com.google.dagger:dagger-producers:2.19:dagger-producers-2.19.jar:a17663abe0fc38b676026950907d4c5f5e2bf338375415861eaff6e3bdb0b768',
|
||||
'com.google.dagger:dagger-spi:2.19:dagger-spi-2.19.jar:e7a6379d82c841f6aac2866948ad1eed716528707814602842a8d844ce04e2e1',
|
||||
'com.google.dagger:dagger:2.19:dagger-2.19.jar:514b6f1e0727c6572e1d65cb27e4ae668b7aeaeb93a29515182965265b609939',
|
||||
'com.google.errorprone:error_prone_annotations:2.0.18:error_prone_annotations-2.0.18.jar:cb4cfad870bf563a07199f3ebea5763f0dec440fcda0b318640b1feaa788656b',
|
||||
'com.google.errorprone:error_prone_annotations:2.1.3:error_prone_annotations-2.1.3.jar:03d0329547c13da9e17c634d1049ea2ead093925e290567e1a364fd6b1fc7ff8',
|
||||
'com.google.errorprone:javac-shaded:9-dev-r4023-3:javac-shaded-9-dev-r4023-3.jar:65bfccf60986c47fbc17c9ebab0be626afc41741e0a6ec7109e0768817a36f30',
|
||||
'com.google.googlejavaformat:google-java-format:1.5:google-java-format-1.5.jar:aa19ad7850fb85178aa22f2fddb163b84d6ce4d0035872f30d4408195ca1144e',
|
||||
'com.google.guava:guava:23.0:guava-23.0.jar:7baa80df284117e5b945b19b98d367a85ea7b7801bd358ff657946c3bd1b6596',
|
||||
'com.google.guava:guava:25.0-jre:guava-25.0-jre.jar:3fd4341776428c7e0e5c18a7c10de129475b69ab9d30aeafbb5c277bb6074fa9',
|
||||
'com.google.guava:guava:26.0-jre:guava-26.0-jre.jar:a0e9cabad665bc20bcd2b01f108e5fc03f756e13aea80abaadb9f407033bea2c',
|
||||
'com.google.j2objc:j2objc-annotations:1.1:j2objc-annotations-1.1.jar:40ceb7157feb263949e0f503fe5f71689333a621021aa20ce0d0acee3badaa0f',
|
||||
'com.google.jimfs:jimfs:1.1:jimfs-1.1.jar:c4828e28d7c0a930af9387510b3bada7daa5c04d7c25a75c7b8b081f1c257ddd',
|
||||
'com.google.protobuf:protobuf-java:3.4.0:protobuf-java-3.4.0.jar:dce7e66b32456a1b1198da0caff3a8acb71548658391e798c79369241e6490a4',
|
||||
@@ -68,21 +66,22 @@ dependencyVerification {
|
||||
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
|
||||
'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.7:obfs4proxy-android-0.0.7.zip:abdfb5d889d848de9bf214f9276abbf454808a505b870819eccc9a9e985bf617',
|
||||
'org.briarproject:tor-android:0.3.4.8:tor-android-0.3.4.8.zip:989a0352d9d8d8172cd6c2137654e165e5d2beb10ed1211bab3814e224ad1926',
|
||||
'org.briarproject:obfs4proxy-android:0.0.9:obfs4proxy-android-0.0.9.zip:9b7e9181535ea8d8bbe8ae6338e08cf4c5fc1e357a779393e0ce49586d459ae0',
|
||||
'org.briarproject:tor-android:0.3.5.8:tor-android-0.3.5.8.zip:42a13a6f185be1a62f42e3f30ce66a3c099ac5ec890a65e7593111b65b44a54a',
|
||||
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
|
||||
'org.codehaus.groovy:groovy-all:2.4.12:groovy-all-2.4.12.jar:6a56af4bd48903d56bec62821876cadefafd007360cc6bd0d8f7aa8d72b38be4',
|
||||
'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',
|
||||
'org.codehaus.mojo:animal-sniffer-annotations:1.14:animal-sniffer-annotations-1.14.jar:2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d',
|
||||
'org.glassfish.jaxb:jaxb-core:2.2.11:jaxb-core-2.2.11.jar:37bcaee8ebb04362c8352a5bf6221b86967ecdab5164c696b10b9a2bb587b2aa',
|
||||
'org.glassfish.jaxb:jaxb-runtime:2.2.11:jaxb-runtime-2.2.11.jar:a874f2351cfba8e2946be3002d10c18a6da8f21b52ba2acf52f2b85d5520ed70',
|
||||
'org.glassfish.jaxb:txw2:2.2.11:txw2-2.2.11.jar:272a3ccad45a4511351920cd2a8633c53cab8d5220c7a92954da5526bb5eafea',
|
||||
'org.hamcrest:hamcrest-core:1.3:hamcrest-core-1.3.jar:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9',
|
||||
'org.hamcrest:hamcrest-library:1.3:hamcrest-library-1.3.jar:711d64522f9ec410983bd310934296da134be4254a125080a0416ec178dfad1c',
|
||||
'org.jetbrains.kotlin:kotlin-reflect:1.2.0:kotlin-reflect-1.2.0.jar:4f48a872bad6e4d9c053f4ad610d11e4012ad7e58dc19a03dd5eb811f36069dd',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-common:1.2.71:kotlin-stdlib-common-1.2.71.jar:63999687ff2fce8a592dd180ffbbf8f1d21c26b4044c55cdc74ff3cf3b3cf328',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.71:kotlin-stdlib-jdk7-1.2.71.jar:b136bd61b240e07d4d92ce00d3bd1dbf584400a7bf5f220c2f3cd22446858082',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71:kotlin-stdlib-jdk8-1.2.71.jar:ac3c8abf47790b64b4f7e2509a53f0c145e061ac1612a597520535d199946ea9',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib:1.2.71:kotlin-stdlib-1.2.71.jar:4c895c270b87f5fec2a2796e1d89c15407ee821de961527c28588bb46afbc68b',
|
||||
'org.jetbrains.kotlin:kotlin-reflect:1.3.21:kotlin-reflect-1.3.21.jar:a3065c822633191e0a3e3ee12a29bec234fc4b2864a6bb87ef48cce3e9e0c26a',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-common:1.3.21:kotlin-stdlib-common-1.3.21.jar:cea61f7b611895e64f58569a9757fc0ab0d582f107211e1930e0ce2a0add52a7',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21:kotlin-stdlib-jdk7-1.3.21.jar:a87875604fd42140da6938ae4d35ee61081f4482536efc6d2615b8b626a198af',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.21:kotlin-stdlib-jdk8-1.3.21.jar:5823ed66ac122a1c55442ebca5a209a843ccd87f562edc31a787f3d2e47f74d4',
|
||||
'org.jetbrains.kotlin:kotlin-stdlib:1.3.21:kotlin-stdlib-1.3.21.jar:38ba2370d9f06f50433e06b2ca775b94473c2e2785f410926079ab793c72b034',
|
||||
'org.jetbrains.trove4j:trove4j:20160824:trove4j-20160824.jar:1917871c8deb468307a584680c87a44572f5a8b0b98c6d397fc0f5f86596dbe7',
|
||||
'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478',
|
||||
'org.jmock:jmock-junit4:2.8.2:jmock-junit4-2.8.2.jar:f7ee4df4f7bd7b7f1cafad3b99eb74d579f109d5992ff625347352edb55e674c',
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.briarproject.bramble.api.contact;
|
||||
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ContactExchangeListener {
|
||||
|
||||
void contactExchangeSucceeded(Author remoteAuthor);
|
||||
|
||||
/**
|
||||
* The exchange failed because the contact already exists.
|
||||
*/
|
||||
void duplicateContact(Author remoteAuthor);
|
||||
|
||||
/**
|
||||
* A general failure.
|
||||
*/
|
||||
void contactExchangeFailed();
|
||||
}
|
||||
@@ -41,8 +41,7 @@ public interface ContactExchangeTask {
|
||||
/**
|
||||
* Exchanges contact information with a remote peer.
|
||||
*/
|
||||
void startExchange(ContactExchangeListener listener,
|
||||
LocalAuthor localAuthor, SecretKey masterSecret,
|
||||
void startExchange(LocalAuthor localAuthor, SecretKey masterSecret,
|
||||
DuplexTransportConnection conn, TransportId transportId,
|
||||
boolean alice);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.briarproject.bramble.api.contact.PendingContact.PendingContactState.FAILED;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ContactManager {
|
||||
|
||||
@@ -52,6 +54,35 @@ public interface ContactManager {
|
||||
long timestamp, boolean alice, boolean verified, boolean active)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the static link that needs to be sent to the contact to be added.
|
||||
*/
|
||||
String getRemoteContactLink();
|
||||
|
||||
/**
|
||||
* Returns true if the given link is syntactically valid.
|
||||
*/
|
||||
boolean isValidRemoteContactLink(String link);
|
||||
|
||||
/**
|
||||
* Requests a new contact to be added via the given {@code link}.
|
||||
*
|
||||
* @param link The link received from the contact we want to add.
|
||||
* @param alias The alias the user has given this contact.
|
||||
* @return A PendingContact representing the contact to be added.
|
||||
*/
|
||||
PendingContact addRemoteContactRequest(String link, String alias);
|
||||
|
||||
/**
|
||||
* Returns a list of {@link PendingContact}s.
|
||||
*/
|
||||
Collection<PendingContact> getPendingContacts();
|
||||
|
||||
/**
|
||||
* Removes a {@link PendingContact} that is in state {@link FAILED}.
|
||||
*/
|
||||
void removePendingContact(PendingContact pendingContact);
|
||||
|
||||
/**
|
||||
* Returns the contact with the given ID.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.briarproject.bramble.api.contact;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class PendingContact {
|
||||
|
||||
public enum PendingContactState {
|
||||
WAITING_FOR_CONNECTION,
|
||||
CONNECTED,
|
||||
ADDING_CONTACT,
|
||||
FAILED
|
||||
}
|
||||
|
||||
private final PendingContactId id;
|
||||
private final String alias;
|
||||
private final PendingContactState state;
|
||||
private final long timestamp;
|
||||
|
||||
public PendingContact(PendingContactId id, String alias,
|
||||
PendingContactState state, long timestamp) {
|
||||
this.id = id;
|
||||
this.alias = alias;
|
||||
this.state = state;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public PendingContactState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof PendingContact &&
|
||||
id.equals(((PendingContact) o).id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.briarproject.bramble.api.contact;
|
||||
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
|
||||
public class PendingContactId extends UniqueId {
|
||||
|
||||
public PendingContactId(byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.api.contact.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ContactExchangeFailedEvent extends Event {
|
||||
|
||||
@Nullable
|
||||
private final Author duplicateRemoteAuthor;
|
||||
|
||||
public ContactExchangeFailedEvent(@Nullable Author duplicateRemoteAuthor) {
|
||||
this.duplicateRemoteAuthor = duplicateRemoteAuthor;
|
||||
}
|
||||
|
||||
public ContactExchangeFailedEvent() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Author getDuplicateRemoteAuthor() {
|
||||
return duplicateRemoteAuthor;
|
||||
}
|
||||
|
||||
public boolean wasDuplicateContact() {
|
||||
return duplicateRemoteAuthor != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.briarproject.bramble.api.contact.event;
|
||||
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ContactExchangeSucceededEvent extends Event {
|
||||
|
||||
private final Author remoteAuthor;
|
||||
|
||||
public ContactExchangeSucceededEvent(Author remoteAuthor) {
|
||||
this.remoteAuthor = remoteAuthor;
|
||||
}
|
||||
|
||||
public Author getRemoteAuthor() {
|
||||
return remoteAuthor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.briarproject.bramble.api.contact.event;
|
||||
|
||||
import org.briarproject.bramble.api.contact.PendingContact.PendingContactState;
|
||||
import org.briarproject.bramble.api.contact.PendingContactId;
|
||||
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 pending contact's state is changed.
|
||||
*/
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class PendingContactStateChangedEvent extends Event {
|
||||
|
||||
private final PendingContactId id;
|
||||
private final PendingContactState state;
|
||||
|
||||
public PendingContactStateChangedEvent(PendingContactId id,
|
||||
PendingContactState state) {
|
||||
this.id = id;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public PendingContactId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PendingContactState getPendingContactState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public interface TorConstants {
|
||||
String PREF_TOR_NETWORK = "network2";
|
||||
String PREF_TOR_PORT = "port";
|
||||
String PREF_TOR_MOBILE = "useMobileData";
|
||||
String PREF_TOR_ONLY_WHEN_CHARGING = "onlyWhenCharging";
|
||||
|
||||
int PREF_TOR_NETWORK_AUTOMATIC = 0;
|
||||
int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1;
|
||||
|
||||
@@ -38,6 +38,13 @@ public interface ClientVersioningManager {
|
||||
Visibility getClientVisibility(Transaction txn, ContactId contactId,
|
||||
ClientId clientId, int majorVersion) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the minor version of the given client that is supported by the
|
||||
* given contact, or -1 if the contact does not support the client.
|
||||
*/
|
||||
int getClientMinorVersion(Transaction txn, ContactId contactId,
|
||||
ClientId clientId, int majorVersion) throws DbException;
|
||||
|
||||
interface ClientVersioningHook {
|
||||
|
||||
void onClientVisibilityChanging(Transaction txn, Contact c,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.briarproject.bramble.util;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
|
||||
public class ByteUtils {
|
||||
|
||||
/**
|
||||
@@ -14,26 +12,15 @@ public class ByteUtils {
|
||||
*/
|
||||
public static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1
|
||||
|
||||
/**
|
||||
* The number of bytes needed to encode a 16-bit integer.
|
||||
*/
|
||||
/** The number of bytes needed to encode a 16-bit integer. */
|
||||
public static final int INT_16_BYTES = 2;
|
||||
|
||||
/**
|
||||
* The number of bytes needed to encode a 32-bit integer.
|
||||
*/
|
||||
/** The number of bytes needed to encode a 32-bit integer. */
|
||||
public static final int INT_32_BYTES = 4;
|
||||
|
||||
/**
|
||||
* The number of bytes needed to encode a 64-bit integer.
|
||||
*/
|
||||
/** The number of bytes needed to encode a 64-bit integer. */
|
||||
public static final int INT_64_BYTES = 8;
|
||||
|
||||
/**
|
||||
* The maximum number of bytes needed to encode a variable-length integer.
|
||||
*/
|
||||
public static final int MAX_VARINT_BYTES = 9;
|
||||
|
||||
public static void writeUint16(int src, byte[] dest, int offset) {
|
||||
if (src < 0) throw new IllegalArgumentException();
|
||||
if (src > MAX_16_BIT_UNSIGNED) throw new IllegalArgumentException();
|
||||
@@ -68,42 +55,6 @@ public class ByteUtils {
|
||||
dest[offset + 7] = (byte) (src & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes needed to represent 'src' as a
|
||||
* variable-length integer.
|
||||
* <p>
|
||||
* 'src' must not be negative.
|
||||
*/
|
||||
public static int getVarIntBytes(long src) {
|
||||
if (src < 0) throw new IllegalArgumentException();
|
||||
int len = 1;
|
||||
while ((src >>= 7) > 0) len++;
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes 'src' to 'dest' as a variable-length integer, starting at
|
||||
* 'offset', and returns the number of bytes written.
|
||||
* <p>
|
||||
* `src` must not be negative.
|
||||
*/
|
||||
public static int writeVarInt(long src, byte[] dest, int offset) {
|
||||
if (src < 0) throw new IllegalArgumentException();
|
||||
int len = getVarIntBytes(src);
|
||||
if (dest.length < offset + len) throw new IllegalArgumentException();
|
||||
// Work backwards from the end
|
||||
int end = offset + len - 1;
|
||||
for (int i = end; i >= offset; i--) {
|
||||
// Encode 7 bits
|
||||
dest[i] = (byte) (src & 0x7F);
|
||||
// Raise the continuation flag, except for the last byte
|
||||
if (i < end) dest[i] |= (byte) 0x80;
|
||||
// Shift out the bits that were encoded
|
||||
src >>= 7;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
public static int readUint16(byte[] src, int offset) {
|
||||
if (src.length < offset + INT_16_BYTES)
|
||||
throw new IllegalArgumentException();
|
||||
@@ -132,46 +83,14 @@ public class ByteUtils {
|
||||
| (src[offset + 7] & 0xFFL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length in bytes of a variable-length integer encoded in
|
||||
* 'src' starting at 'offset'.
|
||||
*
|
||||
* @throws FormatException if there is not a valid variable-length integer
|
||||
* at the specified position.
|
||||
*/
|
||||
public static int getVarIntBytes(byte[] src, int offset)
|
||||
throws FormatException {
|
||||
if (src.length < offset) throw new IllegalArgumentException();
|
||||
for (int i = 0; i < MAX_VARINT_BYTES && offset + i < src.length; i++) {
|
||||
// If the continuation flag is lowered, this is the last byte
|
||||
if ((src[offset + i] & 0x80) == 0) return i + 1;
|
||||
public static int readUint(byte[] src, int bits) {
|
||||
if (src.length << 3 < bits) throw new IllegalArgumentException();
|
||||
int dest = 0;
|
||||
for (int i = 0; i < bits; i++) {
|
||||
if ((src[i >> 3] & 128 >> (i & 7)) != 0) dest |= 1 << bits - i - 1;
|
||||
}
|
||||
// We've read 9 bytes or reached the end of the input without finding
|
||||
// the last byte
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a variable-length integer from 'src' starting at 'offset' and
|
||||
* returns it.
|
||||
*
|
||||
* @throws FormatException if there is not a valid variable-length integer
|
||||
* at the specified position.
|
||||
*/
|
||||
public static long readVarInt(byte[] src, int offset)
|
||||
throws FormatException {
|
||||
if (src.length < offset) throw new IllegalArgumentException();
|
||||
long dest = 0;
|
||||
for (int i = 0; i < MAX_VARINT_BYTES && offset + i < src.length; i++) {
|
||||
// Decode 7 bits
|
||||
dest |= src[offset + i] & 0x7F;
|
||||
// If the continuation flag is lowered, this is the last byte
|
||||
if ((src[offset + i] & 0x80) == 0) return dest;
|
||||
// Make room for the next 7 bits
|
||||
dest <<= 7;
|
||||
}
|
||||
// We've read 9 bytes or reached the end of the input without finding
|
||||
// the last byte
|
||||
throw new FormatException();
|
||||
if (dest < 0) throw new AssertionError();
|
||||
if (dest >= 1 << bits) throw new AssertionError();
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ dependencies {
|
||||
implementation 'org.bitlet:weupnp:0.1.4'
|
||||
implementation 'net.i2p.crypto:eddsa:0.2.0'
|
||||
implementation 'org.whispersystems:curve25519-java:0.5.0'
|
||||
implementation 'org.briarproject:jtorctl:0.3'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.class
|
||||
114
bramble-core/src/main/java/net/freehaven/tor/control/Bytes.java
Normal file
114
bramble-core/src/main/java/net/freehaven/tor/control/Bytes.java
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Static class to do bytewise structure manipulation in Java.
|
||||
*/
|
||||
/* XXXX There must be a better way to do most of this.
|
||||
* XXXX The string logic here uses default encoding, which is stupid.
|
||||
*/
|
||||
final class Bytes {
|
||||
|
||||
/** Write the two-byte value in 's' into the byte array 'ba', starting at
|
||||
* the index 'pos'. */
|
||||
public static void setU16(byte[] ba, int pos, short s) {
|
||||
ba[pos] = (byte)((s >> 8) & 0xff);
|
||||
ba[pos+1] = (byte)((s ) & 0xff);
|
||||
}
|
||||
|
||||
/** Write the four-byte value in 'i' into the byte array 'ba', starting at
|
||||
* the index 'pos'. */
|
||||
public static void setU32(byte[] ba, int pos, int i) {
|
||||
ba[pos] = (byte)((i >> 24) & 0xff);
|
||||
ba[pos+1] = (byte)((i >> 16) & 0xff);
|
||||
ba[pos+2] = (byte)((i >> 8) & 0xff);
|
||||
ba[pos+3] = (byte)((i ) & 0xff);
|
||||
}
|
||||
|
||||
/** Return the four-byte value starting at index 'pos' within 'ba' */
|
||||
public static int getU32(byte[] ba, int pos) {
|
||||
return
|
||||
((ba[pos ]&0xff)<<24) |
|
||||
((ba[pos+1]&0xff)<<16) |
|
||||
((ba[pos+2]&0xff)<< 8) |
|
||||
((ba[pos+3]&0xff));
|
||||
}
|
||||
|
||||
public static String getU32S(byte[] ba, int pos) {
|
||||
return String.valueOf( (getU32(ba,pos))&0xffffffffL );
|
||||
}
|
||||
|
||||
/** Return the two-byte value starting at index 'pos' within 'ba' */
|
||||
public static int getU16(byte[] ba, int pos) {
|
||||
return
|
||||
((ba[pos ]&0xff)<<8) |
|
||||
((ba[pos+1]&0xff));
|
||||
}
|
||||
|
||||
/** Return the string starting at position 'pos' of ba and extending
|
||||
* until a zero byte or the end of the string. */
|
||||
public static String getNulTerminatedStr(byte[] ba, int pos) {
|
||||
int len, maxlen = ba.length-pos;
|
||||
for (len=0; len<maxlen; ++len) {
|
||||
if (ba[pos+len] == 0)
|
||||
break;
|
||||
}
|
||||
return new String(ba, pos, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read bytes from 'ba' starting at 'pos', dividing them into strings
|
||||
* along the character in 'split' and writing them into 'lst'
|
||||
*/
|
||||
public static void splitStr(List<String> lst, byte[] ba, int pos, byte split) {
|
||||
while (pos < ba.length && ba[pos] != 0) {
|
||||
int len;
|
||||
for (len=0; pos+len < ba.length; ++len) {
|
||||
if (ba[pos+len] == 0 || ba[pos+len] == split)
|
||||
break;
|
||||
}
|
||||
if (len>0)
|
||||
lst.add(new String(ba, pos, len));
|
||||
pos += len;
|
||||
if (ba[pos] == split)
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read bytes from 'ba' starting at 'pos', dividing them into strings
|
||||
* along the character in 'split' and writing them into 'lst'
|
||||
*/
|
||||
public static List<String> splitStr(List<String> lst, String str) {
|
||||
// split string on spaces, include trailing/leading
|
||||
String[] tokenArray = str.split(" ", -1);
|
||||
if (lst == null) {
|
||||
lst = Arrays.asList( tokenArray );
|
||||
} else {
|
||||
lst.addAll( Arrays.asList( tokenArray ) );
|
||||
}
|
||||
return lst;
|
||||
}
|
||||
|
||||
private static final char[] NYBBLES = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
|
||||
};
|
||||
|
||||
public static final String hex(byte[] ba) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
for (int i = 0; i < ba.length; ++i) {
|
||||
int b = (ba[i]) & 0xff;
|
||||
buf.append(NYBBLES[b >> 4]);
|
||||
buf.append(NYBBLES[b&0x0f]);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private Bytes() {};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
/** A single key-value pair from Tor's configuration. */
|
||||
public class ConfigEntry {
|
||||
public ConfigEntry(String k, String v) {
|
||||
key = k;
|
||||
value = v;
|
||||
is_default = false;
|
||||
}
|
||||
public ConfigEntry(String k) {
|
||||
key = k;
|
||||
value = "";
|
||||
is_default = true;
|
||||
}
|
||||
public final String key;
|
||||
public final String value;
|
||||
public final boolean is_default;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
/**
|
||||
* Abstract interface whose methods are invoked when Tor sends us an event.
|
||||
*
|
||||
* @see TorControlConnection#setEventHandler
|
||||
* @see TorControlConnection#setEvents
|
||||
*/
|
||||
public interface EventHandler {
|
||||
/**
|
||||
* Invoked when a circuit's status has changed.
|
||||
* Possible values for <b>status</b> are:
|
||||
* <ul>
|
||||
* <li>"LAUNCHED" : circuit ID assigned to new circuit</li>
|
||||
* <li>"BUILT" : all hops finished, can now accept streams</li>
|
||||
* <li>"EXTENDED" : one more hop has been completed</li>
|
||||
* <li>"FAILED" : circuit closed (was not built)</li>
|
||||
* <li>"CLOSED" : circuit closed (was built)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <b>circID</b> is the alphanumeric identifier of the affected circuit,
|
||||
* and <b>path</b> is a comma-separated list of alphanumeric ServerIDs.
|
||||
*/
|
||||
public void circuitStatus(String status, String circID, String path);
|
||||
/**
|
||||
* Invoked when a stream's status has changed.
|
||||
* Possible values for <b>status</b> are:
|
||||
* <ul>
|
||||
* <li>"NEW" : New request to connect</li>
|
||||
* <li>"NEWRESOLVE" : New request to resolve an address</li>
|
||||
* <li>"SENTCONNECT" : Sent a connect cell along a circuit</li>
|
||||
* <li>"SENTRESOLVE" : Sent a resolve cell along a circuit</li>
|
||||
* <li>"SUCCEEDED" : Received a reply; stream established</li>
|
||||
* <li>"FAILED" : Stream failed and not retriable.</li>
|
||||
* <li>"CLOSED" : Stream closed</li>
|
||||
* <li>"DETACHED" : Detached from circuit; still retriable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <b>streamID</b> is the alphanumeric identifier of the affected stream,
|
||||
* and its <b>target</b> is specified as address:port.
|
||||
*/
|
||||
public void streamStatus(String status, String streamID, String target);
|
||||
/**
|
||||
* Invoked when the status of a connection to an OR has changed.
|
||||
* Possible values for <b>status</b> are ["LAUNCHED" | "CONNECTED" | "FAILED" | "CLOSED"].
|
||||
* <b>orName</b> is the alphanumeric identifier of the OR affected.
|
||||
*/
|
||||
public void orConnStatus(String status, String orName);
|
||||
/**
|
||||
* Invoked once per second. <b>read</b> and <b>written</b> are
|
||||
* the number of bytes read and written, respectively, in
|
||||
* the last second.
|
||||
*/
|
||||
public void bandwidthUsed(long read, long written);
|
||||
/**
|
||||
* Invoked whenever Tor learns about new ORs. The <b>orList</b> object
|
||||
* contains the alphanumeric ServerIDs associated with the new ORs.
|
||||
*/
|
||||
public void newDescriptors(java.util.List<String> orList);
|
||||
/**
|
||||
* Invoked when Tor logs a message.
|
||||
* <b>severity</b> is one of ["DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERR"],
|
||||
* and <b>msg</b> is the message string.
|
||||
*/
|
||||
public void message(String severity, String msg);
|
||||
/**
|
||||
* Invoked when an unspecified message is received.
|
||||
* <type> is the message type, and <msg> is the message string.
|
||||
*/
|
||||
public void unrecognized(String type, String msg);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
/**
|
||||
* Implementation of EventHandler that ignores all events. Useful
|
||||
* when you only want to override one method.
|
||||
*/
|
||||
public class NullEventHandler implements EventHandler {
|
||||
public void circuitStatus(String status, String circID, String path) {}
|
||||
public void streamStatus(String status, String streamID, String target) {}
|
||||
public void orConnStatus(String status, String orName) {}
|
||||
public void bandwidthUsed(long read, long written) {}
|
||||
public void newDescriptors(java.util.List<String> orList) {}
|
||||
public void message(String severity, String msg) {}
|
||||
public void unrecognized(String type, String msg) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* A hashed digest of a secret password (used to set control connection
|
||||
* security.)
|
||||
*
|
||||
* For the actual hashing algorithm, see RFC2440's secret-to-key conversion.
|
||||
*/
|
||||
public class PasswordDigest {
|
||||
|
||||
private final byte[] secret;
|
||||
private final String hashedKey;
|
||||
|
||||
/** Return a new password digest with a random secret and salt. */
|
||||
public static PasswordDigest generateDigest() {
|
||||
byte[] secret = new byte[20];
|
||||
SecureRandom rng = new SecureRandom();
|
||||
rng.nextBytes(secret);
|
||||
return new PasswordDigest(secret);
|
||||
}
|
||||
|
||||
/** Construct a new password digest with a given secret and random salt */
|
||||
public PasswordDigest(byte[] secret) {
|
||||
this(secret, null);
|
||||
}
|
||||
|
||||
/** Construct a new password digest with a given secret and random salt.
|
||||
* Note that the 9th byte of the specifier determines the number of hash
|
||||
* iterations as in RFC2440.
|
||||
*/
|
||||
public PasswordDigest(byte[] secret, byte[] specifier) {
|
||||
this.secret = secret.clone();
|
||||
if (specifier == null) {
|
||||
specifier = new byte[9];
|
||||
SecureRandom rng = new SecureRandom();
|
||||
rng.nextBytes(specifier);
|
||||
specifier[8] = 96;
|
||||
}
|
||||
hashedKey = "16:"+encodeBytes(secretToKey(secret, specifier));
|
||||
}
|
||||
|
||||
/** Return the secret used to generate this password hash.
|
||||
*/
|
||||
public byte[] getSecret() {
|
||||
return secret.clone();
|
||||
}
|
||||
|
||||
/** Return the hashed password in the format used by Tor. */
|
||||
public String getHashedPassword() {
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
/** Parameter used by RFC2440's s2k algorithm. */
|
||||
private static final int EXPBIAS = 6;
|
||||
|
||||
/** Implement rfc2440 s2k */
|
||||
public static byte[] secretToKey(byte[] secret, byte[] specifier) {
|
||||
MessageDigest d;
|
||||
try {
|
||||
d = MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new RuntimeException("Can't run without sha-1.");
|
||||
}
|
||||
int c = (specifier[8])&0xff;
|
||||
int count = (16 + (c&15)) << ((c>>4) + EXPBIAS);
|
||||
|
||||
byte[] tmp = new byte[8+secret.length];
|
||||
System.arraycopy(specifier, 0, tmp, 0, 8);
|
||||
System.arraycopy(secret, 0, tmp, 8, secret.length);
|
||||
while (count > 0) {
|
||||
if (count >= tmp.length) {
|
||||
d.update(tmp);
|
||||
count -= tmp.length;
|
||||
} else {
|
||||
d.update(tmp, 0, count);
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
byte[] key = new byte[20+9];
|
||||
System.arraycopy(d.digest(), 0, key, 9, 20);
|
||||
System.arraycopy(specifier, 0, key, 0, 9);
|
||||
return key;
|
||||
}
|
||||
|
||||
/** Return a hexadecimal encoding of a byte array. */
|
||||
// XXX There must be a better way to do this in Java.
|
||||
private static final String encodeBytes(byte[] ba) {
|
||||
return Bytes.hex(ba);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
We broke the version detection stuff in Tor 0.1.2.16 / 0.2.0.4-alpha.
|
||||
Somebody should rip out the v0 control protocol stuff from here, and
|
||||
it should start working again. -RD
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
/** Interface defining constants used by the Tor controller protocol.
|
||||
*/
|
||||
// XXXX Take documentation for these from control-spec.txt
|
||||
public interface TorControlCommands {
|
||||
|
||||
public static final short CMD_ERROR = 0x0000;
|
||||
public static final short CMD_DONE = 0x0001;
|
||||
public static final short CMD_SETCONF = 0x0002;
|
||||
public static final short CMD_GETCONF = 0x0003;
|
||||
public static final short CMD_CONFVALUE = 0x0004;
|
||||
public static final short CMD_SETEVENTS = 0x0005;
|
||||
public static final short CMD_EVENT = 0x0006;
|
||||
public static final short CMD_AUTH = 0x0007;
|
||||
public static final short CMD_SAVECONF = 0x0008;
|
||||
public static final short CMD_SIGNAL = 0x0009;
|
||||
public static final short CMD_MAPADDRESS = 0x000A;
|
||||
public static final short CMD_GETINFO = 0x000B;
|
||||
public static final short CMD_INFOVALUE = 0x000C;
|
||||
public static final short CMD_EXTENDCIRCUIT = 0x000D;
|
||||
public static final short CMD_ATTACHSTREAM = 0x000E;
|
||||
public static final short CMD_POSTDESCRIPTOR = 0x000F;
|
||||
public static final short CMD_FRAGMENTHEADER = 0x0010;
|
||||
public static final short CMD_FRAGMENT = 0x0011;
|
||||
public static final short CMD_REDIRECTSTREAM = 0x0012;
|
||||
public static final short CMD_CLOSESTREAM = 0x0013;
|
||||
public static final short CMD_CLOSECIRCUIT = 0x0014;
|
||||
|
||||
public static final String[] CMD_NAMES = {
|
||||
"ERROR",
|
||||
"DONE",
|
||||
"SETCONF",
|
||||
"GETCONF",
|
||||
"CONFVALUE",
|
||||
"SETEVENTS",
|
||||
"EVENT",
|
||||
"AUTH",
|
||||
"SAVECONF",
|
||||
"SIGNAL",
|
||||
"MAPADDRESS",
|
||||
"GETINFO",
|
||||
"INFOVALUE",
|
||||
"EXTENDCIRCUIT",
|
||||
"ATTACHSTREAM",
|
||||
"POSTDESCRIPTOR",
|
||||
"FRAGMENTHEADER",
|
||||
"FRAGMENT",
|
||||
"REDIRECTSTREAM",
|
||||
"CLOSESTREAM",
|
||||
"CLOSECIRCUIT",
|
||||
};
|
||||
|
||||
public static final short EVENT_CIRCSTATUS = 0x0001;
|
||||
public static final short EVENT_STREAMSTATUS = 0x0002;
|
||||
public static final short EVENT_ORCONNSTATUS = 0x0003;
|
||||
public static final short EVENT_BANDWIDTH = 0x0004;
|
||||
public static final short EVENT_NEWDESCRIPTOR = 0x0006;
|
||||
public static final short EVENT_MSG_DEBUG = 0x0007;
|
||||
public static final short EVENT_MSG_INFO = 0x0008;
|
||||
public static final short EVENT_MSG_NOTICE = 0x0009;
|
||||
public static final short EVENT_MSG_WARN = 0x000A;
|
||||
public static final short EVENT_MSG_ERROR = 0x000B;
|
||||
|
||||
public static final String[] EVENT_NAMES = {
|
||||
"(0)",
|
||||
"CIRC",
|
||||
"STREAM",
|
||||
"ORCONN",
|
||||
"BW",
|
||||
"OLDLOG",
|
||||
"NEWDESC",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"NOTICE",
|
||||
"WARN",
|
||||
"ERR",
|
||||
};
|
||||
|
||||
public static final byte CIRC_STATUS_LAUNCHED = 0x01;
|
||||
public static final byte CIRC_STATUS_BUILT = 0x02;
|
||||
public static final byte CIRC_STATUS_EXTENDED = 0x03;
|
||||
public static final byte CIRC_STATUS_FAILED = 0x04;
|
||||
public static final byte CIRC_STATUS_CLOSED = 0x05;
|
||||
|
||||
public static final String[] CIRC_STATUS_NAMES = {
|
||||
"LAUNCHED",
|
||||
"BUILT",
|
||||
"EXTENDED",
|
||||
"FAILED",
|
||||
"CLOSED",
|
||||
};
|
||||
|
||||
public static final byte STREAM_STATUS_SENT_CONNECT = 0x00;
|
||||
public static final byte STREAM_STATUS_SENT_RESOLVE = 0x01;
|
||||
public static final byte STREAM_STATUS_SUCCEEDED = 0x02;
|
||||
public static final byte STREAM_STATUS_FAILED = 0x03;
|
||||
public static final byte STREAM_STATUS_CLOSED = 0x04;
|
||||
public static final byte STREAM_STATUS_NEW_CONNECT = 0x05;
|
||||
public static final byte STREAM_STATUS_NEW_RESOLVE = 0x06;
|
||||
public static final byte STREAM_STATUS_DETACHED = 0x07;
|
||||
|
||||
public static final String[] STREAM_STATUS_NAMES = {
|
||||
"SENT_CONNECT",
|
||||
"SENT_RESOLVE",
|
||||
"SUCCEEDED",
|
||||
"FAILED",
|
||||
"CLOSED",
|
||||
"NEW_CONNECT",
|
||||
"NEW_RESOLVE",
|
||||
"DETACHED"
|
||||
};
|
||||
|
||||
public static final byte OR_CONN_STATUS_LAUNCHED = 0x00;
|
||||
public static final byte OR_CONN_STATUS_CONNECTED = 0x01;
|
||||
public static final byte OR_CONN_STATUS_FAILED = 0x02;
|
||||
public static final byte OR_CONN_STATUS_CLOSED = 0x03;
|
||||
|
||||
public static final String[] OR_CONN_STATUS_NAMES = {
|
||||
"LAUNCHED","CONNECTED","FAILED","CLOSED"
|
||||
};
|
||||
|
||||
public static final byte SIGNAL_HUP = 0x01;
|
||||
public static final byte SIGNAL_INT = 0x02;
|
||||
public static final byte SIGNAL_USR1 = 0x0A;
|
||||
public static final byte SIGNAL_USR2 = 0x0C;
|
||||
public static final byte SIGNAL_TERM = 0x0F;
|
||||
|
||||
public static final String ERROR_MSGS[] = {
|
||||
"Unspecified error",
|
||||
"Internal error",
|
||||
"Unrecognized message type",
|
||||
"Syntax error",
|
||||
"Unrecognized configuration key",
|
||||
"Invalid configuration value",
|
||||
"Unrecognized byte code",
|
||||
"Unauthorized",
|
||||
"Failed authentication attempt",
|
||||
"Resource exhausted",
|
||||
"No such stream",
|
||||
"No such circuit",
|
||||
"No such OR",
|
||||
};
|
||||
|
||||
public static final String HS_ADDRESS = "onionAddress";
|
||||
public static final String HS_PRIVKEY = "onionPrivKey";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,998 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
/**
|
||||
* A connection to a running Tor process as specified in control-spec.txt.
|
||||
*/
|
||||
public class TorControlConnection implements TorControlCommands {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(TorControlConnection.class.getName());
|
||||
|
||||
private final LinkedList<Waiter> waiters;
|
||||
private final BufferedReader input;
|
||||
private final Writer output;
|
||||
|
||||
private ControlParseThread thread; // Locking: this
|
||||
|
||||
private volatile EventHandler handler;
|
||||
private volatile PrintWriter debugOutput;
|
||||
private volatile IOException parseThreadException;
|
||||
|
||||
static class Waiter {
|
||||
|
||||
List<ReplyLine> response; // Locking: this
|
||||
boolean interrupted;
|
||||
|
||||
List<ReplyLine> getResponse() throws InterruptedException {
|
||||
LOG.info("Entering synchronized (waiter " + hashCode() + ")");
|
||||
synchronized (this) {
|
||||
LOG.info("Entered synchronized (waiter " + hashCode() + ")");
|
||||
while (response == null) {
|
||||
LOG.info("Waiter " + hashCode() + " waiting for response");
|
||||
wait();
|
||||
if (interrupted) {
|
||||
LOG.info("Waiter " + hashCode() + " interrupted");
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
LOG.info("Waiter " + hashCode() + " got response " + response);
|
||||
LOG.info("Leaving synchronized (waiter " + hashCode() + ")");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
void setResponse(List<ReplyLine> response) {
|
||||
LOG.info("Entering synchronized (waiter " + hashCode() + ")");
|
||||
synchronized (this) {
|
||||
LOG.info("Entered synchronized (waiter " + hashCode() + ")");
|
||||
LOG.info("Setting response for waiter " + hashCode() + ": "
|
||||
+ response);
|
||||
this.response = response;
|
||||
notifyAll();
|
||||
LOG.info("Leaving synchronized (waiter " + hashCode() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
void interrupt() {
|
||||
LOG.info("Entering synchronized (waiter " + hashCode() + ")");
|
||||
synchronized (this) {
|
||||
LOG.info("Entered synchronized (waiter " + hashCode() + ")");
|
||||
LOG.info("Interrupting waiter " + hashCode());
|
||||
interrupted = true;
|
||||
notifyAll();
|
||||
LOG.info("Leaving synchronized (waiter " + hashCode() + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ReplyLine {
|
||||
|
||||
final String status;
|
||||
final String msg;
|
||||
final String rest;
|
||||
|
||||
ReplyLine(String status, String msg, String rest) {
|
||||
this.status = status;
|
||||
this.msg = msg;
|
||||
this.rest = rest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return status + " " + msg + " " + rest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new TorControlConnection to communicate with Tor over
|
||||
* a given socket. After calling this constructor, it is typical to
|
||||
* call launchThread and authenticate.
|
||||
*/
|
||||
public TorControlConnection(Socket connection) throws IOException {
|
||||
this(connection.getInputStream(), connection.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new TorControlConnection to communicate with Tor over
|
||||
* an arbitrary pair of data streams.
|
||||
*/
|
||||
public TorControlConnection(InputStream i, OutputStream o) {
|
||||
this(new InputStreamReader(i), new OutputStreamWriter(o));
|
||||
}
|
||||
|
||||
public TorControlConnection(Reader i, Writer o) {
|
||||
this.output = o;
|
||||
if (i instanceof BufferedReader)
|
||||
this.input = (BufferedReader) i;
|
||||
else
|
||||
this.input = new BufferedReader(i);
|
||||
this.waiters = new LinkedList<>();
|
||||
}
|
||||
|
||||
protected final void writeEscaped(String s) throws IOException {
|
||||
StringTokenizer st = new StringTokenizer(s, "\n");
|
||||
while (st.hasMoreTokens()) {
|
||||
String line = st.nextToken();
|
||||
if (line.startsWith("."))
|
||||
line = "." + line;
|
||||
if (line.endsWith("\r"))
|
||||
line += "\n";
|
||||
else
|
||||
line += "\r\n";
|
||||
if (debugOutput != null)
|
||||
debugOutput.print(">> " + line);
|
||||
output.write(line);
|
||||
}
|
||||
output.write(".\r\n");
|
||||
if (debugOutput != null)
|
||||
debugOutput.print(">> .\n");
|
||||
}
|
||||
|
||||
protected static String quote(String s) {
|
||||
StringBuffer sb = new StringBuffer("\"");
|
||||
for (int i = 0; i < s.length(); ++i) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '\r':
|
||||
case '\n':
|
||||
case '\\':
|
||||
case '\"':
|
||||
sb.append('\\');
|
||||
}
|
||||
sb.append(c);
|
||||
}
|
||||
sb.append('\"');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected final ArrayList<ReplyLine> readReply() throws IOException {
|
||||
ArrayList<ReplyLine> reply = new ArrayList<>();
|
||||
char c;
|
||||
do {
|
||||
String line = input.readLine();
|
||||
if (line == null) {
|
||||
// if line is null, the end of the stream has been reached, i.e.
|
||||
// the connection to Tor has been closed!
|
||||
if (reply.isEmpty()) {
|
||||
// nothing received so far, can exit cleanly
|
||||
return reply;
|
||||
}
|
||||
// received half of a reply before the connection broke down
|
||||
throw new TorControlSyntaxError("Connection to Tor " +
|
||||
" broke down while receiving reply!");
|
||||
}
|
||||
if (debugOutput != null)
|
||||
debugOutput.println("<< " + line);
|
||||
if (line.length() < 4)
|
||||
throw new TorControlSyntaxError(
|
||||
"Line (\"" + line + "\") too short");
|
||||
String status = line.substring(0, 3);
|
||||
c = line.charAt(3);
|
||||
String msg = line.substring(4);
|
||||
String rest = null;
|
||||
if (c == '+') {
|
||||
StringBuffer data = new StringBuffer();
|
||||
while (true) {
|
||||
line = input.readLine();
|
||||
if (debugOutput != null)
|
||||
debugOutput.print("<< " + line);
|
||||
if (line.equals("."))
|
||||
break;
|
||||
else if (line.startsWith("."))
|
||||
line = line.substring(1);
|
||||
data.append(line).append('\n');
|
||||
}
|
||||
rest = data.toString();
|
||||
}
|
||||
reply.add(new ReplyLine(status, msg, rest));
|
||||
} while (c != ' ');
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
protected List<ReplyLine> sendAndWaitForResponse(String s,
|
||||
String rest) throws IOException {
|
||||
LOG.info("Entering synchronized (connection)");
|
||||
synchronized (this) {
|
||||
LOG.info("Entered synchronized (connection)");
|
||||
LOG.info("Sending '" + s + "', '" + rest +
|
||||
"' and waiting for response");
|
||||
if (parseThreadException != null) {
|
||||
LOG.info("Throwing previously caught exception "
|
||||
+ parseThreadException);
|
||||
throw parseThreadException;
|
||||
}
|
||||
checkThread();
|
||||
Waiter w = new Waiter();
|
||||
LOG.info("Created waiter " + w.hashCode());
|
||||
if (debugOutput != null)
|
||||
debugOutput.print(">> " + s);
|
||||
LOG.info("Entering synchronized (waiters)");
|
||||
synchronized (waiters) {
|
||||
LOG.info("Entered synchronized (waiters)");
|
||||
output.write(s);
|
||||
LOG.info("Wrote '" + s + "'");
|
||||
if (rest != null) {
|
||||
writeEscaped(rest);
|
||||
LOG.info("Wrote escaped '" + rest + "'");
|
||||
}
|
||||
output.flush();
|
||||
LOG.info("Flushed output");
|
||||
waiters.addLast(w);
|
||||
LOG.info("Added waiter, " + waiters.size() + " waiting");
|
||||
LOG.info("Leaving synchronized (waiters)");
|
||||
}
|
||||
List<ReplyLine> lst;
|
||||
try {
|
||||
LOG.info("Getting response from waiter " + w.hashCode());
|
||||
lst = w.getResponse();
|
||||
LOG.info("Got response from waiter " + w.hashCode() + ": " +
|
||||
lst);
|
||||
} catch (InterruptedException ex) {
|
||||
throw new IOException("Interrupted");
|
||||
}
|
||||
for (Iterator<ReplyLine> i = lst.iterator(); i.hasNext(); ) {
|
||||
ReplyLine c = i.next();
|
||||
if (!c.status.startsWith("2"))
|
||||
throw new TorControlError("Error reply: " + c.msg);
|
||||
}
|
||||
LOG.info("Leaving synchronized (connection)");
|
||||
return lst;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: decode a CMD_EVENT command and dispatch it to our
|
||||
* EventHandler (if any).
|
||||
*/
|
||||
protected void handleEvent(ArrayList<ReplyLine> events) {
|
||||
if (handler == null)
|
||||
return;
|
||||
|
||||
for (Iterator<ReplyLine> i = events.iterator(); i.hasNext(); ) {
|
||||
ReplyLine line = i.next();
|
||||
int idx = line.msg.indexOf(' ');
|
||||
String tp = line.msg.substring(0, idx).toUpperCase();
|
||||
String rest = line.msg.substring(idx + 1);
|
||||
if (tp.equals("CIRC")) {
|
||||
List<String> lst = Bytes.splitStr(null, rest);
|
||||
handler.circuitStatus(lst.get(1),
|
||||
lst.get(0),
|
||||
lst.get(1).equals("LAUNCHED")
|
||||
|| lst.size() < 3 ? ""
|
||||
: lst.get(2));
|
||||
} else if (tp.equals("STREAM")) {
|
||||
List<String> lst = Bytes.splitStr(null, rest);
|
||||
handler.streamStatus(lst.get(1),
|
||||
lst.get(0),
|
||||
lst.get(3));
|
||||
// XXXX circID.
|
||||
} else if (tp.equals("ORCONN")) {
|
||||
List<String> lst = Bytes.splitStr(null, rest);
|
||||
handler.orConnStatus(lst.get(1), lst.get(0));
|
||||
} else if (tp.equals("BW")) {
|
||||
List<String> lst = Bytes.splitStr(null, rest);
|
||||
handler.bandwidthUsed(Integer.parseInt(lst.get(0)),
|
||||
Integer.parseInt(lst.get(1)));
|
||||
} else if (tp.equals("NEWDESC")) {
|
||||
List<String> lst = Bytes.splitStr(null, rest);
|
||||
handler.newDescriptors(lst);
|
||||
} else if (tp.equals("DEBUG") ||
|
||||
tp.equals("INFO") ||
|
||||
tp.equals("NOTICE") ||
|
||||
tp.equals("WARN") ||
|
||||
tp.equals("ERR")) {
|
||||
handler.message(tp, rest);
|
||||
} else {
|
||||
handler.unrecognized(tp, rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets <b>w</b> as the PrintWriter for debugging output,
|
||||
* which writes out all messages passed between Tor and the controller.
|
||||
* Outgoing messages are preceded by "\>\>" and incoming messages are preceded
|
||||
* by "\<\<"
|
||||
*/
|
||||
public void setDebugging(PrintWriter w) {
|
||||
debugOutput = w;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets <b>s</b> as the PrintStream for debugging output,
|
||||
* which writes out all messages passed between Tor and the controller.
|
||||
* Outgoing messages are preceded by "\>\>" and incoming messages are preceded
|
||||
* by "\<\<"
|
||||
*/
|
||||
public void setDebugging(PrintStream s) {
|
||||
debugOutput = new PrintWriter(s, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the EventHandler object that will be notified of any
|
||||
* events Tor delivers to this connection. To make Tor send us
|
||||
* events, call setEvents().
|
||||
*/
|
||||
public void setEventHandler(EventHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a thread to react to Tor's responses in the background.
|
||||
* This is necessary to handle asynchronous events and synchronous
|
||||
* responses that arrive independantly over the same socket.
|
||||
*/
|
||||
public Thread launchThread(boolean daemon) {
|
||||
LOG.info("Entering synchronized (connection)");
|
||||
synchronized (this) {
|
||||
LOG.info("Entered synchronized (connection)");
|
||||
ControlParseThread th = new ControlParseThread();
|
||||
LOG.info("Launching parse thread " + th.hashCode());
|
||||
if (daemon)
|
||||
th.setDaemon(true);
|
||||
th.start();
|
||||
this.thread = th;
|
||||
LOG.info("Leaving synchronized (connection)");
|
||||
return th;
|
||||
}
|
||||
}
|
||||
|
||||
protected class ControlParseThread extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
react();
|
||||
} catch (IOException ex) {
|
||||
LOG.info("Parse thread " + hashCode()
|
||||
+ " caught exception " + ex);
|
||||
parseThreadException = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkThread() {
|
||||
LOG.info("Entering synchronized (connection)");
|
||||
synchronized (this) {
|
||||
LOG.info("Entered synchronized (connection)");
|
||||
if (thread == null)
|
||||
launchThread(true);
|
||||
LOG.info("Leaving synchronized (connection)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* helper: implement the main background loop.
|
||||
*/
|
||||
protected void react() throws IOException {
|
||||
while (true) {
|
||||
ArrayList<ReplyLine> lst = readReply();
|
||||
LOG.info("Read reply: " + lst);
|
||||
if (lst.isEmpty()) {
|
||||
// interrupted queued waiters, there won't be any response.
|
||||
LOG.info("Entering synchronized (waiters)");
|
||||
synchronized (waiters) {
|
||||
LOG.info("Entered synchronized (waiters)");
|
||||
if (!waiters.isEmpty()) {
|
||||
for (Waiter w : waiters) {
|
||||
LOG.info("Interrupting waiter " + w.hashCode());
|
||||
w.interrupt();
|
||||
}
|
||||
} else {
|
||||
LOG.info("No waiters");
|
||||
}
|
||||
LOG.info("Leaving synchronized (waiters)");
|
||||
}
|
||||
throw new IOException("Tor is no longer running");
|
||||
}
|
||||
if ((lst.get(0)).status.startsWith("6")) {
|
||||
LOG.info("Reply is an event");
|
||||
handleEvent(lst);
|
||||
} else {
|
||||
LOG.info("Entering synchronized (waiters)");
|
||||
synchronized (waiters) {
|
||||
LOG.info("Entered synchronized (waiters)");
|
||||
if (!waiters.isEmpty()) {
|
||||
Waiter w;
|
||||
w = waiters.removeFirst();
|
||||
LOG.info("Setting response for waiter " + w.hashCode());
|
||||
w.setResponse(lst);
|
||||
} else {
|
||||
LOG.info("No waiters");
|
||||
}
|
||||
LOG.info("Leaving synchronized (waiters)");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the value of the configuration option 'key' to 'val'.
|
||||
*/
|
||||
public void setConf(String key, String value) throws IOException {
|
||||
List<String> lst = new ArrayList<>();
|
||||
lst.add(key + " " + value);
|
||||
setConf(lst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the values of the configuration options stored in kvMap.
|
||||
*/
|
||||
public void setConf(Map<String, String> kvMap) throws IOException {
|
||||
List<String> lst = new ArrayList<>();
|
||||
for (Iterator<Map.Entry<String, String>> it =
|
||||
kvMap.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, String> ent = it.next();
|
||||
lst.add(ent.getKey() + " " + ent.getValue() + "\n");
|
||||
}
|
||||
setConf(lst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the values of the configuration options stored in
|
||||
* <b>kvList</b>. Each list element in <b>kvList</b> is expected to be
|
||||
* String of the format "key value".
|
||||
* <p>
|
||||
* Tor behaves as though it had just read each of the key-value pairs
|
||||
* from its configuration file. Keywords with no corresponding values have
|
||||
* their configuration values reset to their defaults. setConf is
|
||||
* all-or-nothing: if there is an error in any of the configuration settings,
|
||||
* Tor sets none of them.
|
||||
* <p>
|
||||
* When a configuration option takes multiple values, or when multiple
|
||||
* configuration keys form a context-sensitive group (see getConf below), then
|
||||
* setting any of the options in a setConf command is taken to reset all of
|
||||
* the others. For example, if two ORBindAddress values are configured, and a
|
||||
* command arrives containing a single ORBindAddress value, the new
|
||||
* command's value replaces the two old values.
|
||||
* <p>
|
||||
* To remove all settings for a given option entirely (and go back to its
|
||||
* default value), include a String in <b>kvList</b> containing the key and no value.
|
||||
*/
|
||||
public void setConf(Collection<String> kvList) throws IOException {
|
||||
if (kvList.size() == 0)
|
||||
return;
|
||||
StringBuffer b = new StringBuffer("SETCONF");
|
||||
for (Iterator<String> it = kvList.iterator(); it.hasNext(); ) {
|
||||
String kv = it.next();
|
||||
int i = kv.indexOf(' ');
|
||||
if (i == -1)
|
||||
b.append(" ").append(kv);
|
||||
b.append(" ").append(kv.substring(0, i)).append("=")
|
||||
.append(quote(kv.substring(i + 1)));
|
||||
}
|
||||
b.append("\r\n");
|
||||
sendAndWaitForResponse(b.toString(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to reset the values listed in the collection 'keys' to their
|
||||
* default values.
|
||||
**/
|
||||
public void resetConf(Collection<String> keys) throws IOException {
|
||||
if (keys.size() == 0)
|
||||
return;
|
||||
StringBuffer b = new StringBuffer("RESETCONF");
|
||||
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
|
||||
String key = it.next();
|
||||
b.append(" ").append(key);
|
||||
}
|
||||
b.append("\r\n");
|
||||
sendAndWaitForResponse(b.toString(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of the configuration option 'key'
|
||||
*/
|
||||
public List<ConfigEntry> getConf(String key) throws IOException {
|
||||
List<String> lst = new ArrayList<>();
|
||||
lst.add(key);
|
||||
return getConf(lst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the values of the configuration variables listed in <b>keys</b>.
|
||||
* Results are returned as a list of ConfigEntry objects.
|
||||
* <p>
|
||||
* If an option appears multiple times in the configuration, all of its
|
||||
* key-value pairs are returned in order.
|
||||
* <p>
|
||||
* Some options are context-sensitive, and depend on other options with
|
||||
* different keywords. These cannot be fetched directly. Currently there
|
||||
* is only one such option: clients should use the "HiddenServiceOptions"
|
||||
* virtual keyword to get all HiddenServiceDir, HiddenServicePort,
|
||||
* HiddenServiceNodes, and HiddenServiceExcludeNodes option settings.
|
||||
*/
|
||||
public List<ConfigEntry> getConf(Collection<String> keys)
|
||||
throws IOException {
|
||||
StringBuffer sb = new StringBuffer("GETCONF");
|
||||
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
|
||||
String key = it.next();
|
||||
sb.append(" ").append(key);
|
||||
}
|
||||
sb.append("\r\n");
|
||||
List<ReplyLine> lst = sendAndWaitForResponse(sb.toString(), null);
|
||||
List<ConfigEntry> result = new ArrayList<>();
|
||||
for (Iterator<ReplyLine> it = lst.iterator(); it.hasNext(); ) {
|
||||
String kv = (it.next()).msg;
|
||||
int idx = kv.indexOf('=');
|
||||
if (idx >= 0)
|
||||
result.add(new ConfigEntry(kv.substring(0, idx),
|
||||
kv.substring(idx + 1)));
|
||||
else
|
||||
result.add(new ConfigEntry(kv));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that the server inform the client about interesting events.
|
||||
* Each element of <b>events</b> is one of the following Strings:
|
||||
* ["CIRC" | "STREAM" | "ORCONN" | "BW" | "DEBUG" |
|
||||
* "INFO" | "NOTICE" | "WARN" | "ERR" | "NEWDESC" | "ADDRMAP"] .
|
||||
* <p>
|
||||
* Any events not listed in the <b>events</b> are turned off; thus, calling
|
||||
* setEvents with an empty <b>events</b> argument turns off all event reporting.
|
||||
*/
|
||||
public void setEvents(List<String> events) throws IOException {
|
||||
StringBuffer sb = new StringBuffer("SETEVENTS");
|
||||
for (Iterator<String> it = events.iterator(); it.hasNext(); ) {
|
||||
sb.append(" ").append(it.next());
|
||||
}
|
||||
sb.append("\r\n");
|
||||
sendAndWaitForResponse(sb.toString(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the controller to the Tor server.
|
||||
* <p>
|
||||
* By default, the current Tor implementation trusts all local users, and
|
||||
* the controller can authenticate itself by calling authenticate(new byte[0]).
|
||||
* <p>
|
||||
* If the 'CookieAuthentication' option is true, Tor writes a "magic cookie"
|
||||
* file named "control_auth_cookie" into its data directory. To authenticate,
|
||||
* the controller must send the contents of this file in <b>auth</b>.
|
||||
* <p>
|
||||
* If the 'HashedControlPassword' option is set, <b>auth</b> must contain the salted
|
||||
* hash of a secret password. The salted hash is computed according to the
|
||||
* S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier.
|
||||
* This is then encoded in hexadecimal, prefixed by the indicator sequence
|
||||
* "16:".
|
||||
* <p>
|
||||
* You can generate the salt of a password by calling
|
||||
* 'tor --hash-password <password>'
|
||||
* or by using the provided PasswordDigest class.
|
||||
* To authenticate under this scheme, the controller sends Tor the original
|
||||
* secret that was used to generate the password.
|
||||
*/
|
||||
public void authenticate(byte[] auth) throws IOException {
|
||||
String cmd = "AUTHENTICATE " + Bytes.hex(auth) + "\r\n";
|
||||
sendAndWaitForResponse(cmd, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the server to write out its configuration options into its torrc.
|
||||
*/
|
||||
public void saveConf() throws IOException {
|
||||
sendAndWaitForResponse("SAVECONF\r\n", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a signal from the controller to the Tor server.
|
||||
* <b>signal</b> is one of the following Strings:
|
||||
* <ul>
|
||||
* <li>"RELOAD" or "HUP" : Reload config items, refetch directory</li>
|
||||
* <li>"SHUTDOWN" or "INT" : Controlled shutdown: if server is an OP, exit immediately.
|
||||
* If it's an OR, close listeners and exit after 30 seconds</li>
|
||||
* <li>"DUMP" or "USR1" : Dump stats: log information about open connections and circuits</li>
|
||||
* <li>"DEBUG" or "USR2" : Debug: switch all open logs to loglevel debug</li>
|
||||
* <li>"HALT" or "TERM" : Immediate shutdown: clean up and exit now</li>
|
||||
* </ul>
|
||||
*/
|
||||
public void signal(String signal) throws IOException {
|
||||
String cmd = "SIGNAL " + signal + "\r\n";
|
||||
sendAndWaitForResponse(cmd, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a signal to the Tor process to shut it down or halt it.
|
||||
* Does not wait for a response.
|
||||
*/
|
||||
public void shutdownTor(String signal) throws IOException {
|
||||
String s = "SIGNAL " + signal + "\r\n";
|
||||
Waiter w = new Waiter();
|
||||
if (debugOutput != null)
|
||||
debugOutput.print(">> " + s);
|
||||
LOG.info("Entering synchronized (waiters)");
|
||||
synchronized (waiters) {
|
||||
LOG.info("Entered synchronized (waiters)");
|
||||
output.write(s);
|
||||
output.flush();
|
||||
LOG.info("Leaving synchronized (waiters)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Tor server that future SOCKS requests for connections to a set of original
|
||||
* addresses should be replaced with connections to the specified replacement
|
||||
* addresses. Each element of <b>kvLines</b> is a String of the form
|
||||
* "old-address new-address". This function returns the new address mapping.
|
||||
* <p>
|
||||
* The client may decline to provide a body for the original address, and
|
||||
* instead send a special null address ("0.0.0.0" for IPv4, "::0" for IPv6, or
|
||||
* "." for hostname), signifying that the server should choose the original
|
||||
* address itself, and return that address in the reply. The server
|
||||
* should ensure that it returns an element of address space that is unlikely
|
||||
* to be in actual use. If there is already an address mapped to the
|
||||
* destination address, the server may reuse that mapping.
|
||||
* <p>
|
||||
* If the original address is already mapped to a different address, the old
|
||||
* mapping is removed. If the original address and the destination address
|
||||
* are the same, the server removes any mapping in place for the original
|
||||
* address.
|
||||
* <p>
|
||||
* Mappings set by the controller last until the Tor process exits:
|
||||
* they never expire. If the controller wants the mapping to last only
|
||||
* a certain time, then it must explicitly un-map the address when that
|
||||
* time has elapsed.
|
||||
*/
|
||||
public Map<String, String> mapAddresses(Collection<String> kvLines)
|
||||
throws IOException {
|
||||
StringBuffer sb = new StringBuffer("MAPADDRESS");
|
||||
for (Iterator<String> it = kvLines.iterator(); it.hasNext(); ) {
|
||||
String kv = it.next();
|
||||
int i = kv.indexOf(' ');
|
||||
sb.append(" ").append(kv.substring(0, i)).append("=")
|
||||
.append(quote(kv.substring(i + 1)));
|
||||
}
|
||||
sb.append("\r\n");
|
||||
List<ReplyLine> lst = sendAndWaitForResponse(sb.toString(), null);
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (Iterator<ReplyLine> it = lst.iterator(); it.hasNext(); ) {
|
||||
String kv = (it.next()).msg;
|
||||
int idx = kv.indexOf('=');
|
||||
result.put(kv.substring(0, idx),
|
||||
kv.substring(idx + 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, String> mapAddresses(Map<String, String> addresses)
|
||||
throws IOException {
|
||||
List<String> kvList = new ArrayList<>();
|
||||
for (Iterator<Map.Entry<String, String>> it =
|
||||
addresses.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, String> e = it.next();
|
||||
kvList.add(e.getKey() + " " + e.getValue());
|
||||
}
|
||||
return mapAddresses(kvList);
|
||||
}
|
||||
|
||||
public String mapAddress(String fromAddr, String toAddr)
|
||||
throws IOException {
|
||||
List<String> lst = new ArrayList<>();
|
||||
lst.add(fromAddr + " " + toAddr + "\n");
|
||||
Map<String, String> m = mapAddresses(lst);
|
||||
return m.get(fromAddr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the Tor server for keyed values that are not stored in the torrc
|
||||
* configuration file. Returns a map of keys to values.
|
||||
* <p>
|
||||
* Recognized keys include:
|
||||
* <ul>
|
||||
* <li>"version" : The version of the server's software, including the name
|
||||
* of the software. (example: "Tor 0.0.9.4")</li>
|
||||
* <li>"desc/id/<OR identity>" or "desc/name/<OR nickname>" : the latest server
|
||||
* descriptor for a given OR, NUL-terminated. If no such OR is known, the
|
||||
* corresponding value is an empty string.</li>
|
||||
* <li>"network-status" : a space-separated list of all known OR identities.
|
||||
* This is in the same format as the router-status line in directories;
|
||||
* see tor-spec.txt for details.</li>
|
||||
* <li>"addr-mappings/all"</li>
|
||||
* <li>"addr-mappings/config"</li>
|
||||
* <li>"addr-mappings/cache"</li>
|
||||
* <li>"addr-mappings/control" : a space-separated list of address mappings, each
|
||||
* in the form of "from-address=to-address". The 'config' key
|
||||
* returns those address mappings set in the configuration; the 'cache'
|
||||
* key returns the mappings in the client-side DNS cache; the 'control'
|
||||
* key returns the mappings set via the control interface; the 'all'
|
||||
* target returns the mappings set through any mechanism.</li>
|
||||
* <li>"circuit-status" : A series of lines as for a circuit status event. Each line is of the form:
|
||||
* "CircuitID CircStatus Path"</li>
|
||||
* <li>"stream-status" : A series of lines as for a stream status event. Each is of the form:
|
||||
* "StreamID StreamStatus CircID Target"</li>
|
||||
* <li>"orconn-status" : A series of lines as for an OR connection status event. Each is of the
|
||||
* form: "ServerID ORStatus"</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Map<String, String> getInfo(Collection<String> keys)
|
||||
throws IOException {
|
||||
StringBuffer sb = new StringBuffer("GETINFO");
|
||||
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
|
||||
sb.append(" ").append(it.next());
|
||||
}
|
||||
sb.append("\r\n");
|
||||
List<ReplyLine> lst = sendAndWaitForResponse(sb.toString(), null);
|
||||
Map<String, String> m = new HashMap<>();
|
||||
for (Iterator<ReplyLine> it = lst.iterator(); it.hasNext(); ) {
|
||||
ReplyLine line = it.next();
|
||||
int idx = line.msg.indexOf('=');
|
||||
if (idx < 0)
|
||||
break;
|
||||
String k = line.msg.substring(0, idx);
|
||||
String v;
|
||||
if (line.rest != null) {
|
||||
v = line.rest;
|
||||
} else {
|
||||
v = line.msg.substring(idx + 1);
|
||||
}
|
||||
m.put(k, v);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the value of the information field 'key'
|
||||
*/
|
||||
public String getInfo(String key) throws IOException {
|
||||
List<String> lst = new ArrayList<>();
|
||||
lst.add(key);
|
||||
Map<String, String> m = getInfo(lst);
|
||||
return m.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* An extendCircuit request takes one of two forms: either the <b>circID</b> is zero, in
|
||||
* which case it is a request for the server to build a new circuit according
|
||||
* to the specified path, or the <b>circID</b> is nonzero, in which case it is a
|
||||
* request for the server to extend an existing circuit with that ID according
|
||||
* to the specified <b>path</b>.
|
||||
* <p>
|
||||
* If successful, returns the Circuit ID of the (maybe newly created) circuit.
|
||||
*/
|
||||
public String extendCircuit(String circID, String path) throws IOException {
|
||||
List<ReplyLine> lst = sendAndWaitForResponse(
|
||||
"EXTENDCIRCUIT " + circID + " " + path + "\r\n", null);
|
||||
return (lst.get(0)).msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the Tor server that the stream specified by <b>streamID</b> should be
|
||||
* associated with the circuit specified by <b>circID</b>.
|
||||
* <p>
|
||||
* Each stream may be associated with
|
||||
* at most one circuit, and multiple streams may share the same circuit.
|
||||
* Streams can only be attached to completed circuits (that is, circuits that
|
||||
* have sent a circuit status "BUILT" event or are listed as built in a
|
||||
* getInfo circuit-status request).
|
||||
* <p>
|
||||
* If <b>circID</b> is 0, responsibility for attaching the given stream is
|
||||
* returned to Tor.
|
||||
* <p>
|
||||
* By default, Tor automatically attaches streams to
|
||||
* circuits itself, unless the configuration variable
|
||||
* "__LeaveStreamsUnattached" is set to "1". Attempting to attach streams
|
||||
* via TC when "__LeaveStreamsUnattached" is false may cause a race between
|
||||
* Tor and the controller, as both attempt to attach streams to circuits.
|
||||
*/
|
||||
public void attachStream(String streamID, String circID)
|
||||
throws IOException {
|
||||
sendAndWaitForResponse(
|
||||
"ATTACHSTREAM " + streamID + " " + circID + "\r\n", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor about the server descriptor in <b>desc</b>.
|
||||
* <p>
|
||||
* The descriptor, when parsed, must contain a number of well-specified
|
||||
* fields, including fields for its nickname and identity.
|
||||
*/
|
||||
// More documentation here on format of desc?
|
||||
// No need for return value? control-spec.txt says reply is merely "250 OK" on success...
|
||||
public String postDescriptor(String desc) throws IOException {
|
||||
List<ReplyLine> lst =
|
||||
sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc);
|
||||
return (lst.get(0)).msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to change the exit address of the stream identified by <b>streamID</b>
|
||||
* to <b>address</b>. No remapping is performed on the new provided address.
|
||||
* <p>
|
||||
* To be sure that the modified address will be used, this event must be sent
|
||||
* after a new stream event is received, and before attaching this stream to
|
||||
* a circuit.
|
||||
*/
|
||||
public void redirectStream(String streamID, String address)
|
||||
throws IOException {
|
||||
sendAndWaitForResponse(
|
||||
"REDIRECTSTREAM " + streamID + " " + address + "\r\n",
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to close the stream identified by <b>streamID</b>.
|
||||
* <b>reason</b> should be one of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal:
|
||||
* <ul>
|
||||
* <li>1 -- REASON_MISC (catch-all for unlisted reasons)</li>
|
||||
* <li>2 -- REASON_RESOLVEFAILED (couldn't look up hostname)</li>
|
||||
* <li>3 -- REASON_CONNECTREFUSED (remote host refused connection)</li>
|
||||
* <li>4 -- REASON_EXITPOLICY (OR refuses to connect to host or port)</li>
|
||||
* <li>5 -- REASON_DESTROY (Circuit is being destroyed)</li>
|
||||
* <li>6 -- REASON_DONE (Anonymized TCP connection was closed)</li>
|
||||
* <li>7 -- REASON_TIMEOUT (Connection timed out, or OR timed out while connecting)</li>
|
||||
* <li>8 -- (unallocated)</li>
|
||||
* <li>9 -- REASON_HIBERNATING (OR is temporarily hibernating)</li>
|
||||
* <li>10 -- REASON_INTERNAL (Internal error at the OR)</li>
|
||||
* <li>11 -- REASON_RESOURCELIMIT (OR has no resources to fulfill request)</li>
|
||||
* <li>12 -- REASON_CONNRESET (Connection was unexpectedly reset)</li>
|
||||
* <li>13 -- REASON_TORPROTOCOL (Sent when closing connection because of Tor protocol violations)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Tor may hold the stream open for a while to flush any data that is pending.
|
||||
*/
|
||||
public void closeStream(String streamID, byte reason)
|
||||
throws IOException {
|
||||
sendAndWaitForResponse(
|
||||
"CLOSESTREAM " + streamID + " " + reason + "\r\n", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to close the circuit identified by <b>circID</b>.
|
||||
* If <b>ifUnused</b> is true, do not close the circuit unless it is unused.
|
||||
*/
|
||||
public void closeCircuit(String circID, boolean ifUnused)
|
||||
throws IOException {
|
||||
sendAndWaitForResponse("CLOSECIRCUIT " + circID +
|
||||
(ifUnused ? " IFUNUSED" : "") + "\r\n", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to exit when this control connection is closed. This command
|
||||
* was added in Tor 0.2.2.28-beta.
|
||||
*/
|
||||
public void takeOwnership() throws IOException {
|
||||
sendAndWaitForResponse("TAKEOWNERSHIP\r\n", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to generate and set up a new onion service using the best
|
||||
* supported algorithm.
|
||||
* <p/>
|
||||
* ADD_ONION was added in Tor 0.2.7.1-alpha.
|
||||
*/
|
||||
public Map<String, String> addOnion(Map<Integer, String> portLines)
|
||||
throws IOException {
|
||||
return addOnion("NEW:BEST", portLines, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to generate and set up a new onion service using the best
|
||||
* supported algorithm.
|
||||
* <p/>
|
||||
* ADD_ONION was added in Tor 0.2.7.1-alpha.
|
||||
*/
|
||||
public Map<String, String> addOnion(Map<Integer, String> portLines,
|
||||
boolean ephemeral, boolean detach)
|
||||
throws IOException {
|
||||
return addOnion("NEW:BEST", portLines, ephemeral, detach);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to set up an onion service using the provided private key.
|
||||
* <p/>
|
||||
* ADD_ONION was added in Tor 0.2.7.1-alpha.
|
||||
*/
|
||||
public Map<String, String> addOnion(String privKey,
|
||||
Map<Integer, String> portLines)
|
||||
throws IOException {
|
||||
return addOnion(privKey, portLines, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to set up an onion service using the provided private key.
|
||||
* <p/>
|
||||
* ADD_ONION was added in Tor 0.2.7.1-alpha.
|
||||
*/
|
||||
public Map<String, String> addOnion(String privKey,
|
||||
Map<Integer, String> portLines,
|
||||
boolean ephemeral, boolean detach)
|
||||
throws IOException {
|
||||
List<String> flags = new ArrayList<>();
|
||||
if (ephemeral)
|
||||
flags.add("DiscardPK");
|
||||
if (detach)
|
||||
flags.add("Detach");
|
||||
return addOnion(privKey, portLines, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to set up an onion service.
|
||||
* <p/>
|
||||
* ADD_ONION was added in Tor 0.2.7.1-alpha.
|
||||
*/
|
||||
public Map<String, String> addOnion(String privKey,
|
||||
Map<Integer, String> portLines,
|
||||
List<String> flags)
|
||||
throws IOException {
|
||||
if (privKey.indexOf(':') < 0)
|
||||
throw new IllegalArgumentException("Invalid privKey");
|
||||
if (portLines == null || portLines.size() < 1)
|
||||
throw new IllegalArgumentException(
|
||||
"Must provide at least one port line");
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append("ADD_ONION ").append(privKey);
|
||||
if (flags != null && flags.size() > 0) {
|
||||
b.append(" Flags=");
|
||||
String separator = "";
|
||||
for (String flag : flags) {
|
||||
b.append(separator).append(flag);
|
||||
separator = ",";
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Integer, String> portLine : portLines.entrySet()) {
|
||||
int virtPort = portLine.getKey();
|
||||
String target = portLine.getValue();
|
||||
b.append(" Port=").append(virtPort);
|
||||
if (target != null && target.length() > 0)
|
||||
b.append(",").append(target);
|
||||
}
|
||||
b.append("\r\n");
|
||||
List<ReplyLine> lst = sendAndWaitForResponse(b.toString(), null);
|
||||
Map<String, String> ret = new HashMap<>();
|
||||
ret.put(HS_ADDRESS, (lst.get(0)).msg.split("=", 2)[1]);
|
||||
if (lst.size() > 2)
|
||||
ret.put(HS_PRIVKEY, (lst.get(1)).msg.split("=", 2)[1]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to take down an onion service previously set up with
|
||||
* addOnion(). The hostname excludes the .onion extension.
|
||||
* <p/>
|
||||
* DEL_ONION was added in Tor 0.2.7.1-alpha.
|
||||
*/
|
||||
public void delOnion(String hostname) throws IOException {
|
||||
sendAndWaitForResponse("DEL_ONION " + hostname + "\r\n", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Tor to forget any cached client state relating to the hidden
|
||||
* service with the given hostname (excluding the .onion extension).
|
||||
*/
|
||||
public void forgetHiddenService(String hostname) throws IOException {
|
||||
sendAndWaitForResponse("HSFORGET " + hostname + "\r\n", null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An exception raised when Tor tells us about an error.
|
||||
*/
|
||||
public class TorControlError extends IOException {
|
||||
|
||||
static final long serialVersionUID = 3;
|
||||
|
||||
private final int errorType;
|
||||
|
||||
public TorControlError(int type, String s) {
|
||||
super(s);
|
||||
errorType = type;
|
||||
}
|
||||
|
||||
public TorControlError(String s) {
|
||||
this(-1, s);
|
||||
}
|
||||
|
||||
public int getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
public String getErrorMsg() {
|
||||
try {
|
||||
if (errorType == -1)
|
||||
return null;
|
||||
return TorControlCommands.ERROR_MSGS[errorType];
|
||||
} catch (ArrayIndexOutOfBoundsException ex) {
|
||||
return "Unrecongized error #"+errorType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An exception raised when Tor behaves in an unexpected way.
|
||||
*/
|
||||
public class TorControlSyntaxError extends IOException {
|
||||
|
||||
static final long serialVersionUID = 3;
|
||||
|
||||
public TorControlSyntaxError(String s) { super(s); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.class
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control.examples;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Iterator;
|
||||
import net.freehaven.tor.control.EventHandler;
|
||||
|
||||
public class DebuggingEventHandler implements EventHandler {
|
||||
|
||||
private final PrintWriter out;
|
||||
|
||||
public DebuggingEventHandler(PrintWriter p) {
|
||||
out = p;
|
||||
}
|
||||
|
||||
public void circuitStatus(String status, String circID, String path) {
|
||||
out.println("Circuit "+circID+" is now "+status+" (path="+path+")");
|
||||
}
|
||||
public void streamStatus(String status, String streamID, String target) {
|
||||
out.println("Stream "+streamID+" is now "+status+" (target="+target+")");
|
||||
}
|
||||
public void orConnStatus(String status, String orName) {
|
||||
out.println("OR connection to "+orName+" is now "+status);
|
||||
}
|
||||
public void bandwidthUsed(long read, long written) {
|
||||
out.println("Bandwidth usage: "+read+" bytes read; "+
|
||||
written+" bytes written.");
|
||||
}
|
||||
public void newDescriptors(java.util.List<String> orList) {
|
||||
out.println("New descriptors for routers:");
|
||||
for (Iterator<String> i = orList.iterator(); i.hasNext(); )
|
||||
out.println(" "+i.next());
|
||||
}
|
||||
public void message(String type, String msg) {
|
||||
out.println("["+type+"] "+msg.trim());
|
||||
}
|
||||
|
||||
public void unrecognized(String type, String msg) {
|
||||
out.println("unrecognized event ["+type+"] "+msg.trim());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2005 Nick Mathewson, Roger Dingledine
|
||||
// See LICENSE file for copying information
|
||||
package net.freehaven.tor.control.examples;
|
||||
|
||||
import net.freehaven.tor.control.*;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class Main implements TorControlCommands {
|
||||
|
||||
public static void main(String args[]) {
|
||||
if (args.length < 1) {
|
||||
System.err.println("No command given.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (args[0].equals("set-config")) {
|
||||
setConfig(args);
|
||||
} else if (args[0].equals("get-config")) {
|
||||
getConfig(args);
|
||||
} else if (args[0].equals("get-info")) {
|
||||
getInfo(args);
|
||||
} else if (args[0].equals("listen")) {
|
||||
listenForEvents(args);
|
||||
} else if (args[0].equals("signal")) {
|
||||
signal(args);
|
||||
} else if (args[0].equals("auth")) {
|
||||
authDemo(args);
|
||||
} else {
|
||||
System.err.println("Unrecognized command: "+args[0]);
|
||||
}
|
||||
} catch (EOFException ex) {
|
||||
System.out.println("Control socket closed by Tor.");
|
||||
} catch (TorControlError ex) {
|
||||
System.err.println("Error from Tor process: "+
|
||||
ex+" ["+ex.getErrorMsg()+"]");
|
||||
} catch (IOException ex) {
|
||||
System.err.println("IO exception when talking to Tor process: "+
|
||||
ex);
|
||||
ex.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
private static TorControlConnection getConnection(String[] args,
|
||||
boolean daemon) throws IOException {
|
||||
Socket s = new Socket("127.0.0.1", 9100);
|
||||
TorControlConnection conn = new TorControlConnection(s);
|
||||
conn.launchThread(daemon);
|
||||
conn.authenticate(new byte[0]);
|
||||
return conn;
|
||||
}
|
||||
|
||||
private static TorControlConnection getConnection(String[] args)
|
||||
throws IOException {
|
||||
return getConnection(args, true);
|
||||
}
|
||||
|
||||
public static void setConfig(String[] args) throws IOException {
|
||||
// Usage: "set-config [-save] key value key value key value"
|
||||
TorControlConnection conn = getConnection(args);
|
||||
ArrayList<String> lst = new ArrayList<String>();
|
||||
int i = 1;
|
||||
boolean save = false;
|
||||
if (args[i].equals("-save")) {
|
||||
save = true;
|
||||
++i;
|
||||
}
|
||||
for (; i < args.length; i +=2) {
|
||||
lst.add(args[i]+" "+args[i+1]);
|
||||
}
|
||||
conn.setConf(lst);
|
||||
if (save) {
|
||||
conn.saveConf();
|
||||
}
|
||||
}
|
||||
|
||||
public static void getConfig(String[] args) throws IOException {
|
||||
// Usage: get-config key key key
|
||||
TorControlConnection conn = getConnection(args);
|
||||
List<ConfigEntry> lst = conn.getConf(Arrays.asList(args).subList(1,args.length));
|
||||
for (Iterator<ConfigEntry> i = lst.iterator(); i.hasNext(); ) {
|
||||
ConfigEntry e = i.next();
|
||||
System.out.println("KEY: "+e.key);
|
||||
System.out.println("VAL: "+e.value);
|
||||
}
|
||||
}
|
||||
|
||||
public static void getInfo(String[] args) throws IOException {
|
||||
TorControlConnection conn = getConnection(args);
|
||||
Map<String,String> m = conn.getInfo(Arrays.asList(args).subList(1,args.length));
|
||||
for (Iterator<Map.Entry<String, String>> i = m.entrySet().iterator(); i.hasNext(); ) {
|
||||
Map.Entry<String,String> e = i.next();
|
||||
System.out.println("KEY: "+e.getKey());
|
||||
System.out.println("VAL: "+e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public static void listenForEvents(String[] args) throws IOException {
|
||||
// Usage: listen [circ|stream|orconn|bw|newdesc|info|notice|warn|error]*
|
||||
TorControlConnection conn = getConnection(args, false);
|
||||
ArrayList<String> lst = new ArrayList<String>();
|
||||
for (int i = 1; i < args.length; ++i) {
|
||||
lst.add(args[i]);
|
||||
}
|
||||
conn.setEventHandler(
|
||||
new DebuggingEventHandler(new PrintWriter(System.out, true)));
|
||||
conn.setEvents(lst);
|
||||
}
|
||||
|
||||
public static void signal(String[] args) throws IOException {
|
||||
// Usage signal [reload|shutdown|dump|debug|halt]
|
||||
TorControlConnection conn = getConnection(args, false);
|
||||
// distinguish shutdown signal from other signals
|
||||
if ("SHUTDOWN".equalsIgnoreCase(args[1])
|
||||
|| "HALT".equalsIgnoreCase(args[1])) {
|
||||
conn.shutdownTor(args[1].toUpperCase());
|
||||
} else {
|
||||
conn.signal(args[1].toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
public static void authDemo(String[] args) throws IOException {
|
||||
|
||||
PasswordDigest pwd = PasswordDigest.generateDigest();
|
||||
Socket s = new Socket("127.0.0.1", 9100);
|
||||
TorControlConnection conn = new TorControlConnection(s);
|
||||
conn.launchThread(true);
|
||||
conn.authenticate(new byte[0]);
|
||||
|
||||
conn.setConf("HashedControlPassword", pwd.getHashedPassword());
|
||||
|
||||
s = new Socket("127.0.0.1", 9100);
|
||||
conn = new TorControlConnection(s);
|
||||
conn.launchThread(true);
|
||||
conn.authenticate(pwd.getSecret());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package org.briarproject.bramble.contact;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeListener;
|
||||
import org.briarproject.bramble.api.contact.ContactExchangeTask;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.contact.event.ContactExchangeFailedEvent;
|
||||
import org.briarproject.bramble.api.contact.event.ContactExchangeSucceededEvent;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
@@ -13,6 +14,7 @@ import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.db.ContactExistsException;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
@@ -63,6 +65,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
private final ClientHelper clientHelper;
|
||||
private final RecordReaderFactory recordReaderFactory;
|
||||
private final RecordWriterFactory recordWriterFactory;
|
||||
private final EventBus eventBus;
|
||||
private final Clock clock;
|
||||
private final ConnectionManager connectionManager;
|
||||
private final ContactManager contactManager;
|
||||
@@ -71,7 +74,6 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
private final StreamReaderFactory streamReaderFactory;
|
||||
private final StreamWriterFactory streamWriterFactory;
|
||||
|
||||
private volatile ContactExchangeListener listener;
|
||||
private volatile LocalAuthor localAuthor;
|
||||
private volatile DuplexTransportConnection conn;
|
||||
private volatile TransportId transportId;
|
||||
@@ -81,8 +83,9 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
@Inject
|
||||
ContactExchangeTaskImpl(DatabaseComponent db, ClientHelper clientHelper,
|
||||
RecordReaderFactory recordReaderFactory,
|
||||
RecordWriterFactory recordWriterFactory, Clock clock,
|
||||
ConnectionManager connectionManager, ContactManager contactManager,
|
||||
RecordWriterFactory recordWriterFactory, EventBus eventBus,
|
||||
Clock clock, ConnectionManager connectionManager,
|
||||
ContactManager contactManager,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
CryptoComponent crypto, StreamReaderFactory streamReaderFactory,
|
||||
StreamWriterFactory streamWriterFactory) {
|
||||
@@ -90,6 +93,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
this.clientHelper = clientHelper;
|
||||
this.recordReaderFactory = recordReaderFactory;
|
||||
this.recordWriterFactory = recordWriterFactory;
|
||||
this.eventBus = eventBus;
|
||||
this.clock = clock;
|
||||
this.connectionManager = connectionManager;
|
||||
this.contactManager = contactManager;
|
||||
@@ -100,11 +104,9 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startExchange(ContactExchangeListener listener,
|
||||
LocalAuthor localAuthor, SecretKey masterSecret,
|
||||
public void startExchange(LocalAuthor localAuthor, SecretKey masterSecret,
|
||||
DuplexTransportConnection conn, TransportId transportId,
|
||||
boolean alice) {
|
||||
this.listener = listener;
|
||||
this.localAuthor = localAuthor;
|
||||
this.conn = conn;
|
||||
this.transportId = transportId;
|
||||
@@ -123,8 +125,8 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
out = conn.getWriter().getOutputStream();
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
listener.contactExchangeFailed();
|
||||
tryToClose(conn);
|
||||
eventBus.broadcast(new ContactExchangeFailedEvent());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,7 +136,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
localProperties = transportPropertyManager.getLocalProperties();
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
listener.contactExchangeFailed();
|
||||
eventBus.broadcast(new ContactExchangeFailedEvent());
|
||||
tryToClose(conn);
|
||||
return;
|
||||
}
|
||||
@@ -196,7 +198,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
listener.contactExchangeFailed();
|
||||
eventBus.broadcast(new ContactExchangeFailedEvent());
|
||||
tryToClose(conn);
|
||||
return;
|
||||
}
|
||||
@@ -204,7 +206,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
// Verify the contact's signature
|
||||
if (!verify(remoteInfo.author, remoteNonce, remoteInfo.signature)) {
|
||||
LOG.warning("Invalid signature");
|
||||
listener.contactExchangeFailed();
|
||||
eventBus.broadcast(new ContactExchangeFailedEvent());
|
||||
tryToClose(conn);
|
||||
return;
|
||||
}
|
||||
@@ -221,15 +223,17 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
|
||||
conn);
|
||||
// Pseudonym exchange succeeded
|
||||
LOG.info("Pseudonym exchange succeeded");
|
||||
listener.contactExchangeSucceeded(remoteInfo.author);
|
||||
eventBus.broadcast(
|
||||
new ContactExchangeSucceededEvent(remoteInfo.author));
|
||||
} catch (ContactExistsException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
tryToClose(conn);
|
||||
listener.duplicateContact(remoteInfo.author);
|
||||
eventBus.broadcast(
|
||||
new ContactExchangeFailedEvent(remoteInfo.author));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
tryToClose(conn);
|
||||
listener.contactExchangeFailed();
|
||||
eventBus.broadcast(new ContactExchangeFailedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.briarproject.bramble.contact;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.contact.PendingContact;
|
||||
import org.briarproject.bramble.api.contact.PendingContactId;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
@@ -19,12 +21,16 @@ import org.briarproject.bramble.api.transport.KeyManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.contact.PendingContact.PendingContactState.WAITING_FOR_CONNECTION;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES;
|
||||
import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN;
|
||||
@@ -36,6 +42,12 @@ import static org.briarproject.bramble.util.StringUtils.toUtf8;
|
||||
@NotNullByDefault
|
||||
class ContactManagerImpl implements ContactManager {
|
||||
|
||||
private static final int LINK_LENGTH = 64;
|
||||
private static final String REMOTE_CONTACT_LINK =
|
||||
"briar://" + getRandomBase32String(LINK_LENGTH);
|
||||
private static final Pattern LINK_REGEX =
|
||||
Pattern.compile("(briar://)?([a-z2-7]{" + LINK_LENGTH + "})");
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final KeyManager keyManager;
|
||||
private final IdentityManager identityManager;
|
||||
@@ -84,6 +96,48 @@ class ContactManagerImpl implements ContactManager {
|
||||
verified, active));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteContactLink() {
|
||||
// TODO replace with real implementation
|
||||
return REMOTE_CONTACT_LINK;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static String getRandomBase32String(int length) {
|
||||
Random random = new Random();
|
||||
char[] c = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
int character = random.nextInt(32);
|
||||
if (character < 26) c[i] = (char) ('a' + character);
|
||||
else c[i] = (char) ('2' + (character - 26));
|
||||
}
|
||||
return new String(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidRemoteContactLink(String link) {
|
||||
return LINK_REGEX.matcher(link).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingContact addRemoteContactRequest(String link, String alias) {
|
||||
// TODO replace with real implementation
|
||||
PendingContactId id = new PendingContactId(link.getBytes());
|
||||
return new PendingContact(id, alias, WAITING_FOR_CONNECTION,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PendingContact> getPendingContacts() {
|
||||
// TODO replace with real implementation
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePendingContact(PendingContact pendingContact) {
|
||||
// TODO replace with real implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contact getContact(ContactId c) throws DbException {
|
||||
return db.transactionWithResult(true, txn -> db.getContact(txn, c));
|
||||
|
||||
@@ -17,16 +17,24 @@ public interface CircumventionProvider {
|
||||
String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"};
|
||||
|
||||
/**
|
||||
* Countries where vanilla bridge connection are likely to work.
|
||||
* Countries where obfs4 bridge connection are likely to work.
|
||||
* Should be a subset of {@link #BLOCKED}.
|
||||
*/
|
||||
String[] BRIDGES = { "EG", "BY", "TR", "SY", "VE" };
|
||||
String[] BRIDGES = { "CN", "IR", "EG", "BY", "TR", "SY", "VE" };
|
||||
|
||||
/**
|
||||
* Countries where obfs4 bridges won't work and meek is needed.
|
||||
* Should be a subset of {@link #BRIDGES}.
|
||||
*/
|
||||
String[] NEEDS_MEEK = {"CN", "IR"};
|
||||
|
||||
boolean isTorProbablyBlocked(String countryCode);
|
||||
|
||||
boolean doBridgesWork(String countryCode);
|
||||
|
||||
boolean needsMeek(String countryCode);
|
||||
|
||||
@IoExecutor
|
||||
List<String> getBridges();
|
||||
List<String> getBridges(boolean meek);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
new HashSet<>(asList(BLOCKED));
|
||||
private static final Set<String> BRIDGES_WORK_IN_COUNTRIES =
|
||||
new HashSet<>(asList(BRIDGES));
|
||||
private static final Set<String> BRIDGES_NEED_MEEK =
|
||||
new HashSet<>(asList(NEEDS_MEEK));
|
||||
|
||||
@Nullable
|
||||
private volatile List<String> bridges = null;
|
||||
@@ -40,9 +42,14 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
return BRIDGES_WORK_IN_COUNTRIES.contains(countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsMeek(String countryCode) {
|
||||
return BRIDGES_NEED_MEEK.contains(countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public List<String> getBridges() {
|
||||
public List<String> getBridges(boolean useMeek) {
|
||||
List<String> bridges = this.bridges;
|
||||
if (bridges != null) return new ArrayList<>(bridges);
|
||||
|
||||
@@ -53,6 +60,8 @@ class CircumventionProviderImpl implements CircumventionProvider {
|
||||
bridges = new ArrayList<>();
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
boolean isMeekBridge = line.startsWith("Bridge meek");
|
||||
if (useMeek && !isMeekBridge || !useMeek && isMeekBridge) continue;
|
||||
if (!line.startsWith("#")) bridges.add(line);
|
||||
}
|
||||
scanner.close();
|
||||
|
||||
@@ -38,6 +38,8 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
@@ -69,6 +71,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_WITH_BRIDGES;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V2;
|
||||
import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
|
||||
@@ -105,7 +108,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
private final ResourceProvider resourceProvider;
|
||||
private final int maxLatency, maxIdleTime, socketTimeout;
|
||||
private final File torDirectory, torFile, geoIpFile, obfs4File, configFile;
|
||||
private final File doneFile, cookieFile;
|
||||
private final File doneFile, cookieFile, jtorCTLFile;
|
||||
private final ConnectionStatus connectionStatus;
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
|
||||
@@ -150,6 +153,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
configFile = new File(torDirectory, "torrc");
|
||||
doneFile = new File(torDirectory, "done");
|
||||
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
|
||||
jtorCTLFile = new File(torDirectory, "jtorctl.out");
|
||||
connectionStatus = new ConnectionStatus();
|
||||
// Don't execute more than one connection status check at a time
|
||||
connectionStatusExecutor =
|
||||
@@ -204,6 +208,19 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
} catch (SecurityException | IOException e) {
|
||||
throw new PluginException(e);
|
||||
}
|
||||
// FIXME
|
||||
long torPid = getPid(torProcess);
|
||||
LOG.info("Tor PID: " + torPid);
|
||||
/*
|
||||
pb = new ProcessBuilder("/usr/bin/strace", "-ff", "-o", "strace.out",
|
||||
"-p", String.valueOf(torPid));
|
||||
try {
|
||||
pb.start();
|
||||
LOG.info("Started strace");
|
||||
} catch (SecurityException | IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
*/
|
||||
// Log the process's standard output until it detaches
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
Scanner stdout = new Scanner(torProcess.getInputStream());
|
||||
@@ -248,6 +265,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
controlConnection = new TorControlConnection(controlSocket);
|
||||
controlConnection.authenticate(read(cookieFile));
|
||||
controlConnection.setConf("LOG", "debug file torlog");
|
||||
controlConnection.setDebugging(
|
||||
new PrintWriter(new FileOutputStream(jtorCTLFile), true));
|
||||
// FIXME Spam the control port with enable/disable network commands
|
||||
spamControlPort();
|
||||
// Tell Tor to exit when the control connection is closed
|
||||
controlConnection.takeOwnership();
|
||||
controlConnection.resetConf(Collections.singletonList(OWNER));
|
||||
@@ -271,6 +293,38 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
bind();
|
||||
}
|
||||
|
||||
// FIXME
|
||||
private long getPid(Process p) {
|
||||
long pid = -1;
|
||||
try {
|
||||
if (p.getClass().getName().equals("java.lang.UNIXProcess")) {
|
||||
Field f = p.getClass().getDeclaredField("pid");
|
||||
f.setAccessible(true);
|
||||
pid = f.getLong(p);
|
||||
f.setAccessible(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logException(LOG, INFO, e);
|
||||
}
|
||||
return pid;
|
||||
}
|
||||
|
||||
// FIXME
|
||||
private void spamControlPort() {
|
||||
ioExecutor.execute(() -> {
|
||||
LOG.info("Spamming control port");
|
||||
try {
|
||||
//noinspection InfiniteLoopStatement
|
||||
for (boolean bridges = true; ; bridges = !bridges) {
|
||||
LOG.info("Enable bridges " + bridges);
|
||||
enableBridges(bridges, false);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean assetsAreUpToDate() {
|
||||
return doneFile.lastModified() > getLastUpdateTime();
|
||||
}
|
||||
@@ -469,13 +523,19 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (!enable) callback.transportDisabled();
|
||||
}
|
||||
|
||||
private void enableBridges(boolean enable) throws IOException {
|
||||
private void enableBridges(boolean enable, boolean needsMeek)
|
||||
throws IOException {
|
||||
if (enable) {
|
||||
Collection<String> conf = new ArrayList<>();
|
||||
conf.add("UseBridges 1");
|
||||
conf.add("ClientTransportPlugin obfs4 exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
conf.addAll(circumventionProvider.getBridges());
|
||||
if (needsMeek) {
|
||||
conf.add("ClientTransportPlugin meek_lite exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
} else {
|
||||
conf.add("ClientTransportPlugin obfs4 exec " +
|
||||
obfs4File.getAbsolutePath());
|
||||
}
|
||||
conf.addAll(circumventionProvider.getBridges(needsMeek));
|
||||
controlConnection.setConf(conf);
|
||||
} else {
|
||||
controlConnection.setConf("UseBridges", "0");
|
||||
@@ -648,8 +708,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (s.getNamespace().equals(ID.getString())) {
|
||||
LOG.info("Tor settings updated");
|
||||
settings = s.getSettings();
|
||||
// Works around a bug introduced in Tor 0.3.4.8. Could be
|
||||
// replaced with callback.transportDisabled() when fixed.
|
||||
// Works around a bug introduced in Tor 0.3.4.8.
|
||||
// https://trac.torproject.org/projects/tor/ticket/28027
|
||||
// Could be replaced with callback.transportDisabled()
|
||||
// when fixed.
|
||||
disableNetwork();
|
||||
updateConnectionStatus(networkManager.getNetworkStatus(),
|
||||
batteryManager.isCharging());
|
||||
@@ -685,6 +747,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
int network = settings.getInt(PREF_TOR_NETWORK,
|
||||
PREF_TOR_NETWORK_AUTOMATIC);
|
||||
boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE, true);
|
||||
boolean onlyWhenCharging =
|
||||
settings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING, false);
|
||||
boolean bridgesWork = circumventionProvider.doBridgesWork(country);
|
||||
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
|
||||
|
||||
@@ -699,21 +763,29 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (!online) {
|
||||
LOG.info("Disabling network, device is offline");
|
||||
enableNetwork(false);
|
||||
} else if (!charging && onlyWhenCharging) {
|
||||
LOG.info("Disabling network, device is on battery");
|
||||
enableNetwork(false);
|
||||
} else if (network == PREF_TOR_NETWORK_NEVER ||
|
||||
(!useMobile && !wifi)) {
|
||||
LOG.info("Disabling network due to setting");
|
||||
LOG.info("Disabling network, device is using mobile data");
|
||||
enableNetwork(false);
|
||||
} else if (automatic && blocked && !bridgesWork) {
|
||||
LOG.info("Disabling network, country is blocked");
|
||||
enableNetwork(false);
|
||||
} else if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
|
||||
(automatic && bridgesWork)) {
|
||||
LOG.info("Enabling network, using bridges");
|
||||
enableBridges(true);
|
||||
if (circumventionProvider.needsMeek(country)) {
|
||||
LOG.info("Enabling network, using meek bridges");
|
||||
enableBridges(true, true);
|
||||
} else {
|
||||
LOG.info("Enabling network, using obfs4 bridges");
|
||||
enableBridges(true, false);
|
||||
}
|
||||
enableNetwork(true);
|
||||
} else {
|
||||
LOG.info("Enabling network, not using bridges");
|
||||
enableBridges(false);
|
||||
enableBridges(false, false);
|
||||
enableNetwork(true);
|
||||
}
|
||||
if (online && wifi && charging) {
|
||||
|
||||
@@ -89,14 +89,9 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
|
||||
public Visibility getClientVisibility(Transaction txn, ContactId contactId,
|
||||
ClientId clientId, int majorVersion) throws DbException {
|
||||
try {
|
||||
Contact contact = db.getContact(txn, contactId);
|
||||
Group g = getContactGroup(contact);
|
||||
// Contact may be in the process of being added or removed, so
|
||||
// contact group may not exist
|
||||
if (!db.containsGroup(txn, g.getId())) return INVISIBLE;
|
||||
LatestUpdates latest = findLatestUpdates(txn, g.getId());
|
||||
LatestUpdates latest = findLatestUpdates(txn, contactId);
|
||||
if (latest == null || latest.remote == null) return INVISIBLE;
|
||||
if (latest.local == null) throw new DbException();
|
||||
if (latest.remote == null) return INVISIBLE;
|
||||
Update localUpdate = loadUpdate(txn, latest.local.messageId);
|
||||
Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
|
||||
Map<ClientMajorVersion, Visibility> visibilities =
|
||||
@@ -110,6 +105,24 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClientMinorVersion(Transaction txn, ContactId contactId,
|
||||
ClientId clientId, int majorVersion) throws DbException {
|
||||
try {
|
||||
LatestUpdates latest = findLatestUpdates(txn, contactId);
|
||||
if (latest == null || latest.remote == null) return -1;
|
||||
Update remoteUpdate = loadUpdate(txn, latest.remote.messageId);
|
||||
ClientMajorVersion cv =
|
||||
new ClientMajorVersion(clientId, majorVersion);
|
||||
for (ClientState remote : remoteUpdate.states) {
|
||||
if (remote.majorVersion.equals(cv)) return remote.minorVersion;
|
||||
}
|
||||
return -1;
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLocalState(Transaction txn) throws DbException {
|
||||
if (db.containsGroup(txn, localGroup.getId())) return;
|
||||
@@ -336,6 +349,17 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
|
||||
MAJOR_VERSION, c);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LatestUpdates findLatestUpdates(Transaction txn, ContactId c)
|
||||
throws DbException, FormatException {
|
||||
Contact contact = db.getContact(txn, c);
|
||||
Group g = getContactGroup(contact);
|
||||
// Contact may be in the process of being added or removed, so
|
||||
// contact group may not exist
|
||||
if (!db.containsGroup(txn, g.getId())) return null;
|
||||
return findLatestUpdates(txn, g.getId());
|
||||
}
|
||||
|
||||
private LatestUpdates findLatestUpdates(Transaction txn, GroupId g)
|
||||
throws DbException, FormatException {
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
|
||||
Bridge obfs4 52.15.78.72:9443 02069A3C5362476936B62BA6F5ACC41ABD573A9B cert=ijYG/OKc7kqu2YzKNFfeXN7/BG2BOgfEP2KyYEiGDQthnHbsOiTWHeIG0WJVW+BckzDgKw iat-mode=0
|
||||
Bridge obfs4 13.58.29.242:9443 0C58939A77DA6B6B29D4B5236A75865659607AE0 cert=OylWIEHb/ezpq1zWxW0sgKRn+9ARH2eOcQOZ8/Gew+4l+oKOhQ2jUX/Y+FSl61JorXZUWA iat-mode=0
|
||||
Bridge obfs4 45.33.37.112:9443 60A609BB4ABE8D46E634AE81ED29ADAB7776B399 cert=t5v19WmNv5Sc2YPNr8RQids365W7MY8zJwQVkOxBjUMFomMWARDzsbYpcWLLcw0J9Gm+BQ iat-mode=0
|
||||
Bridge obfs4 45.33.37.112:9443 60A609BB4ABE8D46E634AE81ED29ADAB7776B399 cert=t5v19WmNv5Sc2YPNr8RQids365W7MY8zJwQVkOxBjUMFomMWARDzsbYpcWLLcw0J9Gm+BQ iat-mode=0
|
||||
Bridge meek_lite 0.0.2.0:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
|
||||
@@ -1,135 +1,118 @@
|
||||
package org.briarproject.bramble.util;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.test.BrambleTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import static java.util.Arrays.fill;
|
||||
import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED;
|
||||
import static org.briarproject.bramble.util.ByteUtils.MAX_32_BIT_UNSIGNED;
|
||||
import static org.briarproject.bramble.util.ByteUtils.MAX_VARINT_BYTES;
|
||||
import static org.briarproject.bramble.util.ByteUtils.getVarIntBytes;
|
||||
import static org.briarproject.bramble.util.ByteUtils.readUint16;
|
||||
import static org.briarproject.bramble.util.ByteUtils.readUint32;
|
||||
import static org.briarproject.bramble.util.ByteUtils.readUint64;
|
||||
import static org.briarproject.bramble.util.ByteUtils.readVarInt;
|
||||
import static org.briarproject.bramble.util.ByteUtils.writeUint16;
|
||||
import static org.briarproject.bramble.util.ByteUtils.writeUint32;
|
||||
import static org.briarproject.bramble.util.ByteUtils.writeUint64;
|
||||
import static org.briarproject.bramble.util.ByteUtils.writeVarInt;
|
||||
import static org.briarproject.bramble.util.StringUtils.fromHexString;
|
||||
import static org.briarproject.bramble.util.StringUtils.toHexString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public class ByteUtilsTest extends BrambleTestCase {
|
||||
|
||||
@Test
|
||||
public void testReadUint16() {
|
||||
byte[] b = fromHexString("00000000");
|
||||
assertEquals(0, readUint16(b, 1));
|
||||
b = fromHexString("00000100");
|
||||
assertEquals(1, readUint16(b, 1));
|
||||
b = fromHexString("007FFF00");
|
||||
assertEquals(Short.MAX_VALUE, readUint16(b, 1));
|
||||
b = fromHexString("00FFFF00");
|
||||
assertEquals(65535, readUint16(b, 1));
|
||||
byte[] b = StringUtils.fromHexString("00000000");
|
||||
assertEquals(0, ByteUtils.readUint16(b, 1));
|
||||
b = StringUtils.fromHexString("00000100");
|
||||
assertEquals(1, ByteUtils.readUint16(b, 1));
|
||||
b = StringUtils.fromHexString("007FFF00");
|
||||
assertEquals(Short.MAX_VALUE, ByteUtils.readUint16(b, 1));
|
||||
b = StringUtils.fromHexString("00FFFF00");
|
||||
assertEquals(65535, ByteUtils.readUint16(b, 1));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadUint16ValidatesArguments1() {
|
||||
readUint16(new byte[1], 0);
|
||||
ByteUtils.readUint16(new byte[1], 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadUint16ValidatesArguments2() {
|
||||
readUint16(new byte[2], 1);
|
||||
ByteUtils.readUint16(new byte[2], 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadUint32() {
|
||||
byte[] b = fromHexString("000000000000");
|
||||
assertEquals(0, readUint32(b, 1));
|
||||
b = fromHexString("000000000100");
|
||||
assertEquals(1, readUint32(b, 1));
|
||||
b = fromHexString("007FFFFFFF00");
|
||||
assertEquals(Integer.MAX_VALUE, readUint32(b, 1));
|
||||
b = fromHexString("00FFFFFFFF00");
|
||||
assertEquals(4294967295L, readUint32(b, 1));
|
||||
byte[] b = StringUtils.fromHexString("000000000000");
|
||||
assertEquals(0, ByteUtils.readUint32(b, 1));
|
||||
b = StringUtils.fromHexString("000000000100");
|
||||
assertEquals(1, ByteUtils.readUint32(b, 1));
|
||||
b = StringUtils.fromHexString("007FFFFFFF00");
|
||||
assertEquals(Integer.MAX_VALUE, ByteUtils.readUint32(b, 1));
|
||||
b = StringUtils.fromHexString("00FFFFFFFF00");
|
||||
assertEquals(4294967295L, ByteUtils.readUint32(b, 1));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadUint32ValidatesArguments1() {
|
||||
readUint32(new byte[3], 0);
|
||||
ByteUtils.readUint32(new byte[3], 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadUint32ValidatesArguments2() {
|
||||
readUint32(new byte[4], 1);
|
||||
ByteUtils.readUint32(new byte[4], 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadUint64() {
|
||||
byte[] b = fromHexString("00000000000000000000");
|
||||
assertEquals(0L, readUint64(b, 1));
|
||||
b = fromHexString("00000000000000000100");
|
||||
assertEquals(1L, readUint64(b, 1));
|
||||
b = fromHexString("007FFFFFFFFFFFFFFF00");
|
||||
assertEquals(Long.MAX_VALUE, readUint64(b, 1));
|
||||
b = fromHexString("00800000000000000000");
|
||||
assertEquals(Long.MIN_VALUE, readUint64(b, 1));
|
||||
b = fromHexString("00FFFFFFFFFFFFFFFF00");
|
||||
assertEquals(-1L, readUint64(b, 1));
|
||||
byte[] b = StringUtils.fromHexString("00000000000000000000");
|
||||
assertEquals(0L, ByteUtils.readUint64(b, 1));
|
||||
b = StringUtils.fromHexString("00000000000000000100");
|
||||
assertEquals(1L, ByteUtils.readUint64(b, 1));
|
||||
b = StringUtils.fromHexString("007FFFFFFFFFFFFFFF00");
|
||||
assertEquals(Long.MAX_VALUE, ByteUtils.readUint64(b, 1));
|
||||
b = StringUtils.fromHexString("00800000000000000000");
|
||||
assertEquals(Long.MIN_VALUE, ByteUtils.readUint64(b, 1));
|
||||
b = StringUtils.fromHexString("00FFFFFFFFFFFFFFFF00");
|
||||
assertEquals(-1L, ByteUtils.readUint64(b, 1));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadUint64ValidatesArguments1() {
|
||||
readUint64(new byte[7], 0);
|
||||
ByteUtils.readUint64(new byte[7], 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadUint64ValidatesArguments2() {
|
||||
readUint64(new byte[8], 1);
|
||||
ByteUtils.readUint64(new byte[8], 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteUint16() {
|
||||
byte[] b = new byte[4];
|
||||
writeUint16(0, b, 1);
|
||||
assertEquals("00000000", toHexString(b));
|
||||
writeUint16(1, b, 1);
|
||||
assertEquals("00000100", toHexString(b));
|
||||
writeUint16(Short.MAX_VALUE, b, 1);
|
||||
assertEquals("007FFF00", toHexString(b));
|
||||
writeUint16(MAX_16_BIT_UNSIGNED, b, 1);
|
||||
assertEquals("00FFFF00", toHexString(b));
|
||||
ByteUtils.writeUint16(0, b, 1);
|
||||
assertEquals("00000000", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint16(1, b, 1);
|
||||
assertEquals("00000100", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint16(Short.MAX_VALUE, b, 1);
|
||||
assertEquals("007FFF00", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint16(MAX_16_BIT_UNSIGNED, b, 1);
|
||||
assertEquals("00FFFF00", StringUtils.toHexString(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteUint16ValidatesArguments() {
|
||||
try {
|
||||
writeUint16(0, new byte[1], 0);
|
||||
ByteUtils.writeUint16(0, new byte[1], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint16(0, new byte[2], 1);
|
||||
ByteUtils.writeUint16(0, new byte[2], 1);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint16(-1, new byte[2], 0);
|
||||
ByteUtils.writeUint16(-1, new byte[2], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint16(MAX_16_BIT_UNSIGNED + 1, new byte[2], 0);
|
||||
ByteUtils.writeUint16(MAX_16_BIT_UNSIGNED + 1, new byte[2], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
@@ -139,38 +122,38 @@ public class ByteUtilsTest extends BrambleTestCase {
|
||||
@Test
|
||||
public void testWriteUint32() {
|
||||
byte[] b = new byte[6];
|
||||
writeUint32(0, b, 1);
|
||||
assertEquals("000000000000", toHexString(b));
|
||||
writeUint32(1, b, 1);
|
||||
assertEquals("000000000100", toHexString(b));
|
||||
writeUint32(Integer.MAX_VALUE, b, 1);
|
||||
assertEquals("007FFFFFFF00", toHexString(b));
|
||||
writeUint32(MAX_32_BIT_UNSIGNED, b, 1);
|
||||
assertEquals("00FFFFFFFF00", toHexString(b));
|
||||
ByteUtils.writeUint32(0, b, 1);
|
||||
assertEquals("000000000000", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint32(1, b, 1);
|
||||
assertEquals("000000000100", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint32(Integer.MAX_VALUE, b, 1);
|
||||
assertEquals("007FFFFFFF00", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint32(MAX_32_BIT_UNSIGNED, b, 1);
|
||||
assertEquals("00FFFFFFFF00", StringUtils.toHexString(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteUint32ValidatesArguments() {
|
||||
try {
|
||||
writeUint32(0, new byte[3], 0);
|
||||
ByteUtils.writeUint32(0, new byte[3], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint32(0, new byte[4], 1);
|
||||
ByteUtils.writeUint32(0, new byte[4], 1);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint32(-1, new byte[4], 0);
|
||||
ByteUtils.writeUint32(-1, new byte[4], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint32(MAX_32_BIT_UNSIGNED + 1, new byte[4], 0);
|
||||
ByteUtils.writeUint32(MAX_32_BIT_UNSIGNED + 1, new byte[4], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
@@ -180,30 +163,30 @@ public class ByteUtilsTest extends BrambleTestCase {
|
||||
@Test
|
||||
public void testWriteUint64() {
|
||||
byte[] b = new byte[10];
|
||||
writeUint64(0, b, 1);
|
||||
assertEquals("00000000000000000000", toHexString(b));
|
||||
writeUint64(1, b, 1);
|
||||
assertEquals("00000000000000000100", toHexString(b));
|
||||
writeUint64(Long.MAX_VALUE, b, 1);
|
||||
assertEquals("007FFFFFFFFFFFFFFF00", toHexString(b));
|
||||
ByteUtils.writeUint64(0, b, 1);
|
||||
assertEquals("00000000000000000000", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint64(1, b, 1);
|
||||
assertEquals("00000000000000000100", StringUtils.toHexString(b));
|
||||
ByteUtils.writeUint64(Long.MAX_VALUE, b, 1);
|
||||
assertEquals("007FFFFFFFFFFFFFFF00", StringUtils.toHexString(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteUint64ValidatesArguments() {
|
||||
try {
|
||||
writeUint64(0, new byte[7], 0);
|
||||
ByteUtils.writeUint64(0, new byte[7], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint64(0, new byte[8], 1);
|
||||
ByteUtils.writeUint64(0, new byte[8], 1);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
}
|
||||
try {
|
||||
writeUint64(-1, new byte[8], 0);
|
||||
ByteUtils.writeUint64(-1, new byte[8], 0);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// Expected
|
||||
@@ -211,170 +194,17 @@ public class ByteUtilsTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetVarIntBytesToWrite() {
|
||||
assertEquals(1, getVarIntBytes(0));
|
||||
assertEquals(1, getVarIntBytes(0x7F)); // Max 7-bit int
|
||||
assertEquals(2, getVarIntBytes(0x7F + 1));
|
||||
assertEquals(2, getVarIntBytes(0x3FFF)); // Max 14-bit int
|
||||
assertEquals(3, getVarIntBytes(0x3FFF + 1));
|
||||
assertEquals(3, getVarIntBytes(0x1FFFFF)); // Max 21-bit int
|
||||
assertEquals(4, getVarIntBytes(0x1FFFFF + 1));
|
||||
assertEquals(4, getVarIntBytes(0xFFFFFFF)); // Max 28-bit int
|
||||
assertEquals(5, getVarIntBytes(0xFFFFFFF + 1));
|
||||
assertEquals(5, getVarIntBytes(0x7FFFFFFFFL)); // Max 35-bit int
|
||||
assertEquals(6, getVarIntBytes(0x7FFFFFFFFL + 1));
|
||||
assertEquals(6, getVarIntBytes(0x3FFFFFFFFFFL)); // Max 42-bit int
|
||||
assertEquals(7, getVarIntBytes(0x3FFFFFFFFFFL + 1));
|
||||
assertEquals(7, getVarIntBytes(0x1FFFFFFFFFFFFL)); // Max 49-bit int
|
||||
assertEquals(8, getVarIntBytes(0x1FFFFFFFFFFFFL + 1));
|
||||
assertEquals(8, getVarIntBytes(0xFFFFFFFFFFFFFFL)); // Max 56-bit int
|
||||
assertEquals(9, getVarIntBytes(0xFFFFFFFFFFFFFFL + 1));
|
||||
assertEquals(9, getVarIntBytes(0x7FFFFFFFFFFFFFFFL)); // Max 63-bit int
|
||||
assertEquals(MAX_VARINT_BYTES, getVarIntBytes(Long.MAX_VALUE)); // Same
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteVarInt() {
|
||||
testWriteVarInt(0, 1, "00");
|
||||
testWriteVarInt(1, 1, "01");
|
||||
testWriteVarInt(0x7F, 1, "7F"); // Max 7-bit int
|
||||
testWriteVarInt(0x7F + 1, 2, "8100");
|
||||
testWriteVarInt(0x3FFF, 2, "FF7F"); // Max 14-bit int
|
||||
testWriteVarInt(0x3FFF + 1, 3, "818000");
|
||||
testWriteVarInt(0x1FFFFF, 3, "FFFF7F"); // Max 21-bit int
|
||||
testWriteVarInt(0x1FFFFF + 1, 4, "81808000");
|
||||
testWriteVarInt(0xFFFFFFF, 4, "FFFFFF7F"); // Max 28-bit int
|
||||
testWriteVarInt(0xFFFFFFF + 1, 5, "8180808000");
|
||||
testWriteVarInt(0x7FFFFFFFFL, 5, "FFFFFFFF7F"); // Max 35-bit int
|
||||
testWriteVarInt(0x7FFFFFFFFL + 1, 6, "818080808000");
|
||||
testWriteVarInt(0x3FFFFFFFFFFL, 6, "FFFFFFFFFF7F"); // Max 42-bit int
|
||||
testWriteVarInt(0x3FFFFFFFFFFL + 1, 7, "81808080808000");
|
||||
testWriteVarInt(0x1FFFFFFFFFFFFL, 7, "FFFFFFFFFFFF7F"); // Max 49
|
||||
testWriteVarInt(0x1FFFFFFFFFFFFL + 1, 8, "8180808080808000");
|
||||
testWriteVarInt(0xFFFFFFFFFFFFFFL, 8, "FFFFFFFFFFFFFF7F"); // Max 56
|
||||
testWriteVarInt(0xFFFFFFFFFFFFFFL + 1, 9, "818080808080808000");
|
||||
testWriteVarInt(0x7FFFFFFFFFFFFFFFL, 9, "FFFFFFFFFFFFFFFF7F"); // Max 63
|
||||
testWriteVarInt(Long.MAX_VALUE, MAX_VARINT_BYTES, "FFFFFFFFFFFFFFFF7F");
|
||||
}
|
||||
|
||||
private void testWriteVarInt(long src, int len, String destHex) {
|
||||
byte[] dest = new byte[9];
|
||||
assertEquals(len, writeVarInt(src, dest, 0));
|
||||
assertEquals(destHex, toHexString(dest).substring(0, len * 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetVarIntBytesToRead() throws FormatException {
|
||||
testGetVarIntBytesToRead(1, "00", 0);
|
||||
testGetVarIntBytesToRead(1, "01", 0);
|
||||
testGetVarIntBytesToRead(1, "7F", 0); // Max 7-bit int
|
||||
testGetVarIntBytesToRead(2, "8100", 0);
|
||||
testGetVarIntBytesToRead(2, "FF7F", 0); // Max 14-bit int
|
||||
testGetVarIntBytesToRead(3, "818000", 0);
|
||||
testGetVarIntBytesToRead(3, "FFFF7F", 0); // Max 21-bit int
|
||||
testGetVarIntBytesToRead(4, "81808000", 0);
|
||||
testGetVarIntBytesToRead(4, "FFFFFF7F", 0); // Max 28-bit int
|
||||
testGetVarIntBytesToRead(5, "8180808000", 0);
|
||||
testGetVarIntBytesToRead(5, "FFFFFFFF7F", 0); // Max 35-bit int
|
||||
testGetVarIntBytesToRead(6, "818080808000", 0);
|
||||
testGetVarIntBytesToRead(6, "FFFFFFFFFF7F", 0); // Max 42-bit int
|
||||
testGetVarIntBytesToRead(7, "81808080808000", 0);
|
||||
testGetVarIntBytesToRead(7, "FFFFFFFFFFFF7F", 0); // Max 49-bit int
|
||||
testGetVarIntBytesToRead(8, "8180808080808000", 0);
|
||||
testGetVarIntBytesToRead(8, "FFFFFFFFFFFFFF7F", 0); // Max 56-bit int
|
||||
testGetVarIntBytesToRead(9, "818080808080808000", 0);
|
||||
testGetVarIntBytesToRead(9, "FFFFFFFFFFFFFFFF7F", 0); // Max 63-bit int
|
||||
// Start at offset, ignore trailing data
|
||||
testGetVarIntBytesToRead(1, "FF0000", 1);
|
||||
testGetVarIntBytesToRead(9, "00FFFFFFFFFFFFFFFF7F00", 1);
|
||||
}
|
||||
|
||||
private void testGetVarIntBytesToRead(int len, String srcHex, int offset)
|
||||
throws FormatException {
|
||||
assertEquals(len, getVarIntBytes(fromHexString(srcHex), offset));
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testGetVarIntBytesToReadThrowsExceptionAtEndOfInput()
|
||||
throws FormatException {
|
||||
byte[] src = new byte[MAX_VARINT_BYTES - 1];
|
||||
fill(src, (byte) 0xFF);
|
||||
// Reaches end of input without finding lowered continuation flag
|
||||
getVarIntBytes(src, 0);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testGetVarIntBytesToReadThrowsExceptionAfterNineBytes()
|
||||
throws FormatException {
|
||||
byte[] src = new byte[MAX_VARINT_BYTES];
|
||||
fill(src, (byte) 0xFF);
|
||||
// Reaches max length without finding lowered continuation flag
|
||||
getVarIntBytes(src, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadVarInt() throws FormatException {
|
||||
testReadVarInt(0, "00", 0);
|
||||
testReadVarInt(1, "01", 0);
|
||||
testReadVarInt(0x7F, "7F", 0); // Max 7-bit int
|
||||
testReadVarInt(0x7F + 1, "8100", 0);
|
||||
testReadVarInt(0x3FFF, "FF7F", 0); // Max 14-bit int
|
||||
testReadVarInt(0x3FFF + 1, "818000", 0);
|
||||
testReadVarInt(0x1FFFFF, "FFFF7F", 0); // Max 21-bit int
|
||||
testReadVarInt(0x1FFFFF + 1, "81808000", 0);
|
||||
testReadVarInt(0xFFFFFFF, "FFFFFF7F", 0); // Max 28-bit int
|
||||
testReadVarInt(0xFFFFFFF + 1, "8180808000", 0);
|
||||
testReadVarInt(0x7FFFFFFFFL, "FFFFFFFF7F", 0); // Max 35-bit int
|
||||
testReadVarInt(0x7FFFFFFFFL + 1, "818080808000", 0);
|
||||
testReadVarInt(0x3FFFFFFFFFFL, "FFFFFFFFFF7F", 0); // Max 42-bit int
|
||||
testReadVarInt(0x3FFFFFFFFFFL + 1, "81808080808000", 0);
|
||||
testReadVarInt(0x1FFFFFFFFFFFFL, "FFFFFFFFFFFF7F", 0); // Max 49-bit int
|
||||
testReadVarInt(0x1FFFFFFFFFFFFL + 1, "8180808080808000", 0);
|
||||
testReadVarInt(0xFFFFFFFFFFFFFFL, "FFFFFFFFFFFFFF7F", 0); // Max 56
|
||||
testReadVarInt(0xFFFFFFFFFFFFFFL + 1, "818080808080808000", 0);
|
||||
testReadVarInt(0x7FFFFFFFFFFFFFFFL, "FFFFFFFFFFFFFFFF7F", 0); // Max 63
|
||||
testReadVarInt(Long.MAX_VALUE, "FFFFFFFFFFFFFFFF7F", 0);
|
||||
// Start at offset, ignore trailing data
|
||||
testReadVarInt(0, "FF0000", 1);
|
||||
testReadVarInt(Long.MAX_VALUE, "00FFFFFFFFFFFFFFFF7F00", 1);
|
||||
}
|
||||
|
||||
private void testReadVarInt(long dest, String srcHex, int offset)
|
||||
throws FormatException {
|
||||
assertEquals(dest, readVarInt(fromHexString(srcHex), offset));
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testReadVarIntThrowsExceptionAtEndOfInput()
|
||||
throws FormatException {
|
||||
byte[] src = new byte[MAX_VARINT_BYTES - 1];
|
||||
fill(src, (byte) 0xFF);
|
||||
// Reaches end of input without finding lowered continuation flag
|
||||
readVarInt(src, 0);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testReadVarIntThrowsExceptionAfterNineBytes()
|
||||
throws FormatException {
|
||||
byte[] src = new byte[MAX_VARINT_BYTES];
|
||||
fill(src, (byte) 0xFF);
|
||||
// Reaches max length without finding lowered continuation flag
|
||||
readVarInt(src, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteAndReadVarInt() throws FormatException {
|
||||
Random random = new Random();
|
||||
int padding = 10;
|
||||
byte[] buf = new byte[MAX_VARINT_BYTES + padding];
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
long src = random.nextLong() & 0x7FFFFFFFFFFFFFFFL; // Non-negative
|
||||
int offset = random.nextInt(padding);
|
||||
int len = getVarIntBytes(src);
|
||||
assertEquals(len, writeVarInt(src, buf, offset));
|
||||
assertEquals(len, getVarIntBytes(buf, offset));
|
||||
assertEquals(src, readVarInt(buf, offset));
|
||||
fill(buf, (byte) 0);
|
||||
public void testReadUint() {
|
||||
byte[] b = new byte[1];
|
||||
b[0] = (byte) 128;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
assertEquals(1 << i, ByteUtils.readUint(b, i + 1));
|
||||
}
|
||||
b = new byte[2];
|
||||
for (int i = 0; i < 65535; i++) {
|
||||
ByteUtils.writeUint16(i, b, 0);
|
||||
assertEquals(i, ByteUtils.readUint(b, 16));
|
||||
assertEquals(i >> 1, ByteUtils.readUint(b, 15));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfEntry;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.sync.ClientId;
|
||||
@@ -43,6 +44,7 @@ import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
||||
@@ -657,4 +659,327 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
||||
c.registerClient(clientId, 123, 234, hook);
|
||||
assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsInvisibleIfContactGroupDoesNotExist()
|
||||
throws Exception {
|
||||
expectGetContactGroup(false);
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsInvisibleIfNoRemoteUpdateExists() throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test(expected = DbException.class)
|
||||
public void testThrowsExceptionIfNoLocalUpdateExists() throws Exception {
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(singletonMap(remoteUpdateId, remoteUpdateMeta)));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
c.getClientVisibility(txn, contact.getId(), clientId, 123);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsInvisibleIfClientNotSupportedLocally()
|
||||
throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is supported remotely but not locally
|
||||
BdfList localUpdateBody = BdfList.of(new BdfList(), 1L);
|
||||
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
|
||||
will(returnValue(localUpdateBody));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsInvisibleIfClientNotSupportedRemotely()
|
||||
throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is supported locally but not remotely
|
||||
BdfList localUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
|
||||
BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
|
||||
will(returnValue(localUpdateBody));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsVisibleIfClientNotActiveRemotely() throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is supported locally and remotely but not active
|
||||
BdfList localUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
|
||||
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
|
||||
will(returnValue(localUpdateBody));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(VISIBLE, c.getClientVisibility(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsSharedIfClientActiveRemotely() throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is supported locally and remotely and active
|
||||
BdfList localUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
|
||||
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
|
||||
will(returnValue(localUpdateBody));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(SHARED, c.getClientVisibility(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsNegativeIfContactGroupDoesNotExist()
|
||||
throws Exception {
|
||||
expectGetContactGroup(false);
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsNegativeIfNoRemoteUpdateExists() throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsNegativeIfClientNotSupportedRemotely()
|
||||
throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is not supported remotely
|
||||
BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsMinorVersionIfClientNotActiveRemotely()
|
||||
throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is supported remotely but not active
|
||||
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, false)), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(234, c.getClientMinorVersion(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsMinorVersionIfClientActiveRemotely()
|
||||
throws Exception {
|
||||
MessageId localUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary localUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||
MessageId remoteUpdateId = new MessageId(getRandomId());
|
||||
BdfDictionary remoteUpdateMeta = BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L),
|
||||
new BdfEntry(MSG_KEY_LOCAL, false));
|
||||
Map<MessageId, BdfDictionary> messageMetadata = new HashMap<>();
|
||||
messageMetadata.put(localUpdateId, localUpdateMeta);
|
||||
messageMetadata.put(remoteUpdateId, remoteUpdateMeta);
|
||||
// The client is supported remotely and active
|
||||
BdfList remoteUpdateBody = BdfList.of(BdfList.of(
|
||||
BdfList.of(clientId.getString(), 123, 234, true)), 1L);
|
||||
|
||||
expectGetContactGroup(true);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
|
||||
contactGroup.getId());
|
||||
will(returnValue(messageMetadata));
|
||||
oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
|
||||
will(returnValue(remoteUpdateBody));
|
||||
}});
|
||||
|
||||
ClientVersioningManagerImpl c = createInstance();
|
||||
assertEquals(234, c.getClientMinorVersion(txn, contact.getId(),
|
||||
clientId, 123));
|
||||
}
|
||||
|
||||
private void expectGetContactGroup(boolean exists) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).getContact(txn, contact.getId());
|
||||
will(returnValue(contact));
|
||||
oneOf(contactGroupFactory).createContactGroup(CLIENT_ID,
|
||||
MAJOR_VERSION, contact);
|
||||
will(returnValue(contactGroup));
|
||||
oneOf(db).containsGroup(txn, contactGroup.getId());
|
||||
will(returnValue(exists));
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb',
|
||||
'org.briarproject:jtorctl:0.3:jtorctl-0.3.jar:f2939238a097898998432effe93b0334d97a787972ab3a91a8973a1d309fc864',
|
||||
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
|
||||
'org.codehaus.mojo.signature:java16:1.1:java16-1.1.signature:53799223a2c98dba2d0add810bed76315460df285c69e4f397ae6098f87dd619',
|
||||
'org.codehaus.mojo:animal-sniffer-annotations:1.14:animal-sniffer-annotations-1.14.jar:2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d',
|
||||
|
||||
@@ -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.4.8@zip'
|
||||
tor 'org.briarproject:tor:0.3.5.8@zip'
|
||||
tor 'org.briarproject:obfs4proxy:0.0.7@zip'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
|
||||
|
||||
@@ -44,7 +44,7 @@ public class BridgeTest extends BrambleTestCase {
|
||||
public static Iterable<String> data() {
|
||||
BrambleJavaIntegrationTestComponent component =
|
||||
DaggerBrambleJavaIntegrationTestComponent.builder().build();
|
||||
return component.getCircumventionProvider().getBridges();
|
||||
return component.getCircumventionProvider().getBridges(false);
|
||||
}
|
||||
|
||||
private final static long TIMEOUT = SECONDS.toMillis(30);
|
||||
@@ -104,7 +104,12 @@ public class BridgeTest extends BrambleTestCase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getBridges() {
|
||||
public boolean needsMeek(String countryCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getBridges(boolean useMeek) {
|
||||
return singletonList(bridge);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,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.4.8:tor-0.3.4.8.zip:bc0158c34002f471a4fe14a6a481816c918eb520a220bb027f64be902beb757f',
|
||||
'org.briarproject:tor:0.3.5.8:tor-0.3.5.8.zip:96e83391f01984f28669235fc02fbb0243140a2b3b2c73aeffd0042c8d3ced18',
|
||||
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
|
||||
'org.codehaus.mojo:animal-sniffer-annotations:1.14:animal-sniffer-annotations-1.14.jar:2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d',
|
||||
'org.hamcrest:hamcrest-core:1.3:hamcrest-core-1.3.jar:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9',
|
||||
|
||||
@@ -22,8 +22,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 26
|
||||
versionCode 10105
|
||||
versionName "1.1.5"
|
||||
versionCode 10106
|
||||
versionName "1.1.6"
|
||||
applicationId "org.briarproject.briar.android"
|
||||
buildConfigField "String", "GitHash",
|
||||
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
|
||||
@@ -116,7 +116,7 @@ dependencies {
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.12.4'
|
||||
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0'
|
||||
implementation 'com.vanniktech:emoji-google:0.5.1'
|
||||
def glideVersion = '4.8.0'
|
||||
implementation("com.github.bumptech.glide:glide:$glideVersion") {
|
||||
@@ -134,7 +134,7 @@ dependencies {
|
||||
testImplementation project(path: ':bramble-core', configuration: 'testOutput')
|
||||
testImplementation 'org.robolectric:robolectric:4.0.1'
|
||||
testImplementation 'org.robolectric:shadows-support-v4:3.3.2'
|
||||
testImplementation 'org.mockito:mockito-core:2.13.0'
|
||||
testImplementation 'org.mockito:mockito-core:2.19.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation "org.jmock:jmock:2.8.2"
|
||||
testImplementation "org.jmock:jmock-junit4:2.8.2"
|
||||
|
||||
BIN
briar-android/src/androidTestOfficial/assets/animated.gif
Normal file
BIN
briar-android/src/androidTestOfficial/assets/animated.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 B |
BIN
briar-android/src/androidTestOfficial/assets/animated2.gif
Normal file
BIN
briar-android/src/androidTestOfficial/assets/animated2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 317 B |
BIN
briar-android/src/androidTestOfficial/assets/error_high.jpg
Normal file
BIN
briar-android/src/androidTestOfficial/assets/error_high.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
briar-android/src/androidTestOfficial/assets/error_large.gif
Normal file
BIN
briar-android/src/androidTestOfficial/assets/error_large.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 B |
BIN
briar-android/src/androidTestOfficial/assets/error_wide.jpg
Normal file
BIN
briar-android/src/androidTestOfficial/assets/error_wide.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,279 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AttachmentControllerIntegrationTest {
|
||||
|
||||
private static final String smallKitten =
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg/160px-Kitten_in_Rizal_Park%2C_Manila.jpg";
|
||||
private static final String originalKitten =
|
||||
"https://upload.wikimedia.org/wikipedia/commons/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg";
|
||||
private static final String pngKitten =
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c8/Young_cat.png";
|
||||
private static final String uberGif =
|
||||
"https://raw.githubusercontent.com/fuzzdb-project/fuzzdb/master/attack/file-upload/malicious-images/uber.gif";
|
||||
private static final String lottaPixel =
|
||||
"https://raw.githubusercontent.com/fuzzdb-project/fuzzdb/master/attack/file-upload/malicious-images/lottapixel.jpg";
|
||||
private static final String imageIoCrash =
|
||||
"https://www.landaire.net/img/crasher.png";
|
||||
private static final String gimpCrash =
|
||||
"https://gitlab.gnome.org/GNOME/gimp/uploads/75f5b7ed3b09b3f1c13f1f65bffe784f/31153c919d3aa634e8e6cff82219fe7352dd8a37.png";
|
||||
private static final String optiPngAfl =
|
||||
"https://sourceforge.net/p/optipng/bugs/64/attachment/test.gif";
|
||||
private static final String librawError =
|
||||
"https://www.libraw.org/sites/libraw.org/files/P1010671.JPG";
|
||||
|
||||
private final AttachmentDimensions dimensions = new AttachmentDimensions(
|
||||
100, 50, 200, 75, 300
|
||||
);
|
||||
private final MessageId msgId = new MessageId(getRandomId());
|
||||
|
||||
private final AttachmentController controller =
|
||||
new AttachmentController(null, dimensions);
|
||||
|
||||
@Test
|
||||
public void testSmallJpegImage() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(smallKitten);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(msgId, item.getMessageId());
|
||||
assertEquals(160, item.getWidth());
|
||||
assertEquals(240, item.getHeight());
|
||||
assertEquals(160, item.getThumbnailWidth());
|
||||
assertEquals(240, item.getThumbnailHeight());
|
||||
assertEquals("image/jpeg", item.getMimeType());
|
||||
assertEquals("jpg", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBigJpegImage() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(originalKitten);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(msgId, item.getMessageId());
|
||||
assertEquals(1728, item.getWidth());
|
||||
assertEquals(2592, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/jpeg", item.getMimeType());
|
||||
assertEquals("jpg", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmallPngImage() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
|
||||
InputStream is = getUrlInputStream(pngKitten);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(msgId, item.getMessageId());
|
||||
assertEquals(737, item.getWidth());
|
||||
assertEquals(510, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(138, item.getThumbnailHeight());
|
||||
assertEquals("image/png", item.getMimeType());
|
||||
assertEquals("png", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUberGif() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(uberGif);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(1, item.getWidth());
|
||||
assertEquals(1, item.getHeight());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/gif", item.getMimeType());
|
||||
assertEquals("gif", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLottaPixels() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(lottaPixel);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(64250, item.getWidth());
|
||||
assertEquals(64250, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
|
||||
assertEquals("image/jpeg", item.getMimeType());
|
||||
assertEquals("jpg", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImageIoCrash() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(imageIoCrash);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(1184, item.getWidth());
|
||||
assertEquals(448, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/png", item.getMimeType());
|
||||
assertEquals("png", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGimpCrash() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(gimpCrash);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(1, item.getWidth());
|
||||
assertEquals(1, item.getHeight());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/gif", item.getMimeType());
|
||||
assertEquals("gif", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptiPngAfl() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(optiPngAfl);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(32, item.getWidth());
|
||||
assertEquals(32, item.getHeight());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/gif", item.getMimeType());
|
||||
assertEquals("gif", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLibrawError() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getUrlInputStream(librawError);
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertTrue(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmallAnimatedGifMaxDimensions() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
|
||||
InputStream is = getAssetInputStream("animated.gif");
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(65535, item.getWidth());
|
||||
assertEquals(65535, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
|
||||
assertEquals("image/gif", item.getMimeType());
|
||||
assertEquals("gif", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmallAnimatedGifHugeDimensions() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
|
||||
InputStream is = getAssetInputStream("animated2.gif");
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(10000, item.getWidth());
|
||||
assertEquals(10000, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
|
||||
assertEquals("image/gif", item.getMimeType());
|
||||
assertEquals("gif", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmallGifLargeDimensions() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
|
||||
InputStream is = getAssetInputStream("error_large.gif");
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(16384, item.getWidth());
|
||||
assertEquals(16384, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
|
||||
assertEquals("image/gif", item.getMimeType());
|
||||
assertEquals("gif", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHighError() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getAssetInputStream("error_high.jpg");
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(1, item.getWidth());
|
||||
assertEquals(10000, item.getHeight());
|
||||
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/jpeg", item.getMimeType());
|
||||
assertEquals("jpg", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWideError() throws Exception {
|
||||
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
|
||||
InputStream is = getAssetInputStream("error_wide.jpg");
|
||||
Attachment a = new Attachment(is);
|
||||
AttachmentItem item = controller.getAttachmentItem(h, a, true);
|
||||
assertEquals(1920, item.getWidth());
|
||||
assertEquals(1, item.getHeight());
|
||||
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
|
||||
assertEquals(dimensions.minHeight, item.getThumbnailHeight());
|
||||
assertEquals("image/jpeg", item.getMimeType());
|
||||
assertEquals("jpg", item.getExtension());
|
||||
assertFalse(item.hasError());
|
||||
}
|
||||
|
||||
private InputStream getUrlInputStream(String url) throws IOException {
|
||||
return new URL(url).openStream();
|
||||
}
|
||||
|
||||
private InputStream getAssetInputStream(String name) throws IOException {
|
||||
AssetManager assets = InstrumentationRegistry.getContext().getAssets();
|
||||
return assets.open(name);
|
||||
}
|
||||
|
||||
public static byte[] getRandomBytes(int length) {
|
||||
byte[] b = new byte[length];
|
||||
new Random().nextBytes(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
public static byte[] getRandomId() {
|
||||
return getRandomBytes(UniqueId.LENGTH);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.test.rule.ActivityTestRule;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static android.support.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
|
||||
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ConversationActivityNotSignedInTest {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<ConversationActivity> testRule =
|
||||
new ActivityTestRule<>(ConversationActivity.class, false, false);
|
||||
|
||||
@Test
|
||||
public void openWithoutSignedIn() {
|
||||
Context targetContext = getInstrumentation().getTargetContext();
|
||||
Intent intent = new Intent(targetContext, ConversationActivity.class);
|
||||
intent.putExtra(CONTACT_ID, 1);
|
||||
testRule.launchActivity(intent);
|
||||
|
||||
onView(withText(R.string.sign_in_button))
|
||||
.perform(waitUntilMatches(isDisplayed()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="org.briarproject.briar"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
|
||||
@@ -17,9 +18,11 @@
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission-sdk-23 android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:name="org.briarproject.briar.android.BriarApplicationImpl"
|
||||
@@ -28,7 +31,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:logo="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BriarTheme">
|
||||
android:theme="@style/BriarTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<receiver
|
||||
android:name="org.briarproject.briar.android.login.SignInReminderReceiver"
|
||||
@@ -116,7 +120,7 @@
|
||||
<activity
|
||||
android:name=".android.conversation.ImageActivity"
|
||||
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
|
||||
android:theme="@style/BriarTheme.Transparent.NoActionBar">
|
||||
android:theme="@style/BriarTheme.ActionBarOverlay">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
|
||||
@@ -154,7 +158,7 @@
|
||||
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity"
|
||||
android:label="@string/groups_member_list"
|
||||
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
|
||||
@@ -397,7 +401,7 @@
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.panic.PanicResponderActivity"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar">
|
||||
android:theme="@style/TranslucentTheme">
|
||||
<!-- this can never have launchMode singleTask or singleInstance! -->
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
|
||||
@@ -407,12 +411,12 @@
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.logout.ExitActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar">
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.logout.HideUiActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar">
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.briarproject.briar.BriarCoreModule;
|
||||
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.reporting.BriarReportSender;
|
||||
import org.briarproject.briar.android.view.TextInputView;
|
||||
import org.briarproject.briar.android.view.EmojiTextInputView;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||
import org.briarproject.briar.api.android.LockManager;
|
||||
@@ -169,7 +169,7 @@ public interface AndroidComponent
|
||||
|
||||
void inject(NotificationCleanupService notificationCleanupService);
|
||||
|
||||
void inject(TextInputView textInputView);
|
||||
void inject(EmojiTextInputView textInputView);
|
||||
|
||||
void inject(BriarModelLoader briarModelLoader);
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
showForumPostNotification(f.getGroupId());
|
||||
} else if (e instanceof BlogPostAddedEvent) {
|
||||
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
|
||||
showBlogPostNotification(b.getGroupId());
|
||||
if (!b.isLocal()) showBlogPostNotification(b.getGroupId());
|
||||
} else if (e instanceof IntroductionSucceededEvent) {
|
||||
showIntroductionNotification();
|
||||
}
|
||||
@@ -261,8 +261,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
b.setContentText(appContext.getText(text));
|
||||
b.setWhen(0); // Don't show the time
|
||||
b.setOngoing(true);
|
||||
Intent i = new Intent(appContext, NavDrawerActivity.class);
|
||||
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
|
||||
Intent i = new Intent(appContext, SplashScreenActivity.class);
|
||||
b.setContentIntent(PendingIntent.getActivity(appContext, 0, i, 0));
|
||||
if (SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_SERVICE);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.briarproject.briar.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application.ActivityLifecycleCallbacks;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
class BackgroundMonitor implements ActivityLifecycleCallbacks {
|
||||
|
||||
private final AtomicInteger foregroundActivities = new AtomicInteger(0);
|
||||
|
||||
boolean isRunningInBackground() {
|
||||
return foregroundActivities.get() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity a, @Nullable Bundle state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity a) {
|
||||
foregroundActivities.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity a) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity a) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity a) {
|
||||
foregroundActivities.decrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity a,
|
||||
@Nullable Bundle outState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity a) {
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,6 @@ public interface BriarApplication {
|
||||
AndroidComponent getApplicationComponent();
|
||||
|
||||
SharedPreferences getDefaultSharedPreferences();
|
||||
|
||||
boolean isRunningInBackground();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.briarproject.briar.android;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningAppProcessInfo;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -30,6 +32,8 @@ import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static java.util.logging.Level.FINE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static org.acra.ReportField.ANDROID_VERSION;
|
||||
@@ -79,6 +83,7 @@ public class BriarApplicationImpl extends Application
|
||||
Logger.getLogger(BriarApplicationImpl.class.getName());
|
||||
|
||||
private final CachingLogHandler logHandler = new CachingLogHandler();
|
||||
private final BackgroundMonitor backgroundMonitor = new BackgroundMonitor();
|
||||
|
||||
private AndroidComponent applicationComponent;
|
||||
private volatile SharedPreferences prefs;
|
||||
@@ -115,6 +120,9 @@ public class BriarApplicationImpl extends Application
|
||||
|
||||
applicationComponent = createApplicationComponent();
|
||||
EmojiManager.install(new GoogleEmojiProvider());
|
||||
|
||||
if (SDK_INT < 16)
|
||||
registerActivityLifecycleCallbacks(backgroundMonitor);
|
||||
}
|
||||
|
||||
protected AndroidComponent createApplicationComponent() {
|
||||
@@ -173,4 +181,15 @@ public class BriarApplicationImpl extends Application
|
||||
public SharedPreferences getDefaultSharedPreferences() {
|
||||
return prefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunningInBackground() {
|
||||
if (SDK_INT >= 16) {
|
||||
RunningAppProcessInfo info = new RunningAppProcessInfo();
|
||||
ActivityManager.getMyMemoryState(info);
|
||||
return (info.importance != IMPORTANCE_FOREGROUND);
|
||||
} else {
|
||||
return backgroundMonitor.isRunningInBackground();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.briarproject.briar.android;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningAppProcessInfo;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
@@ -35,7 +33,6 @@ import java.util.logging.Logger;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
|
||||
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
import static android.app.NotificationManager.IMPORTANCE_NONE;
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
@@ -74,6 +71,7 @@ public class BriarService extends Service {
|
||||
|
||||
@Nullable
|
||||
private BroadcastReceiver receiver = null;
|
||||
private BriarApplication app;
|
||||
|
||||
@Inject
|
||||
AndroidNotificationManager notificationManager;
|
||||
@@ -93,8 +91,8 @@ public class BriarService extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
BriarApplication application = (BriarApplication) getApplication();
|
||||
application.getApplicationComponent().inject(this);
|
||||
app = (BriarApplication) getApplication();
|
||||
app.getApplicationComponent().inject(this);
|
||||
|
||||
LOG.info("Created");
|
||||
if (created.getAndSet(true)) {
|
||||
@@ -220,8 +218,8 @@ public class BriarService extends Service {
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
LOG.warning("Memory is low");
|
||||
// Clear the UI - this is done in onTrimMemory() if SDK_INT >= 16
|
||||
if (SDK_INT < 16) hideUi();
|
||||
// If we're not in the foreground, clear the UI to save memory
|
||||
if (app.isRunningInBackground()) hideUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -235,20 +233,16 @@ public class BriarService extends Service {
|
||||
LOG.info("Trim memory: near middle of LRU list");
|
||||
} else if (level == TRIM_MEMORY_COMPLETE) {
|
||||
LOG.info("Trim memory: near end of LRU list");
|
||||
} else if (SDK_INT >= 16) {
|
||||
if (level == TRIM_MEMORY_RUNNING_MODERATE) {
|
||||
LOG.info("Trim memory: running moderately low");
|
||||
} else if (level == TRIM_MEMORY_RUNNING_LOW) {
|
||||
LOG.info("Trim memory: running low");
|
||||
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
|
||||
LOG.info("Trim memory: running critically low");
|
||||
// If we're not in the foreground, clear the UI to save memory
|
||||
RunningAppProcessInfo info = new RunningAppProcessInfo();
|
||||
ActivityManager.getMyMemoryState(info);
|
||||
if (info.importance != IMPORTANCE_FOREGROUND) hideUi();
|
||||
} else if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Trim memory: unknown level " + level);
|
||||
}
|
||||
} else if (level == TRIM_MEMORY_RUNNING_MODERATE) {
|
||||
LOG.info("Trim memory: running moderately low");
|
||||
} else if (level == TRIM_MEMORY_RUNNING_LOW) {
|
||||
LOG.info("Trim memory: running low");
|
||||
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
|
||||
// This level may be received if SDK_INT < 16, although the
|
||||
// constant isn't declared until API level 16
|
||||
LOG.warning("Trim memory: running critically low");
|
||||
// If we're not in the foreground, clear the UI to save memory
|
||||
if (app.isRunningInBackground()) hideUi();
|
||||
} else if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Trim memory: unknown level " + level);
|
||||
}
|
||||
|
||||
@@ -3,22 +3,29 @@ package org.briarproject.briar.android;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.fragment.ErrorFragment;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
|
||||
import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID;
|
||||
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class StartupFailureActivity extends BaseActivity implements
|
||||
BaseFragmentListener {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
setContentView(R.layout.activity_fragment_container);
|
||||
@@ -38,7 +45,7 @@ public class StartupFailureActivity extends BaseActivity implements
|
||||
// cancel notification
|
||||
if (notificationId > -1) {
|
||||
Object o = getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
NotificationManager nm = (NotificationManager) requireNonNull(o);
|
||||
nm.cancel(notificationId);
|
||||
}
|
||||
|
||||
@@ -66,7 +73,7 @@ public class StartupFailureActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runOnDbThread(Runnable runnable) {
|
||||
public void runOnDbThread(@NonNull Runnable runnable) {
|
||||
throw new AssertionError("Deprecated and should not be used");
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,10 @@ public interface TestingConstants {
|
||||
long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
|
||||
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
|
||||
Long.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Feature flag for enabling image attachments.
|
||||
*/
|
||||
boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = IS_DEBUG_BUILD;
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.briarproject.briar.android.contact.ContactModule;
|
||||
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||
import org.briarproject.briar.android.conversation.ImageActivity;
|
||||
import org.briarproject.briar.android.conversation.ImageFragment;
|
||||
import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||
@@ -30,7 +31,6 @@ import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
||||
import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
|
||||
import org.briarproject.briar.android.keyagreement.ContactExchangeErrorFragment;
|
||||
import org.briarproject.briar.android.keyagreement.IntroFragment;
|
||||
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
|
||||
import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
|
||||
import org.briarproject.briar.android.login.AuthorNameFragment;
|
||||
@@ -48,7 +48,6 @@ import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupMessageFragment;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupModule;
|
||||
import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity;
|
||||
import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment;
|
||||
@@ -69,10 +68,8 @@ import org.briarproject.briar.android.sharing.ForumInvitationActivity;
|
||||
import org.briarproject.briar.android.sharing.ForumSharingStatusActivity;
|
||||
import org.briarproject.briar.android.sharing.ShareBlogActivity;
|
||||
import org.briarproject.briar.android.sharing.ShareBlogFragment;
|
||||
import org.briarproject.briar.android.sharing.ShareBlogMessageFragment;
|
||||
import org.briarproject.briar.android.sharing.ShareForumActivity;
|
||||
import org.briarproject.briar.android.sharing.ShareForumFragment;
|
||||
import org.briarproject.briar.android.sharing.ShareForumMessageFragment;
|
||||
import org.briarproject.briar.android.sharing.SharingModule;
|
||||
import org.briarproject.briar.android.splash.SplashScreenActivity;
|
||||
import org.briarproject.briar.android.test.TestDataActivity;
|
||||
@@ -182,8 +179,6 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(CreateGroupFragment fragment);
|
||||
|
||||
void inject(CreateGroupMessageFragment fragment);
|
||||
|
||||
void inject(GroupListFragment fragment);
|
||||
|
||||
void inject(GroupInviteFragment fragment);
|
||||
@@ -194,20 +189,14 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(FeedFragment fragment);
|
||||
|
||||
void inject(IntroFragment fragment);
|
||||
|
||||
void inject(KeyAgreementFragment fragment);
|
||||
|
||||
void inject(ContactChooserFragment fragment);
|
||||
|
||||
void inject(ShareForumFragment fragment);
|
||||
|
||||
void inject(ShareForumMessageFragment fragment);
|
||||
|
||||
void inject(ShareBlogFragment fragment);
|
||||
|
||||
void inject(ShareBlogMessageFragment fragment);
|
||||
|
||||
void inject(IntroductionMessageFragment fragment);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
@@ -218,4 +207,6 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(AliasDialogFragment aliasDialogFragment);
|
||||
|
||||
void inject(ImageFragment imageFragment);
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.AndroidComponent;
|
||||
import org.briarproject.briar.android.BriarApplication;
|
||||
@@ -51,6 +53,8 @@ import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOT
|
||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||
* E.g. {@link DevReportActivity}
|
||||
*/
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class BaseActivity extends AppCompatActivity
|
||||
implements DestroyableContext, OnTapFilteredListener {
|
||||
|
||||
@@ -77,6 +81,17 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
// create the ActivityComponent *before* calling super.onCreate()
|
||||
// because it already attaches fragments which need access
|
||||
// to the component for their own injection
|
||||
AndroidComponent applicationComponent =
|
||||
((BriarApplication) getApplication()).getApplicationComponent();
|
||||
activityComponent = DaggerActivityComponent.builder()
|
||||
.androidComponent(applicationComponent)
|
||||
.activityModule(getActivityModule())
|
||||
.forumModule(getForumModule())
|
||||
.build();
|
||||
injectActivity(activityComponent);
|
||||
super.onCreate(state);
|
||||
|
||||
// WARNING: When removing this or making it possible to turn it off,
|
||||
@@ -86,17 +101,6 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
// unlock screen is shown.
|
||||
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
|
||||
|
||||
AndroidComponent applicationComponent =
|
||||
((BriarApplication) getApplication()).getApplicationComponent();
|
||||
|
||||
activityComponent = DaggerActivityComponent.builder()
|
||||
.androidComponent(applicationComponent)
|
||||
.activityModule(getActivityModule())
|
||||
.forumModule(getForumModule())
|
||||
.build();
|
||||
|
||||
injectActivity(activityComponent);
|
||||
|
||||
for (ActivityLifecycleController alc : lifecycleControllers) {
|
||||
alc.onActivityCreate(this);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.android.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v7.app.ActionBar;
|
||||
@@ -10,6 +9,8 @@ import android.transition.Transition;
|
||||
import android.view.Window;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.controller.BriarController;
|
||||
import org.briarproject.briar.android.controller.DbController;
|
||||
@@ -36,7 +37,8 @@ import static org.briarproject.briar.android.util.UiUtils.excludeSystemUi;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
|
||||
|
||||
@SuppressLint("Registered")
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class BriarActivity extends BaseActivity {
|
||||
|
||||
public static final String GROUP_ID = "briar.GROUP_ID";
|
||||
@@ -60,7 +62,8 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
protected void onActivityResult(int request, int result,
|
||||
@Nullable Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
if (request == REQUEST_PASSWORD) {
|
||||
// The result can be RESULT_CANCELED if there's no account
|
||||
@@ -89,7 +92,7 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
} else if (lockManager.isLocked() && !isFinishing()) {
|
||||
// Also check that the activity isn't finishing already.
|
||||
// This is possible if we finished in onActivityResult().
|
||||
// Lauching another UnlockActivity would cause a loop.
|
||||
// Launching another UnlockActivity would cause a loop.
|
||||
Intent i = new Intent(this, UnlockActivity.class);
|
||||
startActivityForResult(i, REQUEST_UNLOCK);
|
||||
} else if (SDK_INT >= 23) {
|
||||
@@ -111,6 +114,10 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
lockManager.onActivityStop();
|
||||
}
|
||||
|
||||
protected boolean signedIn() {
|
||||
return briarController.accountSignedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transition animations.
|
||||
* @param enterTransition used to move views into initial positions
|
||||
@@ -178,13 +185,15 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
b.show();
|
||||
}
|
||||
|
||||
protected void signOut(boolean removeFromRecentApps) {
|
||||
protected void signOut(boolean removeFromRecentApps,
|
||||
boolean deleteAccount) {
|
||||
if (briarController.accountSignedIn()) {
|
||||
// Don't use UiResultHandler because we want the result even if
|
||||
// this activity has been destroyed
|
||||
briarController.signOut(result -> runOnUiThread(
|
||||
() -> exit(removeFromRecentApps)));
|
||||
() -> exit(removeFromRecentApps)), deleteAccount);
|
||||
} else {
|
||||
if (deleteAccount) briarController.deleteAccount();
|
||||
exit(removeFromRecentApps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ public interface RequestCodes {
|
||||
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
|
||||
int REQUEST_UNLOCK = 11;
|
||||
int REQUEST_KEYGUARD_UNLOCK = 12;
|
||||
int REQUEST_ATTACH_IMAGE = 13;
|
||||
int REQUEST_SAVE_ATTACHMENT = 14;
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import javax.annotation.Nullable;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
|
||||
|
||||
@@ -35,7 +37,7 @@ abstract class BasePostFragment extends BaseFragment {
|
||||
static final String POST_ID = "briar.POST_ID";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(BasePostFragment.class.getName());
|
||||
getLogger(BasePostFragment.class.getName());
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@@ -52,7 +54,7 @@ abstract class BasePostFragment extends BaseFragment {
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
// retrieve MessageId of blog post from arguments
|
||||
byte[] p = getArguments().getByteArray(POST_ID);
|
||||
byte[] p = requireNonNull(getArguments()).getByteArray(POST_ID);
|
||||
if (p == null) throw new IllegalStateException("No post ID in args");
|
||||
postId = new MessageId(p);
|
||||
|
||||
@@ -68,6 +70,7 @@ abstract class BasePostFragment extends BaseFragment {
|
||||
|
||||
@Override
|
||||
public void onAuthorClick(BlogPostItem post) {
|
||||
if (getContext() == null) return;
|
||||
Intent i = new Intent(getContext(), BlogActivity.class);
|
||||
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
|
||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
@@ -3,12 +3,14 @@ package org.briarproject.briar.android.blog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView.LayoutManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -43,6 +45,7 @@ import javax.inject.Inject;
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
|
||||
@@ -61,15 +64,17 @@ public class BlogFragment extends BaseFragment
|
||||
BlogController blogController;
|
||||
@Inject
|
||||
SharingController sharingController;
|
||||
@Nullable
|
||||
private Parcelable layoutManagerState;
|
||||
|
||||
private GroupId groupId;
|
||||
private BlogPostAdapter adapter;
|
||||
private LayoutManager layoutManager;
|
||||
private BriarRecyclerView list;
|
||||
private MenuItem writeButton, deleteButton;
|
||||
private boolean isMyBlog = false, canDeleteBlog = false;
|
||||
|
||||
static BlogFragment newInstance(GroupId groupId) {
|
||||
|
||||
BlogFragment f = new BlogFragment();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
@@ -79,34 +84,40 @@ public class BlogFragment extends BaseFragment
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
blogController.setBlogSharingListener(this);
|
||||
sharingController.setSharingListener(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
Bundle args = getArguments();
|
||||
Bundle args = requireNonNull(getArguments());
|
||||
byte[] b = args.getByteArray(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No group ID in args");
|
||||
groupId = new GroupId(b);
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
||||
|
||||
adapter =
|
||||
new BlogPostAdapter(getActivity(), this, getFragmentManager());
|
||||
adapter = new BlogPostAdapter(requireNonNull(getActivity()), this,
|
||||
getFragmentManager());
|
||||
list = v.findViewById(R.id.postList);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
layoutManager = new LinearLayoutManager(getActivity());
|
||||
list.setLayoutManager(layoutManager);
|
||||
list.setAdapter(adapter);
|
||||
list.showProgressBar();
|
||||
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
|
||||
|
||||
return v;
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
layoutManagerState =
|
||||
savedInstanceState.getParcelable("layoutManager");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
blogController.setBlogSharingListener(this);
|
||||
sharingController.setSharingListener(this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,6 +137,15 @@ public class BlogFragment extends BaseFragment
|
||||
list.stopPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (layoutManager != null) {
|
||||
layoutManagerState = layoutManager.onSaveInstanceState();
|
||||
outState.putParcelable("layoutManager", layoutManagerState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.blogs_blog_actions, menu);
|
||||
@@ -218,7 +238,10 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
@Override
|
||||
public void onAuthorClick(BlogPostItem post) {
|
||||
if (post.getGroupId().equals(groupId)) return; // We're already there
|
||||
if (post.getGroupId().equals(groupId) || getContext() == null) {
|
||||
// We're already there
|
||||
return;
|
||||
}
|
||||
Intent i = new Intent(getContext(), BlogActivity.class);
|
||||
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
|
||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
@@ -235,7 +258,12 @@ public class BlogFragment extends BaseFragment
|
||||
list.showData();
|
||||
} else {
|
||||
adapter.addAll(posts);
|
||||
if (reload) list.scrollToPosition(0);
|
||||
if (reload || layoutManagerState == null) {
|
||||
list.scrollToPosition(0);
|
||||
} else {
|
||||
layoutManager.onRestoreInstanceState(
|
||||
layoutManagerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.blog;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@@ -35,6 +36,7 @@ import javax.inject.Inject;
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.support.design.widget.Snackbar.LENGTH_LONG;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
|
||||
|
||||
@@ -53,7 +55,10 @@ public class FeedFragment extends BaseFragment implements
|
||||
private BlogPostAdapter adapter;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private BriarRecyclerView list;
|
||||
private Blog personalBlog = null;
|
||||
@Nullable
|
||||
private Blog personalBlog;
|
||||
@Nullable
|
||||
private Parcelable layoutManagerState;
|
||||
|
||||
public static FeedFragment newInstance() {
|
||||
FeedFragment f = new FeedFragment();
|
||||
@@ -64,13 +69,18 @@ public class FeedFragment extends BaseFragment implements
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
feedController.setFeedListener(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
getActivity().setTitle(R.string.blogs_button);
|
||||
requireNonNull(getActivity()).setTitle(R.string.blogs_button);
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
||||
|
||||
@@ -85,13 +95,12 @@ public class FeedFragment extends BaseFragment implements
|
||||
list.setEmptyText(R.string.blogs_feed_empty_state);
|
||||
list.setEmptyAction(R.string.blogs_feed_empty_state_action);
|
||||
|
||||
return v;
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
layoutManagerState =
|
||||
savedInstanceState.getParcelable("layoutManager");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
feedController.setFeedListener(this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,6 +132,15 @@ public class FeedFragment extends BaseFragment implements
|
||||
// TODO save list position in database/preferences?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (layoutManager != null) {
|
||||
layoutManagerState = layoutManager.onSaveInstanceState();
|
||||
outState.putParcelable("layoutManager", layoutManagerState);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPersonalBlog() {
|
||||
feedController.loadPersonalBlog(
|
||||
new UiResultExceptionHandler<Blog, DbException>(this) {
|
||||
@@ -150,6 +168,12 @@ public class FeedFragment extends BaseFragment implements
|
||||
if (clear) adapter.setItems(posts);
|
||||
else adapter.addAll(posts);
|
||||
if (posts.isEmpty()) list.showData();
|
||||
if (layoutManagerState == null) {
|
||||
list.scrollToPosition(0); // Scroll to the top
|
||||
} else {
|
||||
layoutManager.onRestoreInstanceState(
|
||||
layoutManagerState);
|
||||
}
|
||||
} else {
|
||||
LOG.info("Concurrent update, reloading");
|
||||
loadBlogPosts(clear);
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.briarproject.briar.android.controller.handler.UiResultExceptionHandle
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
|
||||
@UiThread
|
||||
@@ -42,13 +43,17 @@ public class FeedPostFragment extends BasePostFragment {
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
Bundle args = getArguments();
|
||||
Bundle args = requireNonNull(getArguments());
|
||||
byte[] b = args.getByteArray(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No group ID in args");
|
||||
blogId = new GroupId(b);
|
||||
@@ -61,11 +66,6 @@ public class FeedPostFragment extends BasePostFragment {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.briar.android.blog;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -18,7 +19,10 @@ import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.view.TextInputView;
|
||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
||||
import org.briarproject.briar.android.view.TextSendController;
|
||||
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
@@ -27,18 +31,18 @@ import static android.view.View.FOCUS_DOWN;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
|
||||
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
public class ReblogFragment extends BaseFragment implements SendListener {
|
||||
|
||||
public static final String TAG = ReblogFragment.class.getName();
|
||||
|
||||
private ViewHolder ui;
|
||||
private GroupId blogId;
|
||||
private MessageId postId;
|
||||
private BlogPostItem item;
|
||||
|
||||
@Inject
|
||||
@@ -70,24 +74,22 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
Bundle args = getArguments();
|
||||
blogId = new GroupId(args.getByteArray(GROUP_ID));
|
||||
postId = new MessageId(args.getByteArray(POST_ID));
|
||||
Bundle args = requireNonNull(getArguments());
|
||||
GroupId blogId =
|
||||
new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
|
||||
MessageId postId =
|
||||
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_reblog, container, false);
|
||||
ui = new ViewHolder(v);
|
||||
ui.post.setTransitionName(postId);
|
||||
ui.input.setSendButtonEnabled(false);
|
||||
TextSendController sendController =
|
||||
new TextSendController(ui.input, this, true);
|
||||
ui.input.setSendController(sendController);
|
||||
ui.input.setEnabled(false);
|
||||
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
|
||||
showProgressBar();
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
// TODO: Load blog post when fragment is created. #631
|
||||
feedController.loadBlogPost(blogId, postId,
|
||||
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||
this) {
|
||||
@@ -102,6 +104,8 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
handleDbException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private void bindViewHolder() {
|
||||
@@ -112,16 +116,14 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
ui.post.bindItem(item);
|
||||
ui.post.hideReblogButton();
|
||||
|
||||
ui.input.setListener(this);
|
||||
ui.input.setSendButtonEnabled(true);
|
||||
ui.input.setEnabled(true);
|
||||
ui.scrollView.post(() -> ui.scrollView.fullScroll(FOCUS_DOWN));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendClick(String text) {
|
||||
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||
ui.input.hideSoftKeyboard();
|
||||
String comment = getComment();
|
||||
feedController.repeatPost(item, comment,
|
||||
feedController.repeatPost(item, text,
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
@@ -131,12 +133,6 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getComment() {
|
||||
if (ui.input.getText().length() == 0) return null;
|
||||
return ui.input.getText().toString();
|
||||
}
|
||||
|
||||
private void showProgressBar() {
|
||||
ui.progressBar.setVisibility(VISIBLE);
|
||||
ui.input.setVisibility(GONE);
|
||||
|
||||
@@ -29,6 +29,7 @@ import javax.inject.Inject;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@@ -72,6 +73,15 @@ public class RssFeedImportActivity extends BriarActivity {
|
||||
enableOrDisableImportButton();
|
||||
}
|
||||
});
|
||||
urlInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
|
||||
importButton.getVisibility() == VISIBLE) {
|
||||
publish();
|
||||
hideSoftKeyboard(urlInput);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
importButton = findViewById(R.id.importButton);
|
||||
importButton.setOnClickListener(v -> publish());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.briarproject.briar.android.blog;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.ProgressBar;
|
||||
@@ -14,19 +14,22 @@ import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.view.TextInputView;
|
||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
||||
import org.briarproject.briar.android.view.TextSendController;
|
||||
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.blog.BlogManager;
|
||||
import org.briarproject.briar.api.blog.BlogPost;
|
||||
import org.briarproject.briar.api.blog.BlogPostFactory;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -35,10 +38,13 @@ import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class WriteBlogPostActivity extends BriarActivity
|
||||
implements OnEditorActionListener, TextInputListener {
|
||||
implements OnEditorActionListener, SendListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(WriteBlogPostActivity.class.getName());
|
||||
@@ -58,9 +64,8 @@ public class WriteBlogPostActivity extends BriarActivity
|
||||
@Inject
|
||||
volatile BlogManager blogManager;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
Intent i = getIntent();
|
||||
@@ -71,24 +76,10 @@ public class WriteBlogPostActivity extends BriarActivity
|
||||
setContentView(R.layout.activity_write_blog_post);
|
||||
|
||||
input = findViewById(R.id.textInput);
|
||||
input.setSendButtonEnabled(false);
|
||||
input.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count,
|
||||
int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before,
|
||||
int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
enableOrDisablePublishButton();
|
||||
}
|
||||
});
|
||||
input.setListener(this);
|
||||
TextSendController sendController =
|
||||
new TextSendController(input, this, false);
|
||||
input.setSendController(sendController);
|
||||
input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
|
||||
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
}
|
||||
@@ -127,18 +118,15 @@ public class WriteBlogPostActivity extends BriarActivity
|
||||
return true;
|
||||
}
|
||||
|
||||
private void enableOrDisablePublishButton() {
|
||||
input.setSendButtonEnabled(input.getText().length() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendClick(String text) {
|
||||
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||
|
||||
// hide publish button, show progress bar
|
||||
input.hideSoftKeyboard();
|
||||
input.setVisibility(GONE);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
|
||||
text = StringUtils.truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
|
||||
storePost(text);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public abstract class BaseContactListAdapter<I extends ContactItem, VH extends C
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(I c1, I c2) {
|
||||
return c1.getContact().getId().equals(c2.getContact().getId());
|
||||
return c1.getContact().equals(c2.getContact());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,8 +47,7 @@ public abstract class BaseContactListAdapter<I extends ContactItem, VH extends C
|
||||
}
|
||||
|
||||
int findItemPosition(ContactId c) {
|
||||
int count = getItemCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
I item = getItemAt(i);
|
||||
if (item != null && item.getContact().getId().equals(c))
|
||||
return i;
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ContactListAdapter extends
|
||||
BaseContactListAdapter<ContactListItem, ContactListItemViewHolder> {
|
||||
|
||||
@@ -28,6 +30,9 @@ public class ContactListAdapter extends
|
||||
public boolean areContentsTheSame(ContactListItem c1, ContactListItem c2) {
|
||||
// check for all properties that influence visual
|
||||
// representation of contact
|
||||
if (c1.isEmpty() != c2.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (c1.getUnreadCount() != c2.getUnreadCount()) {
|
||||
return false;
|
||||
}
|
||||
@@ -39,11 +44,7 @@ public class ContactListAdapter extends
|
||||
|
||||
@Override
|
||||
public int compare(ContactListItem c1, ContactListItem c2) {
|
||||
long time1 = c1.getTimestamp();
|
||||
long time2 = c2.getTimestamp();
|
||||
if (time1 < time2) return 1;
|
||||
if (time1 > time2) return -1;
|
||||
return 0;
|
||||
return Long.compare(c2.getTimestamp(), c1.getTimestamp());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,11 +51,13 @@ import javax.inject.Inject;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
|
||||
import static android.support.v4.view.ViewCompat.getTransitionName;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -102,8 +104,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
getActivity().setTitle(R.string.contact_list_button);
|
||||
requireNonNull(getActivity()).setTitle(R.string.contact_list_button);
|
||||
|
||||
View contentView = inflater.inflate(R.layout.list, container, false);
|
||||
|
||||
@@ -114,7 +115,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
ContactId contactId = item.getContact().getId();
|
||||
i.putExtra(CONTACT_ID, contactId.getInt());
|
||||
|
||||
if (SDK_INT >= 23) {
|
||||
if (SDK_INT >= 23 && !isSamsung7()) {
|
||||
ContactListItemViewHolder holder =
|
||||
(ContactListItemViewHolder) list
|
||||
.getRecyclerView()
|
||||
@@ -280,7 +281,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
ContactListItem item = adapter.getItemAt(position);
|
||||
if (item != null) {
|
||||
item.setConnected(connected);
|
||||
adapter.notifyItemChanged(position);
|
||||
adapter.updateItemAt(position, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.CONTACTS;
|
||||
import static org.briarproject.briar.android.contactselection.ContactSelectorActivity.getContactsFromIds;
|
||||
@@ -50,10 +51,10 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle args = getArguments();
|
||||
Bundle args = requireNonNull(getArguments());
|
||||
byte[] b = args.getByteArray(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No GroupId");
|
||||
groupId = new GroupId(b);
|
||||
@@ -72,7 +73,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
|
||||
list.setEmptyImage(R.drawable.ic_empty_state_contact_list);
|
||||
list.setEmptyText(getString(R.string.no_contacts_selector));
|
||||
list.setEmptyAction(getString(R.string.no_contacts_selector_action));
|
||||
adapter = getAdapter(getContext(), this);
|
||||
adapter = getAdapter(requireNonNull(getContext()), this);
|
||||
list.setAdapter(adapter);
|
||||
|
||||
// restore selected contacts if available
|
||||
|
||||
@@ -16,5 +16,8 @@ public interface BriarController extends ActivityLifecycleController {
|
||||
|
||||
void doNotAskAgainForDozeWhiteListing();
|
||||
|
||||
void signOut(ResultHandler<Void> eventHandler);
|
||||
void signOut(ResultHandler<Void> eventHandler, boolean deleteAccount);
|
||||
|
||||
void deleteAccount();
|
||||
|
||||
}
|
||||
|
||||
@@ -120,7 +120,8 @@ public class BriarControllerImpl implements BriarController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signOut(ResultHandler<Void> eventHandler) {
|
||||
public void signOut(ResultHandler<Void> eventHandler,
|
||||
boolean deleteAccount) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// Wait for the service to finish starting up
|
||||
@@ -134,11 +135,18 @@ public class BriarControllerImpl implements BriarController {
|
||||
service.waitForShutdown();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for service");
|
||||
} finally {
|
||||
if (deleteAccount) accountManager.deleteAccount();
|
||||
}
|
||||
eventHandler.onResult(null);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAccount() {
|
||||
accountManager.deleteAccount();
|
||||
}
|
||||
|
||||
private void unbindService() {
|
||||
if (bound) activity.unbindService(serviceConnection);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
|
||||
|
||||
setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme);
|
||||
|
||||
if (getActivity() == null) return;
|
||||
((BriarActivity) getActivity()).getActivityComponent().inject(this);
|
||||
BriarActivity a = (BriarActivity) requireNonNull(getActivity());
|
||||
a.getActivityComponent().inject(this);
|
||||
viewModel = ViewModelProviders.of(getActivity(), viewModelFactory)
|
||||
.get(ConversationViewModel.class);
|
||||
}
|
||||
@@ -48,7 +48,6 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_alias_dialog, container,
|
||||
false);
|
||||
|
||||
@@ -69,4 +68,5 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapFactory.Options;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.media.ExifInterface;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.bumptech.glide.util.MarkEnforcingInputStream;
|
||||
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
@@ -42,8 +46,10 @@ class AttachmentController {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(AttachmentController.class.getName());
|
||||
private static final int READ_LIMIT = 1024 * 8192;
|
||||
|
||||
private final MessagingManager messagingManager;
|
||||
private final ImageHelper imageHelper;
|
||||
private final int defaultSize;
|
||||
private final int minWidth, maxWidth;
|
||||
private final int minHeight, maxHeight;
|
||||
@@ -51,18 +57,38 @@ class AttachmentController {
|
||||
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
AttachmentController(MessagingManager messagingManager, Resources res) {
|
||||
AttachmentController(MessagingManager messagingManager,
|
||||
AttachmentDimensions dimensions, ImageHelper imageHelper) {
|
||||
this.messagingManager = messagingManager;
|
||||
defaultSize =
|
||||
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
|
||||
minWidth = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_min_width);
|
||||
maxWidth = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_max_width);
|
||||
minHeight = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_min_height);
|
||||
maxHeight = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_max_height);
|
||||
this.imageHelper = imageHelper;
|
||||
defaultSize = dimensions.defaultSize;
|
||||
minWidth = dimensions.minWidth;
|
||||
maxWidth = dimensions.maxWidth;
|
||||
minHeight = dimensions.minHeight;
|
||||
maxHeight = dimensions.maxHeight;
|
||||
}
|
||||
|
||||
AttachmentController(MessagingManager messagingManager,
|
||||
AttachmentDimensions dimensions) {
|
||||
this(messagingManager, dimensions, new ImageHelper() {
|
||||
@Override
|
||||
public DecodeResult decodeStream(InputStream is) {
|
||||
Options options = new Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(is, null, options);
|
||||
String mimeType = options.outMimeType;
|
||||
if (mimeType == null) mimeType = "";
|
||||
return new DecodeResult(options.outWidth, options.outHeight,
|
||||
mimeType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getExtensionFromMimeType(String mimeType) {
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
return mimeTypeMap.getExtensionFromMimeType(mimeType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void put(MessageId messageId, List<AttachmentItem> attachments) {
|
||||
@@ -81,31 +107,53 @@ class AttachmentController {
|
||||
List<Pair<AttachmentHeader, Attachment>> attachments =
|
||||
new ArrayList<>(headers.size());
|
||||
for (AttachmentHeader h : headers) {
|
||||
Attachment a =
|
||||
messagingManager.getAttachment(h.getMessageId());
|
||||
Attachment a = messagingManager.getAttachment(h.getMessageId());
|
||||
attachments.add(new Pair<>(h, a));
|
||||
}
|
||||
logDuration(LOG, "Loading attachment", start);
|
||||
return attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
|
||||
* <p>
|
||||
* Note: This closes the {@link Attachment}'s {@link InputStream}.
|
||||
*/
|
||||
List<AttachmentItem> getAttachmentItems(
|
||||
List<Pair<AttachmentHeader, Attachment>> attachments) {
|
||||
boolean needsSize = attachments.size() == 1;
|
||||
List<AttachmentItem> items = new ArrayList<>(attachments.size());
|
||||
for (Pair<AttachmentHeader, Attachment> a : attachments) {
|
||||
AttachmentItem item =
|
||||
getAttachmentItem(a.getFirst(), a.getSecond());
|
||||
getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) {
|
||||
/**
|
||||
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
|
||||
* {@link InputStream} which will be closed when this method returns.
|
||||
*/
|
||||
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
|
||||
boolean needsSize) {
|
||||
MessageId messageId = h.getMessageId();
|
||||
Size size = new Size();
|
||||
if (!needsSize) {
|
||||
String mimeType = h.getContentType();
|
||||
String extension = imageHelper.getExtensionFromMimeType(mimeType);
|
||||
boolean hasError = false;
|
||||
if (extension == null) {
|
||||
extension = "";
|
||||
hasError = true;
|
||||
}
|
||||
return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0,
|
||||
0, hasError);
|
||||
}
|
||||
|
||||
InputStream is = a.getStream();
|
||||
is.mark(Integer.MAX_VALUE);
|
||||
Size size = new Size();
|
||||
InputStream is = new MarkEnforcingInputStream(
|
||||
new BufferedInputStream(a.getStream()));
|
||||
is.mark(READ_LIMIT);
|
||||
try {
|
||||
// use exif to get size
|
||||
if (h.getContentType().equals("image/jpeg")) {
|
||||
@@ -118,6 +166,8 @@ class AttachmentController {
|
||||
// use BitmapFactory to get size
|
||||
if (size.error) {
|
||||
is.reset();
|
||||
// need to mark again to re-add read limit
|
||||
is.mark(READ_LIMIT);
|
||||
size = getSizeFromBitmap(is);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -127,19 +177,24 @@ class AttachmentController {
|
||||
}
|
||||
|
||||
// calculate thumbnail size
|
||||
Size thumbnailSize = new Size(defaultSize, defaultSize);
|
||||
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
|
||||
if (!size.error) {
|
||||
thumbnailSize = getThumbnailSize(size.width, size.height);
|
||||
thumbnailSize =
|
||||
getThumbnailSize(size.width, size.height, size.mimeType);
|
||||
}
|
||||
// get file extension
|
||||
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
|
||||
boolean hasError = extension == null || size.error;
|
||||
if (extension == null) extension = "";
|
||||
return new AttachmentItem(messageId, size.width, size.height,
|
||||
thumbnailSize.width, thumbnailSize.height, size.error);
|
||||
size.mimeType, extension, thumbnailSize.width,
|
||||
thumbnailSize.height, hasError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
|
||||
*/
|
||||
private static Size getSizeFromExif(InputStream is)
|
||||
throws IOException {
|
||||
private Size getSizeFromExif(InputStream is) throws IOException {
|
||||
ExifInterface exif = new ExifInterface(is);
|
||||
// these can return 0 independent of default value
|
||||
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
|
||||
@@ -151,24 +206,21 @@ class AttachmentController {
|
||||
orientation == ORIENTATION_TRANSVERSE ||
|
||||
orientation == ORIENTATION_TRANSPOSE) {
|
||||
//noinspection SuspiciousNameCombination
|
||||
return new Size(height, width);
|
||||
return new Size(height, width, "image/jpeg");
|
||||
}
|
||||
return new Size(width, height);
|
||||
return new Size(width, height, "image/jpeg");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of any image {@link InputStream}.
|
||||
*/
|
||||
private static Size getSizeFromBitmap(InputStream is) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(is, null, options);
|
||||
if (options.outWidth < 1 || options.outHeight < 1)
|
||||
return new Size();
|
||||
return new Size(options.outWidth, options.outHeight);
|
||||
private Size getSizeFromBitmap(InputStream is) {
|
||||
DecodeResult result = imageHelper.decodeStream(is);
|
||||
if (result.width < 1 || result.height < 1) return new Size();
|
||||
return new Size(result.width, result.height, result.mimeType);
|
||||
}
|
||||
|
||||
private Size getThumbnailSize(int width, int height) {
|
||||
private Size getThumbnailSize(int width, int height, String mimeType) {
|
||||
float widthPercentage = maxWidth / (float) width;
|
||||
float heightPercentage = maxHeight / (float) height;
|
||||
float scaleFactor = Math.min(widthPercentage, heightPercentage);
|
||||
@@ -184,24 +236,27 @@ class AttachmentController {
|
||||
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
|
||||
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
|
||||
}
|
||||
return new Size(thumbnailWidth, thumbnailHeight);
|
||||
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
|
||||
}
|
||||
|
||||
private static class Size {
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final String mimeType;
|
||||
private final boolean error;
|
||||
|
||||
private Size(int width, int height) {
|
||||
private Size(int width, int height, String mimeType) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mimeType = mimeType;
|
||||
this.error = false;
|
||||
}
|
||||
|
||||
private Size() {
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.mimeType = "";
|
||||
this.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
class AttachmentDimensions {
|
||||
|
||||
final int defaultSize;
|
||||
final int minWidth, maxWidth;
|
||||
final int minHeight, maxHeight;
|
||||
|
||||
@VisibleForTesting
|
||||
AttachmentDimensions(int defaultSize, int minWidth, int maxWidth,
|
||||
int minHeight, int maxHeight) {
|
||||
this.defaultSize = defaultSize;
|
||||
this.minWidth = minWidth;
|
||||
this.maxWidth = maxWidth;
|
||||
this.minHeight = minHeight;
|
||||
this.maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
static AttachmentDimensions getAttachmentDimensions(Resources res) {
|
||||
int defaultSize =
|
||||
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
|
||||
int minWidth = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_min_width);
|
||||
int maxWidth = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_max_width);
|
||||
int minHeight = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_min_height);
|
||||
int maxHeight = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_max_height);
|
||||
return new AttachmentDimensions(defaultSize, minWidth, maxWidth,
|
||||
minHeight, minHeight);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@@ -14,8 +17,10 @@ public class AttachmentItem implements Parcelable {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final int width, height;
|
||||
private final String mimeType, extension;
|
||||
private final int thumbnailWidth, thumbnailHeight;
|
||||
private final boolean hasError;
|
||||
private final long instanceId;
|
||||
|
||||
public static final Creator<AttachmentItem> CREATOR =
|
||||
new Creator<AttachmentItem>() {
|
||||
@@ -30,14 +35,20 @@ public class AttachmentItem implements Parcelable {
|
||||
}
|
||||
};
|
||||
|
||||
AttachmentItem(MessageId messageId, int width, int height,
|
||||
int thumbnailWidth, int thumbnailHeight, boolean hasError) {
|
||||
private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0);
|
||||
|
||||
AttachmentItem(MessageId messageId, int width, int height, String mimeType,
|
||||
String extension, int thumbnailWidth, int thumbnailHeight,
|
||||
boolean hasError) {
|
||||
this.messageId = messageId;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mimeType = mimeType;
|
||||
this.extension = extension;
|
||||
this.thumbnailWidth = thumbnailWidth;
|
||||
this.thumbnailHeight = thumbnailHeight;
|
||||
this.hasError = hasError;
|
||||
instanceId = NEXT_INSTANCE_ID.getAndIncrement();
|
||||
}
|
||||
|
||||
protected AttachmentItem(Parcel in) {
|
||||
@@ -46,9 +57,12 @@ public class AttachmentItem implements Parcelable {
|
||||
messageId = new MessageId(messageIdByte);
|
||||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
mimeType = in.readString();
|
||||
extension = in.readString();
|
||||
thumbnailWidth = in.readInt();
|
||||
thumbnailHeight = in.readInt();
|
||||
hasError = in.readByte() != 0;
|
||||
instanceId = in.readLong();
|
||||
}
|
||||
|
||||
public MessageId getMessageId() {
|
||||
@@ -63,6 +77,14 @@ public class AttachmentItem implements Parcelable {
|
||||
return height;
|
||||
}
|
||||
|
||||
String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
String getExtension() {
|
||||
return extension;
|
||||
}
|
||||
|
||||
int getThumbnailWidth() {
|
||||
return thumbnailWidth;
|
||||
}
|
||||
@@ -75,9 +97,8 @@ public class AttachmentItem implements Parcelable {
|
||||
return hasError;
|
||||
}
|
||||
|
||||
// TODO use counter instead, because in theory one attachment can appear in more than one messages
|
||||
String getTransitionName() {
|
||||
return String.valueOf(messageId.hashCode());
|
||||
return String.valueOf(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,9 +111,18 @@ public class AttachmentItem implements Parcelable {
|
||||
dest.writeByteArray(messageId.getBytes());
|
||||
dest.writeInt(width);
|
||||
dest.writeInt(height);
|
||||
dest.writeString(mimeType);
|
||||
dest.writeString(extension);
|
||||
dest.writeInt(thumbnailWidth);
|
||||
dest.writeInt(thumbnailHeight);
|
||||
dest.writeByte((byte) (hasError ? 1 : 0));
|
||||
dest.writeLong(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
return o instanceof AttachmentItem &&
|
||||
instanceId == ((AttachmentItem) o).instanceId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.arch.lifecycle.Observer;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.design.widget.Snackbar;
|
||||
@@ -27,7 +30,6 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
@@ -44,14 +46,10 @@ 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.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
@@ -62,8 +60,12 @@ import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||
import org.briarproject.briar.android.view.ImagePreview;
|
||||
import org.briarproject.briar.android.view.TextAttachmentController;
|
||||
import org.briarproject.briar.android.view.TextAttachmentController.AttachImageListener;
|
||||
import org.briarproject.briar.android.view.TextInputView;
|
||||
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
|
||||
import org.briarproject.briar.android.view.TextSendController;
|
||||
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||
import org.briarproject.briar.api.client.ProtocolStateException;
|
||||
@@ -78,7 +80,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessage;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
|
||||
@@ -100,11 +101,12 @@ import im.delight.android.identicons.IdenticonDrawable;
|
||||
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
|
||||
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener;
|
||||
|
||||
import static android.arch.lifecycle.Lifecycle.State.STARTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
|
||||
import static android.support.v4.view.ViewCompat.setTransitionName;
|
||||
import static android.support.v7.util.SortedList.INVALID_POSITION;
|
||||
import static android.view.Gravity.END;
|
||||
import static android.view.Gravity.RIGHT;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.sort;
|
||||
@@ -114,11 +116,14 @@ import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
|
||||
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
|
||||
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;
|
||||
@@ -129,15 +134,16 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ConversationActivity extends BriarActivity
|
||||
implements EventListener, ConversationListener, TextInputListener,
|
||||
TextCache, AttachmentCache {
|
||||
implements EventListener, ConversationListener, SendListener,
|
||||
TextCache, AttachmentCache, AttachImageListener {
|
||||
|
||||
public static final String CONTACT_ID = "briar.CONTACT_ID";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ConversationActivity.class.getName());
|
||||
private static final String SHOW_ONBOARDING_INTRODUCTION =
|
||||
"showOnboardingIntroduction";
|
||||
|
||||
private static final int TRANSITION_DURATION_MS = 500;
|
||||
private static final int ONBOARDING_DELAY_MS = 250;
|
||||
|
||||
@Inject
|
||||
AndroidNotificationManager notificationManager;
|
||||
@@ -146,20 +152,8 @@ public class ConversationActivity extends BriarActivity
|
||||
@Inject
|
||||
@CryptoExecutor
|
||||
Executor cryptoExecutor;
|
||||
|
||||
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
|
||||
private AttachmentController attachmentController;
|
||||
|
||||
private ConversationViewModel viewModel;
|
||||
private ConversationVisitor visitor;
|
||||
private ConversationAdapter adapter;
|
||||
private Toolbar toolbar;
|
||||
private CircleImageView toolbarAvatar;
|
||||
private ImageView toolbarStatus;
|
||||
private TextView toolbarTitle;
|
||||
private BriarRecyclerView list;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private TextInputView textInputView;
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
@@ -182,22 +176,37 @@ public class ConversationActivity extends BriarActivity
|
||||
volatile BlogSharingManager blogSharingManager;
|
||||
@Inject
|
||||
volatile GroupInvitationManager groupInvitationManager;
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private volatile ContactId contactId;
|
||||
@Nullable
|
||||
private volatile GroupId messagingGroupId;
|
||||
|
||||
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
|
||||
private final Observer<String> contactNameObserver = name -> {
|
||||
requireNonNull(name);
|
||||
loadMessages();
|
||||
};
|
||||
|
||||
private AttachmentController attachmentController;
|
||||
private ConversationViewModel viewModel;
|
||||
private ConversationVisitor visitor;
|
||||
private ConversationAdapter adapter;
|
||||
private Toolbar toolbar;
|
||||
private CircleImageView toolbarAvatar;
|
||||
private ImageView toolbarStatus;
|
||||
private TextView toolbarTitle;
|
||||
private BriarRecyclerView list;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private TextInputView textInputView;
|
||||
private TextSendController sendController;
|
||||
@Nullable
|
||||
private Parcelable layoutManagerState;
|
||||
|
||||
private volatile ContactId contactId;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
if (SDK_INT >= 21) {
|
||||
Transition slide = new Slide(END);
|
||||
// Spurious lint warning - using END causes a crash
|
||||
@SuppressLint("RtlHardcoded")
|
||||
Transition slide = new Slide(RIGHT);
|
||||
slide.setDuration(TRANSITION_DURATION_MS);
|
||||
setSceneTransitionAnimation(slide, null, slide);
|
||||
}
|
||||
super.onCreate(state);
|
||||
@@ -209,7 +218,6 @@ public class ConversationActivity extends BriarActivity
|
||||
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)
|
||||
.get(ConversationViewModel.class);
|
||||
viewModel.setContactId(contactId);
|
||||
attachmentController = viewModel.getAttachmentController();
|
||||
|
||||
setContentView(R.layout.activity_conversation);
|
||||
@@ -233,6 +241,8 @@ public class ConversationActivity extends BriarActivity
|
||||
requireNonNull(deleted);
|
||||
if (deleted) finish();
|
||||
});
|
||||
viewModel.getAddedPrivateMessage().observe(this,
|
||||
this::onAddedPrivateMessage);
|
||||
|
||||
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
|
||||
setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
|
||||
@@ -245,9 +255,34 @@ public class ConversationActivity extends BriarActivity
|
||||
list.setLayoutManager(layoutManager);
|
||||
list.setAdapter(adapter);
|
||||
list.setEmptyText(getString(R.string.no_private_messages));
|
||||
ConversationScrollListener scrollListener =
|
||||
new ConversationScrollListener(adapter, viewModel);
|
||||
list.getRecyclerView().addOnScrollListener(scrollListener);
|
||||
|
||||
textInputView = findViewById(R.id.text_input_container);
|
||||
textInputView.setListener(this);
|
||||
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
|
||||
ImagePreview imagePreview = findViewById(R.id.imagePreview);
|
||||
sendController = new TextAttachmentController(textInputView,
|
||||
imagePreview, this, this);
|
||||
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
|
||||
if (hasSupport != null && hasSupport) {
|
||||
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
|
||||
((TextAttachmentController) sendController)
|
||||
.setImagesSupported();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sendController = new TextSendController(textInputView, this, false);
|
||||
}
|
||||
textInputView.setSendController(sendController);
|
||||
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
|
||||
textInputView.setEnabled(false);
|
||||
textInputView.addOnKeyboardShownListener(this::scrollToBottom);
|
||||
}
|
||||
|
||||
private void scrollToBottom() {
|
||||
int items = adapter.getItemCount();
|
||||
if (items > 0) list.scrollToPosition(items - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -256,7 +291,8 @@ public class ConversationActivity extends BriarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
protected void onActivityResult(int request, int result,
|
||||
@Nullable Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
|
||||
if (request == REQUEST_INTRODUCTION && result == RESULT_OK) {
|
||||
@@ -264,6 +300,9 @@ public class ConversationActivity extends BriarActivity
|
||||
Snackbar.LENGTH_SHORT);
|
||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||
snackbar.show();
|
||||
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
|
||||
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
|
||||
((TextAttachmentController) sendController).onImageReceived(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +317,16 @@ public class ConversationActivity extends BriarActivity
|
||||
list.startPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Trigger loading of contact data, noop if data was loaded already.
|
||||
//
|
||||
// We can only start loading data *after* we are sure
|
||||
// the user has signed in. After sign-in, onCreate() isn't run again.
|
||||
if (signedIn()) viewModel.setContactId(contactId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
@@ -287,16 +336,39 @@ public class ConversationActivity extends BriarActivity
|
||||
list.stopPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (layoutManager != null) {
|
||||
layoutManagerState = layoutManager.onSaveInstanceState();
|
||||
outState.putParcelable("layoutManager", layoutManagerState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
layoutManagerState = savedInstanceState.getParcelable("layoutManager");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu items for use in the action bar
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.conversation_actions, menu);
|
||||
|
||||
enableIntroductionActionIfAvailable(
|
||||
menu.findItem(R.id.action_introduction));
|
||||
enableAliasActionIfAvailable(
|
||||
menu.findItem(R.id.action_set_alias));
|
||||
// enable introduction action if available
|
||||
observeOnce(viewModel.showIntroductionAction(), this, enable -> {
|
||||
if (enable != null && enable) {
|
||||
menu.findItem(R.id.action_introduction).setEnabled(true);
|
||||
// show introduction onboarding, if needed
|
||||
observeOnce(viewModel.showIntroductionOnboarding(), this,
|
||||
this::showIntroductionOnboarding);
|
||||
}
|
||||
});
|
||||
// enable alias action if available
|
||||
observeOnce(viewModel.getContact(), this, contact ->
|
||||
menu.findItem(R.id.action_set_alias).setEnabled(true));
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
@@ -359,33 +431,10 @@ public class ConversationActivity extends BriarActivity
|
||||
Long.compare(b.getTimestamp(), a.getTimestamp()));
|
||||
if (!sorted.isEmpty()) {
|
||||
// If the latest header is a private message, eagerly load
|
||||
// its text so we can set the scroll position correctly
|
||||
// its size so we can set the scroll position correctly
|
||||
ConversationMessageHeader latest = sorted.get(0);
|
||||
if (latest instanceof PrivateMessageHeader) {
|
||||
MessageId id = latest.getId();
|
||||
PrivateMessageHeader h = (PrivateMessageHeader) latest;
|
||||
if (h.hasText()) {
|
||||
String text = textCache.get(id);
|
||||
if (text == null) {
|
||||
LOG.info(
|
||||
"Eagerly loading text of latest message");
|
||||
text = messagingManager.getMessageText(id);
|
||||
textCache.put(id, text);
|
||||
}
|
||||
}
|
||||
if (!h.getAttachmentHeaders().isEmpty()) {
|
||||
List<AttachmentItem> items =
|
||||
attachmentController.get(id);
|
||||
if (items == null) {
|
||||
LOG.info(
|
||||
"Eagerly loading image size for latest message");
|
||||
items = attachmentController.getAttachmentItems(
|
||||
attachmentController
|
||||
.getMessageAttachments(
|
||||
h.getAttachmentHeaders()));
|
||||
attachmentController.put(id, items);
|
||||
}
|
||||
}
|
||||
eagerlyLoadMessageSize((PrivateMessageHeader) latest);
|
||||
}
|
||||
}
|
||||
displayMessages(revision, sorted);
|
||||
@@ -397,17 +446,51 @@ public class ConversationActivity extends BriarActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void eagerlyLoadMessageSize(PrivateMessageHeader h)
|
||||
throws DbException {
|
||||
MessageId id = h.getId();
|
||||
// If the message has text, load it
|
||||
if (h.hasText()) {
|
||||
String text = textCache.get(id);
|
||||
if (text == null) {
|
||||
LOG.info("Eagerly loading text for latest message");
|
||||
text = messagingManager.getMessageText(id);
|
||||
textCache.put(id, text);
|
||||
}
|
||||
}
|
||||
// If the message has a single image, load its size - for multiple
|
||||
// images we use a grid so the size is fixed
|
||||
if (h.getAttachmentHeaders().size() == 1) {
|
||||
List<AttachmentItem> items = attachmentController.get(id);
|
||||
if (items == null) {
|
||||
LOG.info("Eagerly loading image size for latest message");
|
||||
items = attachmentController.getAttachmentItems(
|
||||
attachmentController.getMessageAttachments(
|
||||
h.getAttachmentHeaders()));
|
||||
attachmentController.put(id, items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void displayMessages(int revision,
|
||||
Collection<ConversationMessageHeader> headers) {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
if (revision == adapter.getRevision()) {
|
||||
adapter.incrementRevision();
|
||||
textInputView.setSendButtonEnabled(true);
|
||||
textInputView.setEnabled(true);
|
||||
// start observing onboarding after enabling (only once, because
|
||||
// we only update this when an onboarding should be shown)
|
||||
observeOnce(viewModel.showImageOnboarding(), this,
|
||||
this::showImageOnboarding);
|
||||
List<ConversationItem> items = createItems(headers);
|
||||
adapter.addAll(items);
|
||||
list.showData();
|
||||
// Scroll to the bottom
|
||||
list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
if (layoutManagerState == null) {
|
||||
scrollToBottom();
|
||||
} else {
|
||||
// Restore the previous scroll position
|
||||
layoutManager.onRestoreInstanceState(layoutManagerState);
|
||||
}
|
||||
} else {
|
||||
LOG.info("Concurrent update, reloading");
|
||||
loadMessages();
|
||||
@@ -448,21 +531,28 @@ public class ConversationActivity extends BriarActivity
|
||||
Pair<Integer, ConversationMessageItem> pair =
|
||||
adapter.getMessageItem(m);
|
||||
if (pair != null) {
|
||||
boolean scroll = shouldScrollWhenUpdatingMessage();
|
||||
pair.getSecond().setText(text);
|
||||
boolean bottom = adapter.isScrolledToBottom(layoutManager);
|
||||
adapter.notifyItemChanged(pair.getFirst());
|
||||
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
if (scroll) scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When a message's text or attachments are loaded, scroll to the bottom
|
||||
// if the conversation is visible and we were previously at the bottom
|
||||
private boolean shouldScrollWhenUpdatingMessage() {
|
||||
return getLifecycle().getCurrentState().isAtLeast(STARTED)
|
||||
&& adapter.isScrolledToBottom(layoutManager);
|
||||
}
|
||||
|
||||
private void loadMessageAttachments(MessageId messageId,
|
||||
List<AttachmentHeader> headers) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
List<Pair<AttachmentHeader, Attachment>> attachments =
|
||||
attachmentController.getMessageAttachments(headers);
|
||||
// TODO move getting the items off to the IoExecutor
|
||||
// TODO move getting the items off to IoExecutor, if size == 1
|
||||
List<AttachmentItem> items =
|
||||
attachmentController.getAttachmentItems(attachments);
|
||||
displayMessageAttachments(messageId, items);
|
||||
@@ -479,10 +569,10 @@ public class ConversationActivity extends BriarActivity
|
||||
Pair<Integer, ConversationMessageItem> pair =
|
||||
adapter.getMessageItem(m);
|
||||
if (pair != null) {
|
||||
boolean scroll = shouldScrollWhenUpdatingMessage();
|
||||
pair.getSecond().setAttachments(items);
|
||||
boolean bottom = adapter.isScrolledToBottom(layoutManager);
|
||||
adapter.notifyItemChanged(pair.getFirst());
|
||||
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
if (scroll) scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -531,10 +621,13 @@ public class ConversationActivity extends BriarActivity
|
||||
|
||||
private void addConversationItem(ConversationItem item) {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
boolean bottom = adapter.isScrolledToBottom(layoutManager);
|
||||
adapter.incrementRevision();
|
||||
adapter.add(item);
|
||||
if (bottom) list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
// When adding a new message, scroll to the bottom if the
|
||||
// conversation is visible, even if we're not currently at
|
||||
// the bottom
|
||||
if (getLifecycle().getCurrentState().isAtLeast(STARTED))
|
||||
scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -570,14 +663,18 @@ public class ConversationActivity extends BriarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendClick(String text) {
|
||||
if (text.isEmpty()) return;
|
||||
text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
|
||||
public void onAttachImage(Intent intent) {
|
||||
startActivityForResult(intent, REQUEST_ATTACH_IMAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||
if (isNullOrEmpty(text) && imageUris.isEmpty())
|
||||
throw new AssertionError();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
||||
if (messagingGroupId == null) loadGroupId(text, timestamp);
|
||||
else createMessage(text, timestamp);
|
||||
textInputView.setText("");
|
||||
viewModel.sendMessage(text, imageUris, timestamp);
|
||||
textInputView.clearText();
|
||||
}
|
||||
|
||||
private long getMinTimestampForNewMessage() {
|
||||
@@ -586,48 +683,10 @@ public class ConversationActivity extends BriarActivity
|
||||
return item == null ? 0 : item.getTime() + 1;
|
||||
}
|
||||
|
||||
private void loadGroupId(String text, long timestamp) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
messagingGroupId =
|
||||
messagingManager.getConversationId(contactId);
|
||||
createMessage(text, timestamp);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void createMessage(String text, long timestamp) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
try {
|
||||
//noinspection ConstantConditions init in loadGroupId()
|
||||
storeMessage(privateMessageFactory.createPrivateMessage(
|
||||
messagingGroupId, timestamp, text, emptyList()), text);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeMessage(PrivateMessage m, String text) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.addLocalMessage(m);
|
||||
logDuration(LOG, "Storing message", start);
|
||||
Message message = m.getMessage();
|
||||
PrivateMessageHeader h = new PrivateMessageHeader(
|
||||
message.getId(), message.getGroupId(),
|
||||
message.getTimestamp(), true, false, false, false,
|
||||
true, emptyList());
|
||||
textCache.put(message.getId(), text);
|
||||
addConversationItem(h.accept(visitor));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
|
||||
if (h == null) return;
|
||||
addConversationItem(h.accept(visitor));
|
||||
viewModel.onAddedPrivateMessageSeen();
|
||||
}
|
||||
|
||||
private void askToRemoveContact() {
|
||||
@@ -664,91 +723,70 @@ public class ConversationActivity extends BriarActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void enableIntroductionActionIfAvailable(MenuItem item) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
if (contactManager.getActiveContacts().size() > 1) {
|
||||
enableIntroductionAction(item);
|
||||
Settings settings =
|
||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||
if (settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION,
|
||||
true)) {
|
||||
showIntroductionOnboarding();
|
||||
}
|
||||
}
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
private void showImageOnboarding(@Nullable Boolean show) {
|
||||
if (show == null || !show) return;
|
||||
if (SDK_INT >= 21) {
|
||||
// show onboarding only after the enter transition has ended
|
||||
// otherwise the tap target animation won't play
|
||||
textInputView.postDelayed(this::showImageOnboarding,
|
||||
TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS);
|
||||
} else {
|
||||
showImageOnboarding();
|
||||
}
|
||||
}
|
||||
|
||||
private void enableAliasActionIfAvailable(MenuItem item) {
|
||||
observeOnce(viewModel.getContact(), this, c -> item.setEnabled(true));
|
||||
private void showImageOnboarding() {
|
||||
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS
|
||||
((TextAttachmentController) sendController)
|
||||
.showImageOnboarding(this, () ->
|
||||
viewModel.onImageOnboardingSeen());
|
||||
}
|
||||
|
||||
private void enableIntroductionAction(MenuItem item) {
|
||||
runOnUiThreadUnlessDestroyed(() -> item.setEnabled(true));
|
||||
private void showIntroductionOnboarding(@Nullable Boolean show) {
|
||||
if (show == null || !show) return;
|
||||
if (SDK_INT >= 21) {
|
||||
// show onboarding only after the enter transition has ended
|
||||
// otherwise the tap target animation won't play
|
||||
textInputView.postDelayed(this::showIntroductionOnboarding,
|
||||
TRANSITION_DURATION_MS + ONBOARDING_DELAY_MS);
|
||||
} else {
|
||||
showIntroductionOnboarding();
|
||||
}
|
||||
}
|
||||
|
||||
private void showIntroductionOnboarding() {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
// find view of overflow icon
|
||||
View target = null;
|
||||
for (int i = 0; i < toolbar.getChildCount(); i++) {
|
||||
if (toolbar.getChildAt(i) instanceof ActionMenuView) {
|
||||
ActionMenuView menu =
|
||||
(ActionMenuView) toolbar.getChildAt(i);
|
||||
target = menu.getChildAt(menu.getChildCount() - 1);
|
||||
break;
|
||||
}
|
||||
// find view of overflow icon
|
||||
View target = null;
|
||||
for (int i = 0; i < toolbar.getChildCount(); i++) {
|
||||
if (toolbar.getChildAt(i) instanceof ActionMenuView) {
|
||||
ActionMenuView menu = (ActionMenuView) toolbar.getChildAt(i);
|
||||
// The overflow icon should be the last child of the menu
|
||||
target = menu.getChildAt(menu.getChildCount() - 1);
|
||||
// If the menu hasn't been populated yet, use the menu itself
|
||||
// as the target
|
||||
if (target == null) target = menu;
|
||||
break;
|
||||
}
|
||||
if (target == null) {
|
||||
LOG.warning("No Overflow Icon found!");
|
||||
return;
|
||||
}
|
||||
if (target == null) {
|
||||
LOG.warning("No Overflow Icon found!");
|
||||
return;
|
||||
}
|
||||
|
||||
PromptStateChangeListener listener = (prompt, state) -> {
|
||||
if (state == STATE_DISMISSED || state == STATE_FINISHED) {
|
||||
viewModel.onIntroductionOnboardingSeen();
|
||||
}
|
||||
|
||||
PromptStateChangeListener listener = (prompt, state) -> {
|
||||
if (state == STATE_DISMISSED || state == STATE_FINISHED) {
|
||||
introductionOnboardingSeen();
|
||||
}
|
||||
};
|
||||
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
|
||||
R.style.OnboardingDialogTheme).setTarget(target)
|
||||
.setPrimaryText(R.string.introduction_onboarding_title)
|
||||
.setSecondaryText(R.string.introduction_onboarding_text)
|
||||
.setIcon(R.drawable.ic_more_vert_accent)
|
||||
.setPromptStateChangeListener(listener)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
private void introductionOnboardingSeen() {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Settings settings = new Settings();
|
||||
settings.putBoolean(SHOW_ONBOARDING_INTRODUCTION, false);
|
||||
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemVisible(ConversationItem item) {
|
||||
if (!item.isRead()) markMessageRead(item.getGroupId(), item.getId());
|
||||
}
|
||||
|
||||
private void markMessageRead(GroupId g, MessageId m) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.setReadFlag(g, m, true);
|
||||
logDuration(LOG, "Marking read", start);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
|
||||
R.style.OnboardingDialogTheme).setTarget(target)
|
||||
.setPrimaryText(R.string.introduction_onboarding_title)
|
||||
.setSecondaryText(R.string.introduction_onboarding_text)
|
||||
.setIcon(R.drawable.ic_more_vert_accent)
|
||||
.setBackgroundColour(
|
||||
ContextCompat.getColor(this, R.color.briar_primary))
|
||||
.setPromptStateChangeListener(listener)
|
||||
.show();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -825,19 +863,18 @@ public class ConversationActivity extends BriarActivity
|
||||
} else {
|
||||
name = getString(R.string.you);
|
||||
}
|
||||
ArrayList<AttachmentItem> attachments =
|
||||
new ArrayList<>(messageItem.getAttachments());
|
||||
Intent i = new Intent(this, ImageActivity.class);
|
||||
i.putExtra(ATTACHMENT, item);
|
||||
i.putParcelableArrayListExtra(ATTACHMENTS, attachments);
|
||||
i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item));
|
||||
i.putExtra(NAME, name);
|
||||
i.putExtra(DATE, messageItem.getTime());
|
||||
if (SDK_INT >= 23) {
|
||||
String transitionName = item.getTransitionName();
|
||||
ActivityOptionsCompat options =
|
||||
makeSceneTransitionAnimation(this, view, transitionName);
|
||||
ActivityCompat.startActivity(this, i, options.toBundle());
|
||||
} else {
|
||||
// work-around for android bug #224270
|
||||
startActivity(i);
|
||||
}
|
||||
// restoring list position should not trigger android bug #224270
|
||||
String transitionName = item.getTransitionName();
|
||||
ActivityOptionsCompat options =
|
||||
makeSceneTransitionAnimation(this, view, transitionName);
|
||||
ActivityCompat.startActivity(this, i, options.toBundle());
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView.RecycledViewPool;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -13,19 +14,27 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.BriarAdapter;
|
||||
import org.briarproject.briar.android.util.ItemReturningAdapter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
class ConversationAdapter
|
||||
extends BriarAdapter<ConversationItem, ConversationItemViewHolder> {
|
||||
extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
|
||||
implements ItemReturningAdapter<ConversationItem> {
|
||||
|
||||
private ConversationListener listener;
|
||||
private final RecycledViewPool imageViewPool;
|
||||
private final ImageItemDecoration imageItemDecoration;
|
||||
|
||||
ConversationAdapter(Context ctx,
|
||||
ConversationListener conversationListener) {
|
||||
super(ctx, ConversationItem.class);
|
||||
listener = conversationListener;
|
||||
// This shares the same pool for view recycling between all image lists
|
||||
imageViewPool = new RecycledViewPool();
|
||||
// Share the item decoration as well
|
||||
imageItemDecoration = new ImageItemDecoration(ctx);
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
@@ -42,15 +51,17 @@ class ConversationAdapter
|
||||
type, viewGroup, false);
|
||||
switch (type) {
|
||||
case R.layout.list_item_conversation_msg_in:
|
||||
return new ConversationMessageViewHolder(v, true);
|
||||
return new ConversationMessageViewHolder(v, listener, true,
|
||||
imageViewPool, imageItemDecoration);
|
||||
case R.layout.list_item_conversation_msg_out:
|
||||
return new ConversationMessageViewHolder(v, false);
|
||||
return new ConversationMessageViewHolder(v, listener, false,
|
||||
imageViewPool, imageItemDecoration);
|
||||
case R.layout.list_item_conversation_notice_in:
|
||||
return new ConversationNoticeViewHolder(v, true);
|
||||
return new ConversationNoticeViewHolder(v, listener, true);
|
||||
case R.layout.list_item_conversation_notice_out:
|
||||
return new ConversationNoticeViewHolder(v, false);
|
||||
return new ConversationNoticeViewHolder(v, listener, false);
|
||||
case R.layout.list_item_conversation_request:
|
||||
return new ConversationRequestViewHolder(v, true);
|
||||
return new ConversationRequestViewHolder(v, listener, true);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown ConversationItem");
|
||||
}
|
||||
@@ -59,8 +70,7 @@ class ConversationAdapter
|
||||
@Override
|
||||
public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
|
||||
ConversationItem item = items.get(position);
|
||||
ui.bind(item, listener);
|
||||
listener.onItemVisible(item);
|
||||
ui.bind(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -69,6 +69,10 @@ abstract class ConversationItem {
|
||||
return read;
|
||||
}
|
||||
|
||||
void markRead() {
|
||||
read = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only useful for outgoing messages.
|
||||
*/
|
||||
|
||||
@@ -18,14 +18,17 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate;
|
||||
@NotNullByDefault
|
||||
abstract class ConversationItemViewHolder extends ViewHolder {
|
||||
|
||||
protected final ConversationListener listener;
|
||||
protected final ConstraintLayout layout;
|
||||
@Nullable
|
||||
private final OutItemViewHolder outViewHolder;
|
||||
private final TextView text;
|
||||
protected final TextView time;
|
||||
|
||||
ConversationItemViewHolder(View v, boolean isIncoming) {
|
||||
ConversationItemViewHolder(View v, ConversationListener listener,
|
||||
boolean isIncoming) {
|
||||
super(v);
|
||||
this.listener = listener;
|
||||
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
|
||||
layout = v.findViewById(R.id.layout);
|
||||
text = v.findViewById(R.id.text);
|
||||
@@ -33,7 +36,7 @@ abstract class ConversationItemViewHolder extends ViewHolder {
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
void bind(ConversationItem item, ConversationListener listener) {
|
||||
void bind(ConversationItem item) {
|
||||
if (item.getText() != null) {
|
||||
text.setText(trim(item.getText()));
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
@NotNullByDefault
|
||||
interface ConversationListener {
|
||||
|
||||
void onItemVisible(ConversationItem item);
|
||||
|
||||
void respondToRequest(ConversationRequestItem item, boolean accept);
|
||||
|
||||
void openRequestedShareable(ConversationRequestItem item);
|
||||
|
||||
@@ -1,63 +1,45 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.constraint.ConstraintSet;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.RecycledViewPool;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.load.Transformation;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
import static android.support.constraint.ConstraintSet.WRAP_CONTENT;
|
||||
import static android.support.v4.content.ContextCompat.getColor;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
class ConversationMessageViewHolder extends ConversationItemViewHolder {
|
||||
|
||||
@DrawableRes
|
||||
private static final int ERROR_RES = R.drawable.ic_image_broken;
|
||||
|
||||
private final ImageView imageView;
|
||||
private final ImageAdapter adapter;
|
||||
private final ViewGroup statusLayout;
|
||||
private final int timeColor, timeColorBubble;
|
||||
private final int radiusBig, radiusSmall;
|
||||
private final boolean isRtl;
|
||||
private final ConstraintSet textConstraints = new ConstraintSet();
|
||||
private final ConstraintSet imageConstraints = new ConstraintSet();
|
||||
private final ConstraintSet imageTextConstraints = new ConstraintSet();
|
||||
|
||||
ConversationMessageViewHolder(View v, boolean isIncoming) {
|
||||
super(v, isIncoming);
|
||||
imageView = v.findViewById(R.id.imageView);
|
||||
ConversationMessageViewHolder(View v, ConversationListener listener,
|
||||
boolean isIncoming, RecycledViewPool imageViewPool,
|
||||
ImageItemDecoration imageItemDecoration) {
|
||||
super(v, listener, isIncoming);
|
||||
statusLayout = v.findViewById(R.id.statusLayout);
|
||||
radiusBig = v.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
|
||||
radiusSmall = v.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
|
||||
|
||||
// image list
|
||||
RecyclerView list = v.findViewById(R.id.imageList);
|
||||
list.setRecycledViewPool(imageViewPool);
|
||||
adapter = new ImageAdapter(v.getContext(), listener);
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(imageItemDecoration);
|
||||
|
||||
// remember original status text color
|
||||
timeColor = time.getCurrentTextColor();
|
||||
timeColorBubble =
|
||||
ContextCompat.getColor(v.getContext(), R.color.briar_white);
|
||||
|
||||
// find out if we are showing a RTL language, Use the configuration,
|
||||
// because getting the layout direction of views is not reliable
|
||||
Configuration config =
|
||||
imageView.getContext().getResources().getConfiguration();
|
||||
isRtl = SDK_INT >= 17 &&
|
||||
config.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
|
||||
timeColorBubble = getColor(v.getContext(), R.color.briar_white);
|
||||
|
||||
// clone constraint sets from layout files
|
||||
textConstraints
|
||||
@@ -77,85 +59,55 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(ConversationItem conversationItem,
|
||||
ConversationListener listener) {
|
||||
super.bind(conversationItem, listener);
|
||||
void bind(ConversationItem conversationItem) {
|
||||
super.bind(conversationItem);
|
||||
ConversationMessageItem item =
|
||||
(ConversationMessageItem) conversationItem;
|
||||
if (item.getAttachments().isEmpty()) {
|
||||
bindTextItem();
|
||||
} else {
|
||||
bindImageItem(item, listener);
|
||||
bindImageItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindTextItem() {
|
||||
clearImage();
|
||||
resetStatusLayoutForText();
|
||||
textConstraints.applyTo(layout);
|
||||
adapter.clear();
|
||||
}
|
||||
|
||||
private void bindImageItem(ConversationMessageItem item) {
|
||||
ConstraintSet constraintSet;
|
||||
if (item.getText() == null) {
|
||||
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
|
||||
time.setTextColor(timeColorBubble);
|
||||
constraintSet = imageConstraints;
|
||||
} else {
|
||||
resetStatusLayoutForText();
|
||||
constraintSet = imageTextConstraints;
|
||||
}
|
||||
|
||||
if (item.getAttachments().size() == 1) {
|
||||
// apply image size constraints for a single image
|
||||
AttachmentItem attachment = item.getAttachments().get(0);
|
||||
int width = attachment.getThumbnailWidth();
|
||||
int height = attachment.getThumbnailHeight();
|
||||
constraintSet.constrainWidth(R.id.imageList, width);
|
||||
constraintSet.constrainHeight(R.id.imageList, height);
|
||||
} else {
|
||||
// bubble adapts to size of image list
|
||||
constraintSet.constrainWidth(R.id.imageList, WRAP_CONTENT);
|
||||
constraintSet.constrainHeight(R.id.imageList, WRAP_CONTENT);
|
||||
}
|
||||
constraintSet.applyTo(layout);
|
||||
adapter.setConversationItem(item);
|
||||
}
|
||||
|
||||
private void resetStatusLayoutForText() {
|
||||
statusLayout.setBackgroundResource(0);
|
||||
// also reset padding (the background drawable defines some)
|
||||
statusLayout.setPadding(0, 0, 0, 0);
|
||||
time.setTextColor(timeColor);
|
||||
textConstraints.applyTo(layout);
|
||||
}
|
||||
|
||||
private void bindImageItem(ConversationMessageItem item,
|
||||
ConversationListener listener) {
|
||||
// TODO show more than just the first image
|
||||
AttachmentItem attachment = item.getAttachments().get(0);
|
||||
|
||||
ConstraintSet constraintSet;
|
||||
if (item.getText() == null) {
|
||||
statusLayout
|
||||
.setBackgroundResource(R.drawable.msg_status_bubble);
|
||||
time.setTextColor(timeColorBubble);
|
||||
constraintSet = imageConstraints;
|
||||
} else {
|
||||
statusLayout.setBackgroundResource(0);
|
||||
// also reset padding (the background drawable defines some)
|
||||
statusLayout.setPadding(0, 0, 0, 0);
|
||||
time.setTextColor(timeColor);
|
||||
constraintSet = imageTextConstraints;
|
||||
}
|
||||
|
||||
// apply image size constraints, so glides picks them up for scaling
|
||||
int width = attachment.getThumbnailWidth();
|
||||
int height = attachment.getThumbnailHeight();
|
||||
constraintSet.constrainWidth(R.id.imageView, width);
|
||||
constraintSet.constrainHeight(R.id.imageView, height);
|
||||
constraintSet.applyTo(layout);
|
||||
|
||||
if (attachment.hasError()) {
|
||||
clearImage();
|
||||
imageView.setImageResource(ERROR_RES);
|
||||
} else {
|
||||
loadImage(item, attachment, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearImage() {
|
||||
GlideApp.with(imageView)
|
||||
.clear(imageView);
|
||||
imageView.setOnClickListener(null);
|
||||
}
|
||||
|
||||
private void loadImage(ConversationMessageItem item,
|
||||
AttachmentItem attachment, ConversationListener listener) {
|
||||
boolean leftCornerSmall =
|
||||
(isIncoming() && !isRtl) || (!isIncoming() && isRtl);
|
||||
boolean bottomRound = item.getText() == null;
|
||||
Transformation<Bitmap> transformation = new BriarImageTransformation(
|
||||
radiusSmall, radiusBig, leftCornerSmall, bottomRound);
|
||||
|
||||
GlideApp.with(imageView)
|
||||
.load(attachment)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(ERROR_RES)
|
||||
.transform(transformation)
|
||||
.transition(withCrossFade())
|
||||
.into(imageView)
|
||||
.waitForLayout();
|
||||
imageView.setOnClickListener(
|
||||
view -> listener.onAttachmentClicked(view, item, attachment));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,16 +19,17 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder {
|
||||
|
||||
private final TextView msgText;
|
||||
|
||||
ConversationNoticeViewHolder(View v, boolean isIncoming) {
|
||||
super(v, isIncoming);
|
||||
ConversationNoticeViewHolder(View v, ConversationListener listener,
|
||||
boolean isIncoming) {
|
||||
super(v, listener, isIncoming);
|
||||
msgText = v.findViewById(R.id.msgText);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
void bind(ConversationItem item, ConversationListener listener) {
|
||||
void bind(ConversationItem item) {
|
||||
ConversationNoticeItem notice = (ConversationNoticeItem) item;
|
||||
super.bind(notice, listener);
|
||||
super.bind(notice);
|
||||
|
||||
String text = notice.getMsgText();
|
||||
if (isNullOrEmpty(text)) {
|
||||
|
||||
@@ -17,16 +17,17 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder {
|
||||
private final Button acceptButton;
|
||||
private final Button declineButton;
|
||||
|
||||
ConversationRequestViewHolder(View v, boolean isIncoming) {
|
||||
super(v, isIncoming);
|
||||
ConversationRequestViewHolder(View v, ConversationListener listener,
|
||||
boolean isIncoming) {
|
||||
super(v, listener, isIncoming);
|
||||
acceptButton = v.findViewById(R.id.acceptButton);
|
||||
declineButton = v.findViewById(R.id.declineButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(ConversationItem item, ConversationListener listener) {
|
||||
void bind(ConversationItem item) {
|
||||
ConversationRequestItem request = (ConversationRequestItem) item;
|
||||
super.bind(request, listener);
|
||||
super.bind(request);
|
||||
|
||||
if (request.wasAnswered() && request.canBeOpened()) {
|
||||
acceptButton.setVisibility(VISIBLE);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.android.view.BriarRecyclerViewScrollListener;
|
||||
|
||||
@NotNullByDefault
|
||||
class ConversationScrollListener extends
|
||||
BriarRecyclerViewScrollListener<ConversationAdapter, ConversationItem> {
|
||||
|
||||
private final ConversationViewModel viewModel;
|
||||
|
||||
protected ConversationScrollListener(ConversationAdapter adapter,
|
||||
ConversationViewModel viewModel) {
|
||||
super(adapter);
|
||||
this.viewModel = viewModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onItemVisible(ConversationItem item) {
|
||||
if (!item.isRead()) {
|
||||
viewModel.markMessageRead(item.getGroupId(), item.getId());
|
||||
item.markRead();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,19 +5,41 @@ import android.arch.lifecycle.AndroidViewModel;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.Transformations;
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessage;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -26,19 +48,34 @@ import javax.inject.Inject;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.briar.android.conversation.AttachmentDimensions.getAttachmentDimensions;
|
||||
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
|
||||
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ConversationViewModel extends AndroidViewModel {
|
||||
|
||||
private static Logger LOG =
|
||||
getLogger(ConversationViewModel.class.getName());
|
||||
private static final String SHOW_ONBOARDING_IMAGE =
|
||||
"showOnboardingImage";
|
||||
private static final String SHOW_ONBOARDING_INTRODUCTION =
|
||||
"showOnboardingIntroduction";
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
@CryptoExecutor
|
||||
private final Executor cryptoExecutor;
|
||||
// TODO replace with TransactionManager once it exists
|
||||
private final DatabaseComponent db;
|
||||
private final MessagingManager messagingManager;
|
||||
private final ContactManager contactManager;
|
||||
private final SettingsManager settingsManager;
|
||||
private final PrivateMessageFactory privateMessageFactory;
|
||||
private final AttachmentController attachmentController;
|
||||
|
||||
@Nullable
|
||||
@@ -48,38 +85,64 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
Transformations.map(contact, c -> c.getAuthor().getId());
|
||||
private final LiveData<String> contactName =
|
||||
Transformations.map(contact, UiUtils::getContactDisplayName);
|
||||
private final MutableLiveData<Boolean> imageSupport =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> showImageOnboarding =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> showIntroductionOnboarding =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> showIntroductionAction =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> contactDeleted =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<GroupId> messagingGroupId =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<PrivateMessageHeader> addedHeader =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
ConversationViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
ContactManager contactManager, MessagingManager messagingManager) {
|
||||
@CryptoExecutor Executor cryptoExecutor, DatabaseComponent db,
|
||||
MessagingManager messagingManager, ContactManager contactManager,
|
||||
SettingsManager settingsManager,
|
||||
PrivateMessageFactory privateMessageFactory) {
|
||||
super(application);
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
this.db = db;
|
||||
this.messagingManager = messagingManager;
|
||||
this.contactManager = contactManager;
|
||||
this.settingsManager = settingsManager;
|
||||
this.privateMessageFactory = privateMessageFactory;
|
||||
this.attachmentController = new AttachmentController(messagingManager,
|
||||
application.getResources());
|
||||
getAttachmentDimensions(application.getResources()));
|
||||
contactDeleted.setValue(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting the {@link ContactId} automatically triggers loading of other
|
||||
* data.
|
||||
*/
|
||||
void setContactId(ContactId contactId) {
|
||||
if (this.contactId == null) {
|
||||
this.contactId = contactId;
|
||||
loadContact();
|
||||
loadContact(contactId);
|
||||
} else if (!contactId.equals(this.contactId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadContact() {
|
||||
private void loadContact(ContactId contactId) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
Contact c =
|
||||
contactManager.getContact(requireNonNull(contactId));
|
||||
Contact c = contactManager.getContact(contactId);
|
||||
contact.postValue(c);
|
||||
logDuration(LOG, "Loading contact", start);
|
||||
start = now();
|
||||
checkFeaturesAndOnboarding(contactId);
|
||||
logDuration(LOG, "Checking for image support", start);
|
||||
} catch (NoSuchContactException e) {
|
||||
contactDeleted.postValue(true);
|
||||
} catch (DbException e) {
|
||||
@@ -88,18 +151,196 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
void markMessageRead(GroupId g, MessageId m) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.setReadFlag(g, m, true);
|
||||
logDuration(LOG, "Marking read", start);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setContactAlias(String alias) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
contactManager.setContactAlias(requireNonNull(contactId),
|
||||
alias.isEmpty() ? null : alias);
|
||||
loadContact();
|
||||
loadContact(contactId);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) {
|
||||
if (messagingGroupId.getValue() == null) loadGroupId();
|
||||
observeForeverOnce(messagingGroupId, groupId -> {
|
||||
if (groupId == null) return;
|
||||
// calls through to creating and storing the message
|
||||
storeAttachments(groupId, text, uris, timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadGroupId() {
|
||||
if (contactId == null) throw new IllegalStateException();
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
messagingGroupId.postValue(
|
||||
messagingManager.getConversationId(contactId));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
|
||||
// check if images are supported
|
||||
boolean imagesSupported = db.transactionWithResult(true, txn ->
|
||||
messagingManager.contactSupportsImages(txn, c));
|
||||
imageSupport.postValue(imagesSupported);
|
||||
|
||||
// check if introductions are supported
|
||||
Collection<Contact> contacts = contactManager.getActiveContacts();
|
||||
boolean introductionSupported = contacts.size() > 1;
|
||||
showIntroductionAction.postValue(introductionSupported);
|
||||
|
||||
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||
if (imagesSupported &&
|
||||
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
|
||||
// check if we should show onboarding, only if images are supported
|
||||
showImageOnboarding.postValue(true);
|
||||
// allow observer to stop listening for changes
|
||||
showIntroductionOnboarding.postValue(false);
|
||||
} else {
|
||||
// allow observer to stop listening for changes
|
||||
showImageOnboarding.postValue(false);
|
||||
// we only show one onboarding dialog at a time
|
||||
if (introductionSupported &&
|
||||
settings.getBoolean(SHOW_ONBOARDING_INTRODUCTION, true)) {
|
||||
showIntroductionOnboarding.postValue(true);
|
||||
} else {
|
||||
// allow observer to stop listening for changes
|
||||
showIntroductionOnboarding.postValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onImageOnboardingSeen() {
|
||||
onOnboardingSeen(SHOW_ONBOARDING_IMAGE);
|
||||
}
|
||||
|
||||
void onIntroductionOnboardingSeen() {
|
||||
onOnboardingSeen(SHOW_ONBOARDING_INTRODUCTION);
|
||||
}
|
||||
|
||||
private void onOnboardingSeen(String key) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
Settings settings = new Settings();
|
||||
settings.putBoolean(key, false);
|
||||
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeAttachments(GroupId groupId, @Nullable String text,
|
||||
List<Uri> uris, long timestamp) {
|
||||
dbExecutor.execute(() -> {
|
||||
long start = now();
|
||||
List<AttachmentHeader> attachments = new ArrayList<>();
|
||||
List<AttachmentItem> items = new ArrayList<>();
|
||||
boolean needsSize = uris.size() == 1;
|
||||
for (Uri uri : uris) {
|
||||
Pair<AttachmentHeader, AttachmentItem> pair =
|
||||
createAttachmentHeader(groupId, uri, timestamp,
|
||||
needsSize);
|
||||
if (pair == null) continue;
|
||||
attachments.add(pair.getFirst());
|
||||
items.add(pair.getSecond());
|
||||
}
|
||||
logDuration(LOG, "Storing attachments", start);
|
||||
createMessage(groupId, text, attachments, items, timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@DatabaseExecutor
|
||||
private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
|
||||
GroupId groupId, Uri uri, long timestamp, boolean needsSize) {
|
||||
InputStream is = null;
|
||||
try {
|
||||
ContentResolver contentResolver =
|
||||
getApplication().getContentResolver();
|
||||
is = contentResolver.openInputStream(uri);
|
||||
if (is == null) throw new IOException();
|
||||
String contentType = contentResolver.getType(uri);
|
||||
if (contentType == null) throw new IOException("null content type");
|
||||
AttachmentHeader h = messagingManager
|
||||
.addLocalAttachment(groupId, timestamp, contentType, is);
|
||||
is.close();
|
||||
// re-open stream to get AttachmentItem
|
||||
is = contentResolver.openInputStream(uri);
|
||||
if (is == null) throw new IOException();
|
||||
AttachmentItem item = attachmentController
|
||||
.getAttachmentItem(h, new Attachment(is), needsSize);
|
||||
return new Pair<>(h, item);
|
||||
} catch (DbException | IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return null;
|
||||
} finally {
|
||||
if (is != null) tryToClose(is, LOG, WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
private void createMessage(GroupId groupId, @Nullable String text,
|
||||
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
|
||||
long timestamp) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
try {
|
||||
// TODO remove when text can be null in the backend
|
||||
String msgText = text == null ? "null" : text;
|
||||
PrivateMessage pm = privateMessageFactory
|
||||
.createPrivateMessage(groupId, timestamp, msgText,
|
||||
attachments);
|
||||
attachmentController.put(pm.getMessage().getId(), aItems);
|
||||
storeMessage(pm, msgText, attachments);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeMessage(PrivateMessage m, @Nullable String text,
|
||||
List<AttachmentHeader> attachments) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.addLocalMessage(m);
|
||||
logDuration(LOG, "Storing message", start);
|
||||
Message message = m.getMessage();
|
||||
PrivateMessageHeader h = new PrivateMessageHeader(
|
||||
message.getId(), message.getGroupId(),
|
||||
message.getTimestamp(), true, true, false, false,
|
||||
text != null, attachments);
|
||||
// TODO add text to cache when available here
|
||||
addedHeader.postValue(h);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void onAddedPrivateMessageSeen() {
|
||||
addedHeader.setValue(null);
|
||||
}
|
||||
|
||||
AttachmentController getAttachmentController() {
|
||||
return attachmentController;
|
||||
}
|
||||
@@ -116,8 +357,28 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
return contactName;
|
||||
}
|
||||
|
||||
LiveData<Boolean> hasImageSupport() {
|
||||
return imageSupport;
|
||||
}
|
||||
|
||||
LiveData<Boolean> showImageOnboarding() {
|
||||
return showImageOnboarding;
|
||||
}
|
||||
|
||||
LiveData<Boolean> showIntroductionOnboarding() {
|
||||
return showIntroductionOnboarding;
|
||||
}
|
||||
|
||||
LiveData<Boolean> showIntroductionAction() {
|
||||
return showIntroductionAction;
|
||||
}
|
||||
|
||||
LiveData<Boolean> isContactDeleted() {
|
||||
return contactDeleted;
|
||||
}
|
||||
|
||||
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
|
||||
return addedHeader;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,54 +1,84 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.graphics.drawable.DrawableCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AlertDialog.Builder;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.transition.Fade;
|
||||
import android.transition.Transition;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
import org.briarproject.briar.android.view.PullDownLayout;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.content.Intent.ACTION_CREATE_DOCUMENT;
|
||||
import static android.content.Intent.CATEGORY_OPENABLE;
|
||||
import static android.content.Intent.EXTRA_TITLE;
|
||||
import static android.graphics.Color.TRANSPARENT;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.support.design.widget.Snackbar.LENGTH_LONG;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
|
||||
import static android.widget.ImageView.ScaleType.FIT_START;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT;
|
||||
import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ImageActivity extends BriarActivity
|
||||
implements PullDownLayout.Callback {
|
||||
implements PullDownLayout.Callback, OnGlobalLayoutListener {
|
||||
|
||||
final static String ATTACHMENT = "attachment";
|
||||
final static String ATTACHMENTS = "attachments";
|
||||
final static String ATTACHMENT_POSITION = "position";
|
||||
final static String NAME = "name";
|
||||
final static String DATE = "date";
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private final static int UI_FLAGS_DEFAULT =
|
||||
SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private ImageViewModel viewModel;
|
||||
private PullDownLayout layout;
|
||||
private AppBarLayout appBarLayout;
|
||||
private PhotoView photoView;
|
||||
private ViewPager viewPager;
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
@@ -60,18 +90,23 @@ public class ImageActivity extends BriarActivity
|
||||
super.onCreate(state);
|
||||
|
||||
// Transitions
|
||||
supportPostponeEnterTransition();
|
||||
if (state == null) supportPostponeEnterTransition();
|
||||
Window window = getWindow();
|
||||
if (SDK_INT >= 21) {
|
||||
Transition transition = new Fade();
|
||||
setSceneTransitionAnimation(transition, null, transition);
|
||||
}
|
||||
|
||||
// get View Model
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)
|
||||
.get(ImageViewModel.class);
|
||||
viewModel.getSaveState().observe(this, this::onImageSaveStateChanged);
|
||||
|
||||
// inflate layout
|
||||
setContentView(R.layout.activity_image);
|
||||
layout = findViewById(R.id.layout);
|
||||
layout.getBackground().setAlpha(255);
|
||||
layout.setCallback(this);
|
||||
layout.getViewTreeObserver().addOnGlobalLayoutListener(this);
|
||||
|
||||
// Status Bar
|
||||
if (SDK_INT >= 21) {
|
||||
@@ -88,59 +123,33 @@ public class ImageActivity extends BriarActivity
|
||||
TextView dateView = toolbar.findViewById(R.id.dateView);
|
||||
|
||||
// Intent Extras
|
||||
AttachmentItem attachment = getIntent().getParcelableExtra(ATTACHMENT);
|
||||
String name = getIntent().getStringExtra(NAME);
|
||||
long time = getIntent().getLongExtra(DATE, 0);
|
||||
Intent i = getIntent();
|
||||
attachments = i.getParcelableArrayListExtra(ATTACHMENTS);
|
||||
int position = i.getIntExtra(ATTACHMENT_POSITION, -1);
|
||||
if (position == -1) throw new IllegalStateException();
|
||||
String name = i.getStringExtra(NAME);
|
||||
long time = i.getLongExtra(DATE, 0);
|
||||
String date = formatDateAbsolute(this, time);
|
||||
contactName.setText(name);
|
||||
dateView.setText(date);
|
||||
|
||||
// Image View
|
||||
photoView = findViewById(R.id.photoView);
|
||||
// Set up image ViewPager
|
||||
viewPager = findViewById(R.id.viewPager);
|
||||
ImagePagerAdapter pagerAdapter =
|
||||
new ImagePagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
viewPager.setCurrentItem(position);
|
||||
|
||||
if (SDK_INT >= 16) {
|
||||
photoView.setOnClickListener(view -> toggleSystemUi());
|
||||
window.getDecorView().setSystemUiVisibility(
|
||||
SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
viewModel.getOnImageClicked().observe(this, this::onImageClicked);
|
||||
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
// Request Listener
|
||||
RequestListener<Drawable> listener = new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e,
|
||||
Object model, Target<Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
supportStartPostponedEnterTransition();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model,
|
||||
Target<Drawable> target, DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
|
||||
// set transition name only when not animatable,
|
||||
// because the animation won't start otherwise
|
||||
photoView.setTransitionName(
|
||||
attachment.getTransitionName());
|
||||
}
|
||||
// Move image to the top if overlapping toolbar
|
||||
if (isOverlappingToolbar(resource)) {
|
||||
photoView.setScaleType(FIT_START);
|
||||
}
|
||||
supportStartPostponedEnterTransition();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load Image
|
||||
GlideApp.with(this)
|
||||
.load(attachment)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(R.drawable.ic_image_broken)
|
||||
.dontTransform()
|
||||
.addListener(listener)
|
||||
.into(photoView);
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.image_actions, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,11 +158,36 @@ public class ImageActivity extends BriarActivity
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.action_save_image:
|
||||
showSaveImageDialog();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
viewModel.setToolbarPosition(
|
||||
appBarLayout.getTop(), appBarLayout.getBottom()
|
||||
);
|
||||
if (SDK_INT >= 16) {
|
||||
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result,
|
||||
@Nullable Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK &&
|
||||
data != null) {
|
||||
viewModel.saveImage(getVisibleAttachment(), data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPullStart() {
|
||||
appBarLayout.animate()
|
||||
@@ -163,7 +197,6 @@ public class ImageActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onPull(float progress) {
|
||||
layout.getBackground().setAlpha(Math.round((1 - progress) * 255));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,9 +208,24 @@ public class ImageActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onPullComplete() {
|
||||
showStatusBarBeforeFinishing();
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
showStatusBarBeforeFinishing();
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void onImageClicked(@Nullable Boolean clicked) {
|
||||
if (clicked != null && clicked) {
|
||||
toggleSystemUi();
|
||||
viewModel.onOnImageClickSeen();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void toggleSystemUi() {
|
||||
View decorView = getWindow().getDecorView();
|
||||
@@ -190,10 +238,8 @@ public class ImageActivity extends BriarActivity
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void hideSystemUi(View decorView) {
|
||||
decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| SYSTEM_UI_FLAG_FULLSCREEN
|
||||
);
|
||||
decorView.setSystemUiVisibility(
|
||||
SYSTEM_UI_FLAG_FULLSCREEN | UI_FLAGS_DEFAULT);
|
||||
appBarLayout.animate()
|
||||
.translationYBy(-1 * appBarLayout.getHeight())
|
||||
.alpha(0f)
|
||||
@@ -203,10 +249,7 @@ public class ImageActivity extends BriarActivity
|
||||
|
||||
@RequiresApi(api = 16)
|
||||
private void showSystemUi(View decorView) {
|
||||
decorView.setSystemUiVisibility(
|
||||
SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
);
|
||||
decorView.setSystemUiVisibility(UI_FLAGS_DEFAULT);
|
||||
appBarLayout.animate()
|
||||
.translationYBy(appBarLayout.getHeight())
|
||||
.alpha(1f)
|
||||
@@ -214,20 +257,88 @@ public class ImageActivity extends BriarActivity
|
||||
.start();
|
||||
}
|
||||
|
||||
private boolean isOverlappingToolbar(Drawable drawable) {
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
float widthPercentage = photoView.getWidth() / (float) width;
|
||||
float heightPercentage = photoView.getHeight() / (float) height;
|
||||
float scaleFactor = Math.min(widthPercentage, heightPercentage);
|
||||
int realWidth = (int) (width * scaleFactor);
|
||||
int realHeight = (int) (height * scaleFactor);
|
||||
// return if photo doesn't use the full width,
|
||||
// because it will be moved to the right otherwise
|
||||
if (realWidth < photoView.getWidth()) return false;
|
||||
int drawableTop = (photoView.getHeight() - realHeight) / 2;
|
||||
return drawableTop < appBarLayout.getBottom() &&
|
||||
drawableTop != appBarLayout.getTop();
|
||||
/**
|
||||
* If we don't show the status bar again before finishing this activity,
|
||||
* the return transition will "jump" down the size of the status bar
|
||||
* when the previous activity (with visible status bar) is shown.
|
||||
*/
|
||||
private void showStatusBarBeforeFinishing() {
|
||||
if (SDK_INT >= 16 && appBarLayout.getVisibility() == GONE) {
|
||||
View decorView = getWindow().getDecorView();
|
||||
decorView.setSystemUiVisibility(UI_FLAGS_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSaveImageDialog() {
|
||||
OnClickListener okListener = (dialog, which) -> {
|
||||
if (SDK_INT >= 19) {
|
||||
Intent intent = getCreationIntent();
|
||||
startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT);
|
||||
} else {
|
||||
viewModel.saveImage(getVisibleAttachment());
|
||||
}
|
||||
};
|
||||
Builder builder = new Builder(this, R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.dialog_title_save_image));
|
||||
builder.setMessage(getString(R.string.dialog_message_save_image));
|
||||
Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_security);
|
||||
DrawableCompat.setTint(requireNonNull(icon),
|
||||
ContextCompat.getColor(this, R.color.color_primary));
|
||||
builder.setIcon(icon);
|
||||
builder.setPositiveButton(R.string.save_image, okListener);
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@RequiresApi(api = 19)
|
||||
private Intent getCreationIntent() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
|
||||
Locale.getDefault());
|
||||
String fileName = sdf.format(new Date());
|
||||
Intent intent = new Intent(ACTION_CREATE_DOCUMENT);
|
||||
intent.addCategory(CATEGORY_OPENABLE);
|
||||
intent.setType(getVisibleAttachment().getMimeType());
|
||||
intent.putExtra(EXTRA_TITLE, fileName);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private void onImageSaveStateChanged(@Nullable Boolean error) {
|
||||
if (error == null) return;
|
||||
int stringRes = error ?
|
||||
R.string.save_image_error : R.string.save_image_success;
|
||||
int colorRes = error ?
|
||||
R.color.briar_red : R.color.briar_primary;
|
||||
Snackbar s = Snackbar.make(layout, stringRes, LENGTH_LONG);
|
||||
s.getView().setBackgroundResource(colorRes);
|
||||
s.show();
|
||||
viewModel.onSaveStateSeen();
|
||||
}
|
||||
|
||||
AttachmentItem getVisibleAttachment() {
|
||||
return attachments.get(viewPager.getCurrentItem());
|
||||
}
|
||||
|
||||
private class ImagePagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private boolean isFirst = true;
|
||||
|
||||
private ImagePagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment f = ImageFragment
|
||||
.newInstance(attachments.get(position), isFirst);
|
||||
isFirst = false;
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return attachments.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView.Adapter;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.glide.Radii;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static android.content.Context.WINDOW_SERVICE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isRtl;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImageAdapter extends Adapter<ImageViewHolder> {
|
||||
|
||||
private final List<AttachmentItem> items = new ArrayList<>();
|
||||
private final ConversationListener listener;
|
||||
private final int imageSize;
|
||||
private final int radiusBig, radiusSmall;
|
||||
private final boolean isRtl;
|
||||
@Nullable
|
||||
private ConversationMessageItem conversationItem;
|
||||
|
||||
ImageAdapter(Context ctx, ConversationListener listener) {
|
||||
this.listener = listener;
|
||||
imageSize = getImageSize(ctx);
|
||||
Resources res = ctx.getResources();
|
||||
radiusBig =
|
||||
res.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
|
||||
radiusSmall =
|
||||
res.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
|
||||
isRtl = isRtl(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
|
||||
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||
R.layout.list_item_image, viewGroup, false);
|
||||
return new ImageViewHolder(v, imageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ImageViewHolder imageViewHolder,
|
||||
int position) {
|
||||
// get item
|
||||
requireNonNull(conversationItem);
|
||||
AttachmentItem item = items.get(position);
|
||||
// set onClick listener
|
||||
imageViewHolder.itemView.setOnClickListener(v ->
|
||||
listener.onAttachmentClicked(v, conversationItem, item)
|
||||
);
|
||||
// bind view holder
|
||||
int size = items.size();
|
||||
boolean isIncoming = conversationItem.isIncoming();
|
||||
boolean hasText = conversationItem.getText() != null;
|
||||
Radii r = getRadii(position, size, isIncoming, hasText);
|
||||
imageViewHolder.bind(item, r, size == 1, singleInRow(position, size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
void setConversationItem(ConversationMessageItem item) {
|
||||
this.conversationItem = item;
|
||||
this.items.clear();
|
||||
this.items.addAll(item.getAttachments());
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private int getImageSize(Context ctx) {
|
||||
Resources res = ctx.getResources();
|
||||
WindowManager windowManager =
|
||||
(WindowManager) ctx.getSystemService(WINDOW_SERVICE);
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
if (windowManager == null) {
|
||||
return res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_default);
|
||||
}
|
||||
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
|
||||
int imageSize = displayMetrics.widthPixels / 3;
|
||||
int maxSize = res.getDimensionPixelSize(
|
||||
R.dimen.message_bubble_image_max_width);
|
||||
return Math.min(imageSize, maxSize);
|
||||
}
|
||||
|
||||
private Radii getRadii(int pos, int num, boolean isIncoming,
|
||||
boolean hasText) {
|
||||
boolean left = isLeft(pos);
|
||||
boolean single = num == 1;
|
||||
// Top Row
|
||||
int topLeft;
|
||||
int topRight;
|
||||
if (single) {
|
||||
topLeft = isIncoming ? radiusSmall : radiusBig;
|
||||
topRight = !isIncoming ? radiusSmall : radiusBig;
|
||||
} else if (isTopRow(pos)) {
|
||||
topLeft = left ? (isIncoming ? radiusSmall : radiusBig) : 0;
|
||||
topRight = !left ? (!isIncoming ? radiusSmall : radiusBig) : 0;
|
||||
} else {
|
||||
topLeft = 0;
|
||||
topRight = 0;
|
||||
}
|
||||
// Bottom Row
|
||||
boolean singleInRow = singleInRow(pos, num);
|
||||
int bottomLeft;
|
||||
int bottomRight;
|
||||
if (!hasText && isBottomRow(pos, num)) {
|
||||
bottomLeft = singleInRow || left ? radiusBig : 0;
|
||||
bottomRight = singleInRow || !left ? radiusBig : 0;
|
||||
} else {
|
||||
bottomLeft = 0;
|
||||
bottomRight = 0;
|
||||
}
|
||||
if (isRtl) return new Radii(topRight, topLeft, bottomRight, bottomLeft);
|
||||
return new Radii(topLeft, topRight, bottomLeft, bottomRight);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static boolean isTopRow(int pos) {
|
||||
return pos < 2;
|
||||
}
|
||||
|
||||
static boolean isLeft(int pos) {
|
||||
return pos % 2 == 0;
|
||||
}
|
||||
|
||||
static boolean isBottomRow(int pos, int num) {
|
||||
return num % 2 == 0 ?
|
||||
pos >= num - 2 : // last two, if even
|
||||
pos > num - 2; // last one, if odd
|
||||
}
|
||||
|
||||
static boolean singleInRow(int pos, int num) {
|
||||
// last item of an odd number
|
||||
return num % 2 != 0 && pos == num -1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.widget.ImageView.ScaleType.FIT_START;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersAreNonnullByDefault
|
||||
public class ImageFragment extends Fragment {
|
||||
|
||||
private final static String IS_FIRST = "isFirst";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private AttachmentItem attachment;
|
||||
private boolean isFirst;
|
||||
private ImageViewModel viewModel;
|
||||
private PhotoView photoView;
|
||||
|
||||
static ImageFragment newInstance(AttachmentItem a, boolean isFirst) {
|
||||
ImageFragment f = new ImageFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ATTACHMENT_POSITION, a);
|
||||
args.putBoolean(IS_FIRST, isFirst);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
BaseActivity a = (BaseActivity) requireNonNull(getActivity());
|
||||
a.getActivityComponent().inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle args = requireNonNull(getArguments());
|
||||
attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION));
|
||||
isFirst = args.getBoolean(IS_FIRST);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.fragment_image, container,
|
||||
false);
|
||||
|
||||
viewModel = ViewModelProviders.of(requireNonNull(getActivity()),
|
||||
viewModelFactory).get(ImageViewModel.class);
|
||||
|
||||
photoView = v.findViewById(R.id.photoView);
|
||||
photoView.setScaleLevels(1, 2, 4);
|
||||
photoView.setOnClickListener(view -> viewModel.clickImage());
|
||||
|
||||
// Request Listener
|
||||
RequestListener<Drawable> listener = new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e,
|
||||
Object model, Target<Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
if (getActivity() != null && isFirst)
|
||||
getActivity().supportStartPostponedEnterTransition();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model,
|
||||
Target<Drawable> target, DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
|
||||
// set transition name only when not animatable,
|
||||
// because the animation won't start otherwise
|
||||
photoView.setTransitionName(
|
||||
attachment.getTransitionName());
|
||||
}
|
||||
// Move image to the top if overlapping toolbar
|
||||
if (viewModel.isOverlappingToolbar(photoView, resource)) {
|
||||
photoView.setScaleType(FIT_START);
|
||||
}
|
||||
if (getActivity() != null && isFirst) {
|
||||
getActivity().supportStartPostponedEnterTransition();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load Image
|
||||
GlideApp.with(this)
|
||||
.load(attachment)
|
||||
// TODO allow if size < maxTextureSize ?
|
||||
// .override(SIZE_ORIGINAL)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(R.drawable.ic_image_broken)
|
||||
.addListener(listener)
|
||||
.into(photoView);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@NotNullByDefault
|
||||
interface ImageHelper {
|
||||
|
||||
DecodeResult decodeStream(InputStream is);
|
||||
|
||||
@Nullable
|
||||
String getExtensionFromMimeType(String mimeType);
|
||||
|
||||
class DecodeResult {
|
||||
|
||||
final int width;
|
||||
final int height;
|
||||
final String mimeType;
|
||||
|
||||
DecodeResult(int width, int height, String mimeType) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.ItemDecoration;
|
||||
import android.support.v7.widget.RecyclerView.State;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import static org.briarproject.briar.android.conversation.ImageAdapter.isBottomRow;
|
||||
import static org.briarproject.briar.android.conversation.ImageAdapter.isLeft;
|
||||
import static org.briarproject.briar.android.conversation.ImageAdapter.isTopRow;
|
||||
import static org.briarproject.briar.android.conversation.ImageAdapter.singleInRow;
|
||||
import static org.briarproject.briar.android.util.UiUtils.isRtl;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImageItemDecoration extends ItemDecoration {
|
||||
|
||||
private final int border;
|
||||
private final boolean isRtl;
|
||||
|
||||
ImageItemDecoration(Context ctx) {
|
||||
Resources res = ctx.getResources();
|
||||
|
||||
// for pixel perfection, add a pixel to the border if it has an odd size
|
||||
int b = res.getDimensionPixelSize(R.dimen.message_bubble_border);
|
||||
int realBorderSize = b % 2 == 0 ? b : b + 1;
|
||||
|
||||
// we are applying half the border around the insides of each image
|
||||
// to prevent differently sized images looking slightly broken
|
||||
border = realBorderSize / 2;
|
||||
|
||||
// find out if we are showing a RTL language
|
||||
isRtl = isRtl(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
|
||||
State state) {
|
||||
if (state.getItemCount() == 1) return;
|
||||
int pos = parent.getChildAdapterPosition(view);
|
||||
int num = state.getItemCount();
|
||||
boolean start = isLeft(pos) ^ isRtl;
|
||||
outRect.top = isTopRow(pos) ? 0 : border;
|
||||
outRect.left = start ? 0 : border;
|
||||
outRect.right = start && !singleInRow(pos, num) ? border : 0;
|
||||
outRect.bottom = isBottomRow(pos, num) ? 0 : border;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.load.Transformation;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
|
||||
import org.briarproject.briar.android.conversation.glide.GlideApp;
|
||||
import org.briarproject.briar.android.conversation.glide.Radii;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
@NotNullByDefault
|
||||
class ImageViewHolder extends ViewHolder {
|
||||
|
||||
@DrawableRes
|
||||
private static final int ERROR_RES = R.drawable.ic_image_broken;
|
||||
|
||||
protected final ImageView imageView;
|
||||
private final int imageSize;
|
||||
|
||||
ImageViewHolder(View v, int imageSize) {
|
||||
super(v);
|
||||
imageView = v.findViewById(R.id.imageView);
|
||||
this.imageSize = imageSize;
|
||||
}
|
||||
|
||||
void bind(AttachmentItem attachment, Radii r, boolean single,
|
||||
boolean needsStretch) {
|
||||
if (attachment.hasError()) {
|
||||
GlideApp.with(imageView)
|
||||
.clear(imageView);
|
||||
imageView.setImageResource(ERROR_RES);
|
||||
} else {
|
||||
setImageViewDimensions(attachment, single, needsStretch);
|
||||
loadImage(attachment, r);
|
||||
if (SDK_INT >= 21) {
|
||||
imageView.setTransitionName(attachment.getTransitionName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setImageViewDimensions(AttachmentItem a, boolean single,
|
||||
boolean needsStretch) {
|
||||
LayoutParams params = (LayoutParams) imageView.getLayoutParams();
|
||||
int width = needsStretch ? imageSize * 2 : imageSize;
|
||||
params.width = single ? a.getThumbnailWidth() : width;
|
||||
params.height = single ? a.getThumbnailHeight() : imageSize;
|
||||
params.setFullSpan(!single && needsStretch);
|
||||
imageView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private void loadImage(AttachmentItem a, Radii r) {
|
||||
Transformation<Bitmap> transformation = new BriarImageTransformation(r);
|
||||
GlideApp.with(imageView)
|
||||
.load(a)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(ERROR_RES)
|
||||
.transform(transformation)
|
||||
.transition(withCrossFade())
|
||||
.into(imageView)
|
||||
.waitForLayout();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package org.briarproject.briar.android.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
import android.arch.lifecycle.AndroidViewModel;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.media.MediaScannerConnection.scanFile;
|
||||
import static android.os.Environment.DIRECTORY_PICTURES;
|
||||
import static android.os.Environment.getExternalStoragePublicDirectory;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ImageViewModel extends AndroidViewModel {
|
||||
|
||||
private static Logger LOG = getLogger(ImageViewModel.class.getName());
|
||||
|
||||
private final MessagingManager messagingManager;
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
@IoExecutor
|
||||
private final Executor ioExecutor;
|
||||
|
||||
/**
|
||||
* true means there was an error saving the image, false if image was saved.
|
||||
*/
|
||||
private final MutableLiveData<Boolean> saveState = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> imageClicked =
|
||||
new MutableLiveData<>();
|
||||
private int toolbarTop, toolbarBottom;
|
||||
|
||||
@Inject
|
||||
ImageViewModel(Application application,
|
||||
MessagingManager messagingManager,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
@IoExecutor Executor ioExecutor) {
|
||||
super(application);
|
||||
this.messagingManager = messagingManager;
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.ioExecutor = ioExecutor;
|
||||
}
|
||||
|
||||
void clickImage() {
|
||||
imageClicked.setValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* A LiveData that is true if the image was clicked,
|
||||
* false if it wasn't.
|
||||
*
|
||||
* Call {@link #onOnImageClickSeen()} after consuming an update.
|
||||
*/
|
||||
LiveData<Boolean> getOnImageClicked() {
|
||||
return imageClicked;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void onOnImageClickSeen() {
|
||||
imageClicked.setValue(false);
|
||||
}
|
||||
|
||||
void setToolbarPosition(int top, int bottom) {
|
||||
toolbarTop = top;
|
||||
toolbarBottom = bottom;
|
||||
}
|
||||
|
||||
boolean isOverlappingToolbar(View screenView, Drawable drawable) {
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
float widthPercentage = screenView.getWidth() / (float) width;
|
||||
float heightPercentage = screenView.getHeight() / (float) height;
|
||||
float scaleFactor = Math.min(widthPercentage, heightPercentage);
|
||||
int realWidth = (int) (width * scaleFactor);
|
||||
int realHeight = (int) (height * scaleFactor);
|
||||
// return if image doesn't use the full width,
|
||||
// because it will be moved to the right otherwise
|
||||
if (realWidth < screenView.getWidth()) return false;
|
||||
int drawableTop = (screenView.getHeight() - realHeight) / 2;
|
||||
return drawableTop < toolbarBottom && drawableTop != toolbarTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* A LiveData that is true if there was an error
|
||||
* and false if the image was saved.
|
||||
* It can be null otherwise, if no image was saved recently.
|
||||
*
|
||||
* Call {@link #onSaveStateSeen()} after consuming an update.
|
||||
*/
|
||||
LiveData<Boolean> getSaveState() {
|
||||
return saveState;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void onSaveStateSeen() {
|
||||
saveState.setValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the attachment to a writeable {@link Uri}.
|
||||
*/
|
||||
@UiThread
|
||||
void saveImage(AttachmentItem attachment, @Nullable Uri uri) {
|
||||
if (uri == null) {
|
||||
saveState.setValue(true);
|
||||
} else {
|
||||
saveImage(attachment, () -> getOutputStream(uri), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the attachment on external storage,
|
||||
* assuming the permission was granted during install time.
|
||||
*/
|
||||
void saveImage(AttachmentItem attachment) {
|
||||
File file = getImageFile(attachment);
|
||||
saveImage(attachment, () -> getOutputStream(file), () -> scanFile(
|
||||
getApplication(), new String[] {file.toString()}, null, null));
|
||||
}
|
||||
|
||||
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
|
||||
@Nullable Runnable afterCopy) {
|
||||
MessageId messageId = attachment.getMessageId();
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
Attachment a = messagingManager.getAttachment(messageId);
|
||||
copyImageFromDb(a, osp, afterCopy);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
saveState.postValue(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void copyImageFromDb(Attachment a, OutputStreamProvider osp,
|
||||
@Nullable Runnable afterCopy) {
|
||||
ioExecutor.execute(() -> {
|
||||
try {
|
||||
InputStream is = a.getStream();
|
||||
OutputStream os = osp.getOutputStream();
|
||||
copyAndClose(is, os);
|
||||
if (afterCopy != null) afterCopy.run();
|
||||
saveState.postValue(false);
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
saveState.postValue(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getFileName() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
|
||||
Locale.getDefault());
|
||||
return sdf.format(new Date());
|
||||
}
|
||||
|
||||
private File getImageFile(AttachmentItem attachment) {
|
||||
File path = getExternalStoragePublicDirectory(DIRECTORY_PICTURES);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
path.mkdirs();
|
||||
String fileName = getFileName();
|
||||
String ext = "." + attachment.getExtension();
|
||||
File file = new File(path, fileName + ext);
|
||||
int i = 1;
|
||||
while (file.exists()) {
|
||||
file = new File(path, fileName + " (" + i + ")" + ext);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private OutputStream getOutputStream(File file) throws IOException {
|
||||
return new FileOutputStream(file);
|
||||
}
|
||||
|
||||
private OutputStream getOutputStream(Uri uri) throws IOException {
|
||||
OutputStream os =
|
||||
getApplication().getContentResolver().openOutputStream(uri);
|
||||
if (os == null) throw new IOException();
|
||||
return os;
|
||||
}
|
||||
|
||||
private interface OutputStreamProvider {
|
||||
OutputStream getOutputStream() throws IOException;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user