mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Compare commits
324 Commits
alpha-1.2.
...
beta-1.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b7e6fecf | ||
|
|
980940a7cf | ||
|
|
9a021daae8 | ||
|
|
cc9904a454 | ||
|
|
b0faab9395 | ||
|
|
f1198b47fd | ||
|
|
37f2ab555f | ||
|
|
61ca2a391b | ||
|
|
88537b9323 | ||
|
|
e6c004e8f6 | ||
|
|
dc7290dab7 | ||
|
|
9dff8bd64a | ||
|
|
35c2f60129 | ||
|
|
471a2372c8 | ||
|
|
fba028db03 | ||
|
|
c647c52638 | ||
|
|
cad5edcf86 | ||
|
|
38f70bb6be | ||
|
|
33bdc81b3e | ||
|
|
21fd7f5eed | ||
|
|
6354e91b55 | ||
|
|
8123c06348 | ||
|
|
663c648337 | ||
|
|
bee4e94987 | ||
|
|
c44bdc8762 | ||
|
|
423ecf71d8 | ||
|
|
73c7882cc0 | ||
|
|
552b9ef21a | ||
|
|
683af1ec3a | ||
|
|
77ed15311c | ||
|
|
3d72557618 | ||
|
|
e2a11d42f8 | ||
|
|
0f5ea6ae66 | ||
|
|
5b2c9b85f9 | ||
|
|
94921230d9 | ||
|
|
34788356e6 | ||
|
|
2f719d7f2c | ||
|
|
eb2e8d75f4 | ||
|
|
338d288290 | ||
|
|
03fe1a2d2c | ||
|
|
ade48c7bea | ||
|
|
4a8d89e2bb | ||
|
|
0fed1681ed | ||
|
|
93ad483066 | ||
|
|
d995e73996 | ||
|
|
1d271bab18 | ||
|
|
a9d2aa9366 | ||
|
|
2a48d43e5b | ||
|
|
0a4e23118a | ||
|
|
528a15962f | ||
|
|
0a0cc4e79c | ||
|
|
2b3ba42d70 | ||
|
|
9465331ece | ||
|
|
b0d86bef3e | ||
|
|
a28a2360f1 | ||
|
|
f7a957150e | ||
|
|
67bd065bc3 | ||
|
|
5a3a12767c | ||
|
|
2f86bc312c | ||
|
|
f0fcadfaf4 | ||
|
|
a9d88c849a | ||
|
|
7397efca80 | ||
|
|
17bd6f9a33 | ||
|
|
2d4e7a9fb0 | ||
|
|
0266da993d | ||
|
|
ea5280713f | ||
|
|
0b89d29c7d | ||
|
|
688bac77a8 | ||
|
|
c736bf7c06 | ||
|
|
539730f8ec | ||
|
|
a9c4669c75 | ||
|
|
4c11f93ee2 | ||
|
|
f8dba6fd7f | ||
|
|
419247074f | ||
|
|
2ddb7b5b64 | ||
|
|
c14b59661e | ||
|
|
d70d27e665 | ||
|
|
8991762b0c | ||
|
|
2fc6741c99 | ||
|
|
1075af73f2 | ||
|
|
8671229f76 | ||
|
|
0fb67583ff | ||
|
|
2fc0fd17a2 | ||
|
|
f185860213 | ||
|
|
8ffcdbfc21 | ||
|
|
6a38e2cca8 | ||
|
|
10d9d78ca8 | ||
|
|
03a68038e7 | ||
|
|
fabefcdf4b | ||
|
|
9e4d8ecddf | ||
|
|
1dffbfd8dc | ||
|
|
950db5a87a | ||
|
|
21348d5557 | ||
|
|
eb1a089437 | ||
|
|
17fc81ab7a | ||
|
|
1c54fd1101 | ||
|
|
263bce38cd | ||
|
|
a923c1151c | ||
|
|
0bf10a827f | ||
|
|
49850e4198 | ||
|
|
95b437b311 | ||
|
|
7006f765a6 | ||
|
|
ec3360400c | ||
|
|
5c41d09c52 | ||
|
|
a7590956fd | ||
|
|
a581960121 | ||
|
|
dc57a0b925 | ||
|
|
d6082162ab | ||
|
|
1801afdbb7 | ||
|
|
458add0c9c | ||
|
|
f2374eb141 | ||
|
|
5db5897793 | ||
|
|
f3d628afa7 | ||
|
|
4d3482e40e | ||
|
|
a8cff454ec | ||
|
|
f66cae4749 | ||
|
|
aded1daf92 | ||
|
|
aa1ba0d950 | ||
|
|
071010e438 | ||
|
|
f6d8e364d6 | ||
|
|
f1453ed4c4 | ||
|
|
3a2146cb03 | ||
|
|
24eb76de20 | ||
|
|
693478e0a5 | ||
|
|
bf6be5c5a7 | ||
|
|
a12a639cd3 | ||
|
|
ef37428714 | ||
|
|
644afe8995 | ||
|
|
c66c428124 | ||
|
|
db5b2ea9b6 | ||
|
|
d84603bce2 | ||
|
|
b128370299 | ||
|
|
240e619248 | ||
|
|
c89bde08db | ||
|
|
3ecd1c62b8 | ||
|
|
e3c5497283 | ||
|
|
4bd8ee8ccf | ||
|
|
43b437af92 | ||
|
|
56e0d62597 | ||
|
|
d10e5f025d | ||
|
|
b1a80691db | ||
|
|
049aa61e85 | ||
|
|
7026361234 | ||
|
|
5e30dc5bf4 | ||
|
|
024bfc8ec8 | ||
|
|
04e5e8e4d0 | ||
|
|
7c5d47733f | ||
|
|
b24f2a1818 | ||
|
|
ee6664ce9d | ||
|
|
ab434946b5 | ||
|
|
35e431eb99 | ||
|
|
aa8cddf509 | ||
|
|
c9ede0bfc1 | ||
|
|
6ec9a0f2b2 | ||
|
|
2f86112801 | ||
|
|
c032befe6f | ||
|
|
55eccde031 | ||
|
|
5716820439 | ||
|
|
17d433dd9b | ||
|
|
000812bf6d | ||
|
|
5e2187a877 | ||
|
|
e10b6334f5 | ||
|
|
baa0341727 | ||
|
|
814b2b2582 | ||
|
|
56705bde74 | ||
|
|
dceb38b777 | ||
|
|
9947a6aa1b | ||
|
|
7a3be374c8 | ||
|
|
4ea3ce0e3c | ||
|
|
923185b3f4 | ||
|
|
d91e6c6c1a | ||
|
|
1c93a79448 | ||
|
|
e12ad0cd79 | ||
|
|
8d6bd29b93 | ||
|
|
f941a73999 | ||
|
|
c3057141d8 | ||
|
|
49080cb64c | ||
|
|
27dbe23914 | ||
|
|
d7a2de5817 | ||
|
|
0328aa0630 | ||
|
|
b6cf302131 | ||
|
|
e2a894acd3 | ||
|
|
00ed6d9bb8 | ||
|
|
c9a9734368 | ||
|
|
efc56a8724 | ||
|
|
6e6923b108 | ||
|
|
f459beccdb | ||
|
|
751c5a3245 | ||
|
|
8488499da6 | ||
|
|
96a7e3c425 | ||
|
|
0dcf510466 | ||
|
|
0427b12d52 | ||
|
|
9256c66fcc | ||
|
|
706f4e1c4c | ||
|
|
96debcd616 | ||
|
|
07f20e1e0d | ||
|
|
fee2e503bd | ||
|
|
f9f260bbc1 | ||
|
|
61718192ee | ||
|
|
27893f9cdd | ||
|
|
9b0b80ef04 | ||
|
|
3e1c2df4b1 | ||
|
|
fa745410cc | ||
|
|
a427624e8d | ||
|
|
3798ca1e17 | ||
|
|
113120b3ab | ||
|
|
b10ca5b77f | ||
|
|
f10e3d756a | ||
|
|
9608b974ec | ||
|
|
3b6cc9c633 | ||
|
|
5305dd62d1 | ||
|
|
a066190c60 | ||
|
|
cdae8b35f5 | ||
|
|
6ee57315dd | ||
|
|
64f682146d | ||
|
|
36525fbe9d | ||
|
|
8f628f2d45 | ||
|
|
5e84e5b8b6 | ||
|
|
a64878bd00 | ||
|
|
c53fc5eaa3 | ||
|
|
212751c835 | ||
|
|
fe1c6acebb | ||
|
|
ce47bfe018 | ||
|
|
0ee4ade404 | ||
|
|
e99df2b69e | ||
|
|
db84d07c38 | ||
|
|
db610cfb4c | ||
|
|
5b52417d20 | ||
|
|
8a768cf933 | ||
|
|
bebf3bbc39 | ||
|
|
8a3dd5472b | ||
|
|
f971533a5b | ||
|
|
a12166c13b | ||
|
|
51624a31e3 | ||
|
|
cdc632e1af | ||
|
|
31f87f647e | ||
|
|
dcd37f71d1 | ||
|
|
4ca286b28e | ||
|
|
4f3e4b019a | ||
|
|
62cca1335f | ||
|
|
11a18859fb | ||
|
|
1116a7e125 | ||
|
|
415b315292 | ||
|
|
9818ec2b66 | ||
|
|
95ef061a34 | ||
|
|
aaaf8aa66f | ||
|
|
29965e38d0 | ||
|
|
371d49a213 | ||
|
|
6ed95e145e | ||
|
|
8c025c1173 | ||
|
|
9ce541cc31 | ||
|
|
aa57a4c123 | ||
|
|
58d9deb3b8 | ||
|
|
f0685c4a43 | ||
|
|
484817db08 | ||
|
|
670bf15d31 | ||
|
|
6df1e0fd77 | ||
|
|
ec910cb80f | ||
|
|
372516646d | ||
|
|
72e721b0d3 | ||
|
|
6599093611 | ||
|
|
dceeecf1fe | ||
|
|
ace0b9a3d8 | ||
|
|
7c45c90de9 | ||
|
|
c2a4b5e26a | ||
|
|
feac0ad802 | ||
|
|
60478eba3f | ||
|
|
3639952612 | ||
|
|
c4a654b267 | ||
|
|
ecb31a4d32 | ||
|
|
76f201bb2f | ||
|
|
1d44305e34 | ||
|
|
a37af592cd | ||
|
|
87799b743c | ||
|
|
b898a7c370 | ||
|
|
7f486eef4c | ||
|
|
f3210e3af2 | ||
|
|
225fd6fd49 | ||
|
|
400d259a60 | ||
|
|
4074ac8578 | ||
|
|
b2e6dd4138 | ||
|
|
b608b42174 | ||
|
|
f603254153 | ||
|
|
c851dd228b | ||
|
|
e97478a21a | ||
|
|
726ebcea3f | ||
|
|
2f969775d8 | ||
|
|
d3b855318c | ||
|
|
95104d3383 | ||
|
|
6860a04e8b | ||
|
|
33c24f8655 | ||
|
|
1fa4b78474 | ||
|
|
b678de7529 | ||
|
|
ab1ed0ff5a | ||
|
|
ad20e5230a | ||
|
|
bcc0442add | ||
|
|
700f6e05bf | ||
|
|
d8327d6de2 | ||
|
|
5a55b3d7e3 | ||
|
|
bed87ed439 | ||
|
|
6d1f1c7852 | ||
|
|
f6b3bde724 | ||
|
|
94ec22bef8 | ||
|
|
ae923e5777 | ||
|
|
46b4204805 | ||
|
|
2257c005b3 | ||
|
|
eb9ff9c954 | ||
|
|
4f08f81779 | ||
|
|
2b0815aaac | ||
|
|
a9e83491d3 | ||
|
|
ee967c5d8f | ||
|
|
43740777d4 | ||
|
|
d5b0556ea2 | ||
|
|
227f00c10c | ||
|
|
8b4ff2dc8a | ||
|
|
4be2afb915 | ||
|
|
74447b8ec3 | ||
|
|
d95242bd7e | ||
|
|
51794424ce | ||
|
|
5db099bae6 | ||
|
|
a2faa3bd3b | ||
|
|
a3fb7b5680 | ||
|
|
2fb4825b8f | ||
|
|
b4a5fe6772 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,6 +18,7 @@ local.properties
|
|||||||
|
|
||||||
# Android Studio
|
# Android Studio
|
||||||
.idea/*
|
.idea/*
|
||||||
|
!.idea/inspectionProfiles/
|
||||||
!.idea/runConfigurations/
|
!.idea/runConfigurations/
|
||||||
!.idea/codeStyleSettings.xml
|
!.idea/codeStyleSettings.xml
|
||||||
!.idea/codeStyles
|
!.idea/codeStyles
|
||||||
|
|||||||
@@ -1,30 +1,79 @@
|
|||||||
image: briar/ci-image-android:latest
|
image: briar/ci-image-android:latest
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- optional_tests
|
- optional_tests
|
||||||
- check_reproducibility
|
- check_reproducibility
|
||||||
|
|
||||||
test:
|
workflow:
|
||||||
stage: test
|
# when to create a CI pipeline
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
|
||||||
|
when: never # avoids duplicate jobs for branch and MR
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
- if: '$CI_COMMIT_TAG'
|
||||||
|
|
||||||
|
.base-test:
|
||||||
before_script:
|
before_script:
|
||||||
- set -e
|
- set -e
|
||||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
|
key: "$CI_COMMIT_REF_SLUG"
|
||||||
paths:
|
paths:
|
||||||
- .gradle/wrapper
|
- .gradle/wrapper
|
||||||
- .gradle/caches
|
- .gradle/caches
|
||||||
|
|
||||||
script:
|
|
||||||
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
|
|
||||||
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources
|
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
# these file change every time but should not be cached
|
# these file change every time and should not be cached
|
||||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
||||||
|
|
||||||
|
test:
|
||||||
|
extends: .base-test
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
|
||||||
|
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
when: always
|
||||||
|
- when: always
|
||||||
|
|
||||||
|
android test:
|
||||||
|
extends: .base-test
|
||||||
|
stage: optional_tests
|
||||||
|
image: briar/ci-image-android-emulator:latest
|
||||||
|
script:
|
||||||
|
# start emulator first, so it can fail early
|
||||||
|
- start-emulator.sh
|
||||||
|
# run normal and screenshot tests together (exclude Large tests)
|
||||||
|
- ./gradlew -Djava.security.egd=file:/dev/urandom connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=org.briarproject.briar.android -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest
|
||||||
|
after_script:
|
||||||
|
- adb pull /sdcard/Pictures/screenshots
|
||||||
|
artifacts:
|
||||||
|
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
|
||||||
|
paths:
|
||||||
|
- kernel.log
|
||||||
|
- logcat.txt
|
||||||
|
- briar-android/build/reports/androidTests/connected/flavors/*
|
||||||
|
- screenshots
|
||||||
|
expire_in: 3 days
|
||||||
|
when: on_failure
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||||
|
when: on_success
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
changes:
|
||||||
|
- briar-android/**/*
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- if: '$CI_COMMIT_TAG == null'
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
retry:
|
||||||
|
max: 1
|
||||||
|
tags:
|
||||||
|
- kvm
|
||||||
|
|
||||||
test_reproducible:
|
test_reproducible:
|
||||||
stage: check_reproducibility
|
stage: check_reproducibility
|
||||||
@@ -40,6 +89,7 @@ test_reproducible:
|
|||||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
|
key: "$CI_COMMIT_REF_SLUG"
|
||||||
paths:
|
paths:
|
||||||
- .gradle/wrapper
|
- .gradle/wrapper
|
||||||
- .gradle/caches
|
- .gradle/caches
|
||||||
@@ -52,11 +102,15 @@ test_reproducible:
|
|||||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
||||||
|
|
||||||
manual_tests:
|
bridge test:
|
||||||
extends: .optional_tests
|
extends: .optional_tests
|
||||||
when: manual
|
rules:
|
||||||
except:
|
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||||
- tags
|
when: on_success
|
||||||
|
allow_failure: true
|
||||||
|
- if: '$CI_COMMIT_TAG == null'
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
pre_release_tests:
|
pre_release_tests:
|
||||||
extends: .optional_tests
|
extends: .optional_tests
|
||||||
|
|||||||
1
.idea/dictionaries/briar.xml
generated
1
.idea/dictionaries/briar.xml
generated
@@ -7,6 +7,7 @@
|
|||||||
<w>encrypter</w>
|
<w>encrypter</w>
|
||||||
<w>identicon</w>
|
<w>identicon</w>
|
||||||
<w>introducee</w>
|
<w>introducee</w>
|
||||||
|
<w>introducees</w>
|
||||||
<w>introducer</w>
|
<w>introducer</w>
|
||||||
<w>onboarding</w>
|
<w>onboarding</w>
|
||||||
</words>
|
</words>
|
||||||
|
|||||||
14
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
14
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreObjectMethods" value="true" />
|
||||||
|
<option name="ignoreAnonymousClassMethods" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="WeakerAccess" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS" value="true" />
|
||||||
|
<option name="SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES" value="true" />
|
||||||
|
<option name="SUGGEST_PRIVATE_FOR_INNERS" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
51
.idea/runConfigurations/Instrumentation_Tests__destroys_DB_.xml
generated
Normal file
51
.idea/runConfigurations/Instrumentation_Tests__destroys_DB_.xml
generated
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Instrumentation Tests (destroys DB)" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||||
|
<module name="briar.briar-android" />
|
||||||
|
<option name="TESTING_TYPE" value="1" />
|
||||||
|
<option name="METHOD_NAME" value="" />
|
||||||
|
<option name="CLASS_NAME" value="" />
|
||||||
|
<option name="PACKAGE_NAME" value="org.briarproject.briar.android" />
|
||||||
|
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||||
|
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
|
||||||
|
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
||||||
|
<option name="CLEAR_LOGCAT" value="false" />
|
||||||
|
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||||
|
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||||
|
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||||
|
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||||
|
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||||
|
<Auto>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
</Auto>
|
||||||
|
<Hybrid>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
</Hybrid>
|
||||||
|
<Java />
|
||||||
|
<Native>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
</Native>
|
||||||
|
<Profilers>
|
||||||
|
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
||||||
|
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||||
|
</Profilers>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -6,13 +6,17 @@ apply from: 'witness.gradle'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.2'
|
buildToolsVersion '30.0.3'
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
doNotStrip '**/*.so'
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode 10217
|
versionCode 10304
|
||||||
versionName "1.2.17"
|
versionName "1.3.4"
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class AndroidBluetoothPlugin
|
class AndroidBluetoothPlugin extends
|
||||||
extends BluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
|
AbstractBluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(AndroidBluetoothPlugin.class.getName());
|
getLogger(AndroidBluetoothPlugin.class.getName());
|
||||||
@@ -75,6 +75,7 @@ class AndroidBluetoothPlugin
|
|||||||
|
|
||||||
// Non-null if the plugin started successfully
|
// Non-null if the plugin started successfully
|
||||||
private volatile BluetoothAdapter adapter = null;
|
private volatile BluetoothAdapter adapter = null;
|
||||||
|
private volatile boolean stopDiscoverAndConnect;
|
||||||
|
|
||||||
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
||||||
BluetoothConnectionFactory<BluetoothSocket> connectionFactory,
|
BluetoothConnectionFactory<BluetoothSocket> connectionFactory,
|
||||||
@@ -175,6 +176,11 @@ class AndroidBluetoothPlugin
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
IoUtils.tryToClose(s, LOG, WARNING);
|
IoUtils.tryToClose(s, LOG, WARNING);
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
// BluetoothSocket#connect() may throw an NPE under unknown
|
||||||
|
// circumstances
|
||||||
|
IoUtils.tryToClose(s, LOG, WARNING);
|
||||||
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,22 +188,40 @@ class AndroidBluetoothPlugin
|
|||||||
@Nullable
|
@Nullable
|
||||||
DuplexTransportConnection discoverAndConnect(String uuid) {
|
DuplexTransportConnection discoverAndConnect(String uuid) {
|
||||||
if (adapter == null) return null;
|
if (adapter == null) return null;
|
||||||
for (String address : discoverDevices()) {
|
if (!discoverSemaphore.tryAcquire()) {
|
||||||
try {
|
LOG.info("Discover already running");
|
||||||
if (LOG.isLoggable(INFO))
|
return null;
|
||||||
LOG.info("Connecting to " + scrubMacAddress(address));
|
}
|
||||||
return connectTo(address, uuid);
|
try {
|
||||||
} catch (IOException e) {
|
stopDiscoverAndConnect = false;
|
||||||
if (LOG.isLoggable(INFO)) {
|
for (String address : discoverDevices()) {
|
||||||
LOG.info("Could not connect to "
|
if (stopDiscoverAndConnect) {
|
||||||
+ scrubMacAddress(address));
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connecting to " + scrubMacAddress(address));
|
||||||
|
return connectTo(address, uuid);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Could not connect to "
|
||||||
|
+ scrubMacAddress(address));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
discoverSemaphore.release();
|
||||||
}
|
}
|
||||||
LOG.info("Could not connect to any devices");
|
LOG.info("Could not connect to any devices");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopDiscoverAndConnect() {
|
||||||
|
stopDiscoverAndConnect = true;
|
||||||
|
adapter.cancelDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
private Collection<String> discoverDevices() {
|
private Collection<String> discoverDevices() {
|
||||||
List<String> addresses = new ArrayList<>();
|
List<String> addresses = new ArrayList<>();
|
||||||
BlockingQueue<Intent> intents = new LinkedBlockingQueue<>();
|
BlockingQueue<Intent> intents = new LinkedBlockingQueue<>();
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ import org.briarproject.bramble.api.Pair;
|
|||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
import static android.content.Context.MODE_PRIVATE;
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static java.lang.Runtime.getRuntime;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||||
|
|
||||||
@@ -114,12 +117,21 @@ public class AndroidUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of supported content types for image attachments.
|
* Returns an array of supported content types for image attachments.
|
||||||
* GIFs can't be compressed on API < 24 so they're not supported.
|
|
||||||
* <p>
|
|
||||||
* TODO: Remove this restriction when large message support is added
|
|
||||||
*/
|
*/
|
||||||
public static String[] getSupportedImageContentTypes() {
|
public static String[] getSupportedImageContentTypes() {
|
||||||
if (SDK_INT < 24) return new String[] {"image/jpeg", "image/png"};
|
return new String[] {"image/jpeg", "image/png", "image/gif"};
|
||||||
else return new String[] {"image/jpeg", "image/png", "image/gif"};
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String getSystemProperty(String propName) {
|
||||||
|
try {
|
||||||
|
Process p = getRuntime().exec("getprop " + propName);
|
||||||
|
Scanner s = new Scanner(p.getInputStream());
|
||||||
|
String line = s.nextLine();
|
||||||
|
s.close();
|
||||||
|
return line;
|
||||||
|
} catch (SecurityException | IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ public interface FeatureFlags {
|
|||||||
|
|
||||||
boolean shouldEnableProfilePictures();
|
boolean shouldEnableProfilePictures();
|
||||||
|
|
||||||
|
boolean shouldEnableDisappearingMessages();
|
||||||
|
|
||||||
|
boolean shouldEnableConnectViaBluetooth();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.briarproject.bramble.api.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.Transaction;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for registering a hook with the {@link CleanupManager}
|
||||||
|
* that will be called when a message's cleanup deadline is reached.
|
||||||
|
*/
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface CleanupHook {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the cleanup deadlines of one or more messages are reached.
|
||||||
|
* <p>
|
||||||
|
* The callee is not required to delete the messages, but the hook won't be
|
||||||
|
* called again for these messages unless another cleanup timer is set (see
|
||||||
|
* {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)}
|
||||||
|
* and {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)}).
|
||||||
|
*/
|
||||||
|
void deleteMessages(Transaction txn, GroupId g,
|
||||||
|
Collection<MessageId> messageIds) throws DbException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.briarproject.bramble.api.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.bramble.api.db.Transaction;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.ClientId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CleanupManager is responsible for tracking the cleanup deadlines of
|
||||||
|
* messages and passing them to their respective
|
||||||
|
* {@link CleanupHook CleanupHooks} when the deadlines are reached.
|
||||||
|
* <p>
|
||||||
|
* The CleanupManager responds to
|
||||||
|
* {@link CleanupTimerStartedEvent CleanupTimerStartedEvents} broadcast by the
|
||||||
|
* {@link DatabaseComponent}.
|
||||||
|
* <p>
|
||||||
|
* See {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)},
|
||||||
|
* {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)},
|
||||||
|
* {@link DatabaseComponent#stopCleanupTimer(Transaction, MessageId)}.
|
||||||
|
*/
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface CleanupManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When scheduling a cleanup task we overshoot the deadline by this many
|
||||||
|
* milliseconds to reduce the number of tasks that need to be scheduled
|
||||||
|
* when messages have cleanup deadlines that are close together.
|
||||||
|
*/
|
||||||
|
long BATCH_DELAY_MS = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a hook to be called when messages are due for cleanup.
|
||||||
|
* This method should be called before
|
||||||
|
* {@link LifecycleManager#startServices(SecretKey)}.
|
||||||
|
*/
|
||||||
|
void registerCleanupHook(ClientId c, int majorVersion,
|
||||||
|
CleanupHook hook);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.briarproject.bramble.api.cleanup.event;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event that is broadcast when a message's cleanup timer is started.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
public class CleanupTimerStartedEvent extends Event {
|
||||||
|
|
||||||
|
private final MessageId messageId;
|
||||||
|
private final long cleanupDeadline;
|
||||||
|
|
||||||
|
public CleanupTimerStartedEvent(MessageId messageId,
|
||||||
|
long cleanupDeadline) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.cleanupDeadline = cleanupDeadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageId getMessageId() {
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCleanupDeadline() {
|
||||||
|
return cleanupDeadline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.bramble.api.client;
|
package org.briarproject.bramble.api.client;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||||
@@ -16,6 +17,7 @@ import org.briarproject.bramble.api.sync.Message;
|
|||||||
import org.briarproject.bramble.api.sync.MessageId;
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -50,9 +52,11 @@ public interface ClientHelper {
|
|||||||
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
|
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
|
||||||
throws DbException, FormatException;
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||||
|
BdfDictionary query) throws DbException, FormatException;
|
||||||
|
|
||||||
BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
||||||
throws DbException,
|
throws DbException, FormatException;
|
||||||
FormatException;
|
|
||||||
|
|
||||||
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
|
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
|
||||||
throws DbException, FormatException;
|
throws DbException, FormatException;
|
||||||
@@ -119,4 +123,17 @@ public interface ClientHelper {
|
|||||||
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
||||||
BdfDictionary properties) throws FormatException;
|
BdfDictionary properties) throws FormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the contact ID from the group metadata of the given contact
|
||||||
|
* group.
|
||||||
|
*/
|
||||||
|
ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||||
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the given contact ID in the group metadata of the given contact
|
||||||
|
* group.
|
||||||
|
*/
|
||||||
|
void setContactId(Transaction txn, GroupId contactGroupId, ContactId c)
|
||||||
|
throws DbException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.briarproject.bramble.api.client;
|
||||||
|
|
||||||
|
public interface ContactGroupConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group metadata key for associating a contact ID with a contact group.
|
||||||
|
*/
|
||||||
|
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.briarproject.bramble.api.contact.event;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event that is broadcast when the alias for a contact changed.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
public class ContactAliasChangedEvent extends Event {
|
||||||
|
|
||||||
|
private final ContactId contactId;
|
||||||
|
@Nullable
|
||||||
|
private final String alias;
|
||||||
|
|
||||||
|
public ContactAliasChangedEvent(ContactId contactId,
|
||||||
|
@Nullable String alias) {
|
||||||
|
this.contactId = contactId;
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContactId getContactId() {
|
||||||
|
return contactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,18 @@ import javax.annotation.Nullable;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public interface DatabaseComponent extends TransactionManager {
|
public interface DatabaseComponent extends TransactionManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value for {@link #getNextCleanupDeadline(Transaction)} if
|
||||||
|
* no messages are scheduled to be deleted.
|
||||||
|
*/
|
||||||
|
long NO_CLEANUP_DEADLINE = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value for {@link #startCleanupTimer(Transaction, MessageId)}
|
||||||
|
* if the cleanup timer was not started.
|
||||||
|
*/
|
||||||
|
long TIMER_NOT_STARTED = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the database and returns true if the database already existed.
|
* Opens the database and returns true if the database already existed.
|
||||||
*
|
*
|
||||||
@@ -288,6 +300,16 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
|
Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IDs of any delivered messages in the given group with
|
||||||
|
* metadata that matches all entries in the given query. If the query is
|
||||||
|
* empty, the IDs of all delivered messages are returned.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||||
|
Metadata query) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the IDs of any messages that need to be validated.
|
* Returns the IDs of any messages that need to be validated.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -314,6 +336,15 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
Collection<MessageId> getMessagesToShare(Transaction txn)
|
Collection<MessageId> getMessagesToShare(Transaction txn)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IDs of any messages of any messages that are due for
|
||||||
|
* deletion, along with their group IDs.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
Map<GroupId, Collection<MessageId>> getMessagesToDelete(Transaction txn)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the metadata for all delivered messages in the given group.
|
* Returns the metadata for all delivered messages in the given group.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -395,6 +426,15 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
|
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
|
* message is due to be deleted, or {@link #NO_CLEANUP_DEADLINE}
|
||||||
|
* if no messages are scheduled to be deleted.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
long getNextCleanupDeadline(Transaction txn) throws DbException;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
* message is due to be sent to the given contact. The returned value may
|
* message is due to be sent to the given contact. The returned value may
|
||||||
@@ -535,6 +575,13 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
|
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cleanup timer duration for the given message. This does not
|
||||||
|
* start the message's cleanup timer.
|
||||||
|
*/
|
||||||
|
void setCleanupTimerDuration(Transaction txn, MessageId m, long duration)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given contact as verified.
|
* Marks the given contact as verified.
|
||||||
*/
|
*/
|
||||||
@@ -557,6 +604,12 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
*/
|
*/
|
||||||
void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
|
void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the given message as not shared. This method is only meant for
|
||||||
|
* testing.
|
||||||
|
*/
|
||||||
|
void setMessageNotShared(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given message as shared.
|
* Marks the given message as shared.
|
||||||
*/
|
*/
|
||||||
@@ -599,6 +652,22 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
|
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the cleanup timer for the given message, if a timer duration
|
||||||
|
* has been set and the timer has not already been started.
|
||||||
|
*
|
||||||
|
* @return The cleanup deadline, or {@link #TIMER_NOT_STARTED} if no
|
||||||
|
* timer duration has been set for this message or its timer has already
|
||||||
|
* been started.
|
||||||
|
*/
|
||||||
|
long startCleanupTimer(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the cleanup timer for the given message, if the timer has been
|
||||||
|
* started.
|
||||||
|
*/
|
||||||
|
void stopCleanupTimer(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the given transport keys, deleting any keys they have replaced.
|
* Stores the given transport keys, deleting any keys they have replaced.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class Author implements Nameable {
|
|||||||
/**
|
/**
|
||||||
* Returns the author's name.
|
* Returns the author's name.
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.briarproject.bramble.api.keyagreement.event;
|
|||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event that is broadcast when a BQP protocol completes.
|
* An event that is broadcast when a BQP protocol begins.
|
||||||
*/
|
*/
|
||||||
public class KeyAgreementStartedEvent extends Event {
|
public class KeyAgreementStartedEvent extends Event {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ public interface ReportingConstants {
|
|||||||
* Hidden service address for reporting crashes and feedback to the
|
* Hidden service address for reporting crashes and feedback to the
|
||||||
* developers.
|
* developers.
|
||||||
*/
|
*/
|
||||||
String DEV_ONION_ADDRESS = "cwqmubyvnig3wag3.onion";
|
String DEV_ONION_ADDRESS =
|
||||||
|
"b2nkt5doeamvdmjzfz7g42hk5vdtlnktlgzhel2bgjcc4v4jhnx2qrqd.onion";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.data.BdfList;
|
|||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class ValidationUtils {
|
public class ValidationUtils {
|
||||||
|
|
||||||
@@ -64,4 +66,9 @@ public class ValidationUtils {
|
|||||||
if (dictionary != null && dictionary.size() != size)
|
if (dictionary != null && dictionary.size() != size)
|
||||||
throw new FormatException();
|
throw new FormatException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void checkRange(@Nullable Long l, long min, long max)
|
||||||
|
throws FormatException {
|
||||||
|
if (l != null && (l < min || l > max)) throw new FormatException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.briarproject.bramble.test;
|
||||||
|
|
||||||
|
public interface TimeTravel {
|
||||||
|
|
||||||
|
void setCurrentTimeMillis(long now) throws InterruptedException;
|
||||||
|
|
||||||
|
void addCurrentTimeMillis(long add) throws InterruptedException;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble;
|
package org.briarproject.bramble;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.cleanup.CleanupModule;
|
||||||
import org.briarproject.bramble.contact.ContactModule;
|
import org.briarproject.bramble.contact.ContactModule;
|
||||||
import org.briarproject.bramble.crypto.CryptoExecutorModule;
|
import org.briarproject.bramble.crypto.CryptoExecutorModule;
|
||||||
import org.briarproject.bramble.db.DatabaseExecutorModule;
|
import org.briarproject.bramble.db.DatabaseExecutorModule;
|
||||||
@@ -14,6 +15,8 @@ import org.briarproject.bramble.versioning.VersioningModule;
|
|||||||
|
|
||||||
public interface BrambleCoreEagerSingletons {
|
public interface BrambleCoreEagerSingletons {
|
||||||
|
|
||||||
|
void inject(CleanupModule.EagerSingletons init);
|
||||||
|
|
||||||
void inject(ContactModule.EagerSingletons init);
|
void inject(ContactModule.EagerSingletons init);
|
||||||
|
|
||||||
void inject(CryptoExecutorModule.EagerSingletons init);
|
void inject(CryptoExecutorModule.EagerSingletons init);
|
||||||
@@ -39,6 +42,7 @@ public interface BrambleCoreEagerSingletons {
|
|||||||
class Helper {
|
class Helper {
|
||||||
|
|
||||||
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
|
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
|
||||||
|
c.inject(new CleanupModule.EagerSingletons());
|
||||||
c.inject(new ContactModule.EagerSingletons());
|
c.inject(new ContactModule.EagerSingletons());
|
||||||
c.inject(new CryptoExecutorModule.EagerSingletons());
|
c.inject(new CryptoExecutorModule.EagerSingletons());
|
||||||
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble;
|
package org.briarproject.bramble;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.cleanup.CleanupModule;
|
||||||
import org.briarproject.bramble.client.ClientModule;
|
import org.briarproject.bramble.client.ClientModule;
|
||||||
import org.briarproject.bramble.connection.ConnectionModule;
|
import org.briarproject.bramble.connection.ConnectionModule;
|
||||||
import org.briarproject.bramble.contact.ContactModule;
|
import org.briarproject.bramble.contact.ContactModule;
|
||||||
@@ -21,15 +22,14 @@ import org.briarproject.bramble.rendezvous.RendezvousModule;
|
|||||||
import org.briarproject.bramble.settings.SettingsModule;
|
import org.briarproject.bramble.settings.SettingsModule;
|
||||||
import org.briarproject.bramble.sync.SyncModule;
|
import org.briarproject.bramble.sync.SyncModule;
|
||||||
import org.briarproject.bramble.sync.validation.ValidationModule;
|
import org.briarproject.bramble.sync.validation.ValidationModule;
|
||||||
import org.briarproject.bramble.system.ClockModule;
|
|
||||||
import org.briarproject.bramble.transport.TransportModule;
|
import org.briarproject.bramble.transport.TransportModule;
|
||||||
import org.briarproject.bramble.versioning.VersioningModule;
|
import org.briarproject.bramble.versioning.VersioningModule;
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
|
||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
|
CleanupModule.class,
|
||||||
ClientModule.class,
|
ClientModule.class,
|
||||||
ClockModule.class,
|
|
||||||
ConnectionModule.class,
|
ConnectionModule.class,
|
||||||
ContactModule.class,
|
ContactModule.class,
|
||||||
CryptoModule.class,
|
CryptoModule.class,
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package org.briarproject.bramble.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.CleanupHook;
|
||||||
|
import org.briarproject.bramble.api.cleanup.CleanupManager;
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
|
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.Transaction;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.Service;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.ClientId;
|
||||||
|
import org.briarproject.bramble.api.sync.Group;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||||
|
import org.briarproject.bramble.api.versioning.ClientMajorVersion;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static java.util.logging.Level.INFO;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
|
@ThreadSafe
|
||||||
|
@NotNullByDefault
|
||||||
|
class CleanupManagerImpl implements CleanupManager, Service, EventListener {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
getLogger(CleanupManagerImpl.class.getName());
|
||||||
|
|
||||||
|
private final Executor dbExecutor;
|
||||||
|
private final DatabaseComponent db;
|
||||||
|
private final TaskScheduler taskScheduler;
|
||||||
|
private final Clock clock;
|
||||||
|
private final Map<ClientMajorVersion, CleanupHook> hooks =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private final Set<CleanupTask> pending = new HashSet<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CleanupManagerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
|
DatabaseComponent db, TaskScheduler taskScheduler, Clock clock) {
|
||||||
|
this.dbExecutor = dbExecutor;
|
||||||
|
this.db = db;
|
||||||
|
this.taskScheduler = taskScheduler;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCleanupHook(ClientId c, int majorVersion,
|
||||||
|
CleanupHook hook) {
|
||||||
|
hooks.put(new ClientMajorVersion(c, majorVersion), hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startService() {
|
||||||
|
maybeScheduleTask(clock.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopService() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventOccurred(Event e) {
|
||||||
|
if (e instanceof CleanupTimerStartedEvent) {
|
||||||
|
CleanupTimerStartedEvent a = (CleanupTimerStartedEvent) e;
|
||||||
|
maybeScheduleTask(a.getCleanupDeadline());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeScheduleTask(long deadline) {
|
||||||
|
synchronized (lock) {
|
||||||
|
for (CleanupTask task : pending) {
|
||||||
|
if (task.deadline <= deadline) return;
|
||||||
|
}
|
||||||
|
CleanupTask task = new CleanupTask(deadline);
|
||||||
|
pending.add(task);
|
||||||
|
scheduleTask(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleTask(CleanupTask task) {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
long delay = max(0, task.deadline - now + BATCH_DELAY_MS);
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Scheduling cleanup task in " + delay + " ms");
|
||||||
|
}
|
||||||
|
taskScheduler.schedule(() -> deleteMessagesAndScheduleNextTask(task),
|
||||||
|
dbExecutor, delay, MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteMessagesAndScheduleNextTask(CleanupTask task) {
|
||||||
|
try {
|
||||||
|
synchronized (lock) {
|
||||||
|
pending.remove(task);
|
||||||
|
}
|
||||||
|
long deadline = db.transactionWithResult(false, txn -> {
|
||||||
|
deleteMessages(txn);
|
||||||
|
return db.getNextCleanupDeadline(txn);
|
||||||
|
});
|
||||||
|
if (deadline != NO_CLEANUP_DEADLINE) {
|
||||||
|
maybeScheduleTask(deadline);
|
||||||
|
}
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteMessages(Transaction txn) throws DbException {
|
||||||
|
Map<GroupId, Collection<MessageId>> ids = db.getMessagesToDelete(txn);
|
||||||
|
for (Entry<GroupId, Collection<MessageId>> e : ids.entrySet()) {
|
||||||
|
GroupId groupId = e.getKey();
|
||||||
|
Collection<MessageId> messageIds = e.getValue();
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info(messageIds.size() + " messages to delete");
|
||||||
|
}
|
||||||
|
for (MessageId m : messageIds) db.stopCleanupTimer(txn, m);
|
||||||
|
Group group = db.getGroup(txn, groupId);
|
||||||
|
ClientMajorVersion cv = new ClientMajorVersion(group.getClientId(),
|
||||||
|
group.getMajorVersion());
|
||||||
|
CleanupHook hook = hooks.get(cv);
|
||||||
|
if (hook == null) {
|
||||||
|
throw new IllegalStateException("No cleanup hook for " + cv);
|
||||||
|
}
|
||||||
|
hook.deleteMessages(txn, groupId, messageIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CleanupTask {
|
||||||
|
|
||||||
|
private final long deadline;
|
||||||
|
|
||||||
|
private CleanupTask(long deadline) {
|
||||||
|
this.deadline = deadline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.briarproject.bramble.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.CleanupManager;
|
||||||
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class CleanupModule {
|
||||||
|
|
||||||
|
public static class EagerSingletons {
|
||||||
|
@Inject
|
||||||
|
CleanupManager cleanupManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
CleanupManager provideCleanupManager(LifecycleManager lifecycleManager,
|
||||||
|
EventBus eventBus, CleanupManagerImpl cleanupManager) {
|
||||||
|
lifecycleManager.registerService(cleanupManager);
|
||||||
|
eventBus.addListener(cleanupManager);
|
||||||
|
return cleanupManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@ package org.briarproject.bramble.client;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
import org.briarproject.bramble.api.client.ClientHelper;
|
import org.briarproject.bramble.api.client.ClientHelper;
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.bramble.api.crypto.KeyParser;
|
import org.briarproject.bramble.api.crypto.KeyParser;
|
||||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
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.data.BdfList;
|
||||||
import org.briarproject.bramble.api.data.BdfReader;
|
import org.briarproject.bramble.api.data.BdfReader;
|
||||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||||
@@ -32,6 +34,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
@@ -39,6 +42,7 @@ import java.util.Map.Entry;
|
|||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
|
||||||
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||||
@@ -151,6 +155,12 @@ class ClientHelperImpl implements ClientHelper {
|
|||||||
return metadataParser.parse(metadata);
|
return metadataParser.parse(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||||
|
BdfDictionary query) throws DbException, FormatException {
|
||||||
|
return db.getMessageIds(txn, g, metadataEncoder.encode(query));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
||||||
throws DbException, FormatException {
|
throws DbException, FormatException {
|
||||||
@@ -389,4 +399,27 @@ class ClientHelperImpl implements ClientHelper {
|
|||||||
return tpMap;
|
return tpMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||||
|
throws DbException {
|
||||||
|
try {
|
||||||
|
BdfDictionary meta =
|
||||||
|
getGroupMetadataAsDictionary(txn, contactGroupId);
|
||||||
|
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContactId(Transaction txn, GroupId contactGroupId,
|
||||||
|
ContactId c) throws DbException {
|
||||||
|
BdfDictionary meta = BdfDictionary.of(
|
||||||
|
new BdfEntry(GROUP_KEY_CONTACT_ID, c.getInt()));
|
||||||
|
try {
|
||||||
|
mergeGroupMetadata(txn, contactGroupId, meta);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ interface Database<T> {
|
|||||||
*/
|
*/
|
||||||
void close() throws DbException;
|
void close() throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the dirty flag was set while opening the database,
|
||||||
|
* indicating that the database has not been shut down properly the last
|
||||||
|
* time it was closed and some data could be lost.
|
||||||
|
*/
|
||||||
|
boolean wasDirtyOnInitialisation();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new transaction and returns an object representing it.
|
* Starts a new transaction and returns an object representing it.
|
||||||
*/
|
*/
|
||||||
@@ -497,6 +504,25 @@ interface Database<T> {
|
|||||||
*/
|
*/
|
||||||
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
|
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IDs of any messages of any messages that are due for
|
||||||
|
* deletion, along with their group IDs.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
Map<GroupId, Collection<MessageId>> getMessagesToDelete(T txn)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
|
* message is due to be deleted, or
|
||||||
|
* {@link DatabaseComponent#NO_CLEANUP_DEADLINE} if no messages are
|
||||||
|
* scheduled to be deleted.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
long getNextCleanupDeadline(T txn) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
* message is due to be sent to the given contact. The returned value may
|
* message is due to be sent to the given contact. The returned value may
|
||||||
@@ -606,8 +632,10 @@ interface Database<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks a message as having been seen by the given contact.
|
* Marks a message as having been seen by the given contact.
|
||||||
|
*
|
||||||
|
* @return True if the message was not already marked as seen.
|
||||||
*/
|
*/
|
||||||
void raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
|
boolean raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a contact from the database.
|
* Removes a contact from the database.
|
||||||
@@ -671,6 +699,13 @@ interface Database<T> {
|
|||||||
*/
|
*/
|
||||||
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
|
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cleanup timer duration for the given message. This does not
|
||||||
|
* start the message's cleanup timer.
|
||||||
|
*/
|
||||||
|
void setCleanupTimerDuration(T txn, MessageId m, long duration)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given contact as verified.
|
* Marks the given contact as verified.
|
||||||
*/
|
*/
|
||||||
@@ -701,9 +736,10 @@ interface Database<T> {
|
|||||||
void setMessagePermanent(T txn, MessageId m) throws DbException;
|
void setMessagePermanent(T txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given message as shared.
|
* Marks the given message as shared or not.
|
||||||
*/
|
*/
|
||||||
void setMessageShared(T txn, MessageId m) throws DbException;
|
void setMessageShared(T txn, MessageId m, boolean shared)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the validation and delivery state of the given message.
|
* Sets the validation and delivery state of the given message.
|
||||||
@@ -730,6 +766,22 @@ interface Database<T> {
|
|||||||
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
|
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the cleanup timer for the given message, if a timer duration
|
||||||
|
* has been set and the timer has not already been started.
|
||||||
|
*
|
||||||
|
* @return The cleanup deadline, or
|
||||||
|
* {@link DatabaseComponent#TIMER_NOT_STARTED} if no timer duration has
|
||||||
|
* been set for this message or its timer has already been started.
|
||||||
|
*/
|
||||||
|
long startCleanupTimer(T txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the cleanup timer for the given message, if the timer has been
|
||||||
|
* started.
|
||||||
|
*/
|
||||||
|
void stopCleanupTimer(T txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the transmission count, expiry time and estimated time of arrival
|
* Updates the transmission count, expiry time and estimated time of arrival
|
||||||
* of the given message with respect to the given contact, using the latency
|
* of the given message with respect to the given contact, using the latency
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.briarproject.bramble.db;
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.PendingContact;
|
import org.briarproject.bramble.api.contact.PendingContact;
|
||||||
import org.briarproject.bramble.api.contact.PendingContactId;
|
import org.briarproject.bramble.api.contact.PendingContactId;
|
||||||
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
||||||
|
import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent;
|
||||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||||
import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent;
|
import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent;
|
||||||
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
|
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
|
||||||
@@ -576,6 +578,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
return db.getMessageIds(txn, g);
|
return db.getMessageIds(txn, g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MessageId> getMessageIds(Transaction transaction,
|
||||||
|
GroupId g, Metadata query) throws DbException {
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsGroup(txn, g))
|
||||||
|
throw new NoSuchGroupException();
|
||||||
|
return db.getMessageIds(txn, g, query);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
|
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -597,6 +608,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
return db.getMessagesToShare(txn);
|
return db.getMessagesToShare(txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
|
||||||
|
Transaction transaction) throws DbException {
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
return db.getMessagesToDelete(txn);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
|
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
|
||||||
GroupId g) throws DbException {
|
GroupId g) throws DbException {
|
||||||
@@ -692,6 +710,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
return db.getMessageDependents(txn, m);
|
return db.getMessageDependents(txn, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getNextCleanupDeadline(Transaction transaction)
|
||||||
|
throws DbException {
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
return db.getNextCleanupDeadline(txn);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getNextSendTime(Transaction transaction, ContactId c)
|
public long getNextSendTime(Transaction transaction, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -795,8 +820,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
Collection<MessageId> acked = new ArrayList<>();
|
Collection<MessageId> acked = new ArrayList<>();
|
||||||
for (MessageId m : a.getMessageIds()) {
|
for (MessageId m : a.getMessageIds()) {
|
||||||
if (db.containsVisibleMessage(txn, c, m)) {
|
if (db.containsVisibleMessage(txn, c, m)) {
|
||||||
db.raiseSeenFlag(txn, c, m);
|
if (db.raiseSeenFlag(txn, c, m)) {
|
||||||
acked.add(m);
|
// This is the first time the message has been acked by
|
||||||
|
// this contact. Start the cleanup timer (a no-op unless
|
||||||
|
// a cleanup deadline has been set for this message)
|
||||||
|
long deadline = db.startCleanupTimer(txn, m);
|
||||||
|
if (deadline != TIMER_NOT_STARTED) {
|
||||||
|
transaction.attach(new CleanupTimerStartedEvent(m,
|
||||||
|
deadline));
|
||||||
|
}
|
||||||
|
acked.add(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (acked.size() > 0) {
|
if (acked.size() > 0) {
|
||||||
@@ -952,6 +986,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
db.removeTransportKeys(txn, t, k);
|
db.removeTransportKeys(txn, t, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCleanupTimerDuration(Transaction transaction, MessageId m,
|
||||||
|
long duration) throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
db.setCleanupTimerDuration(txn, m, duration);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContactVerified(Transaction transaction, ContactId c)
|
public void setContactVerified(Transaction transaction, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -970,6 +1014,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
T txn = unbox(transaction);
|
T txn = unbox(transaction);
|
||||||
if (!db.containsContact(txn, c))
|
if (!db.containsContact(txn, c))
|
||||||
throw new NoSuchContactException();
|
throw new NoSuchContactException();
|
||||||
|
transaction.attach(new ContactAliasChangedEvent(c, alias));
|
||||||
db.setContactAlias(txn, c, alias);
|
db.setContactAlias(txn, c, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,6 +1046,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
db.setMessagePermanent(txn, m);
|
db.setMessagePermanent(txn, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMessageNotShared(Transaction transaction, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
db.setMessageShared(txn, m, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageShared(Transaction transaction, MessageId m)
|
public void setMessageShared(Transaction transaction, MessageId m)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -1010,7 +1065,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
throw new NoSuchMessageException();
|
throw new NoSuchMessageException();
|
||||||
if (db.getMessageState(txn, m) != DELIVERED)
|
if (db.getMessageState(txn, m) != DELIVERED)
|
||||||
throw new IllegalArgumentException("Shared undelivered message");
|
throw new IllegalArgumentException("Shared undelivered message");
|
||||||
db.setMessageShared(txn, m);
|
db.setMessageShared(txn, m, true);
|
||||||
transaction.attach(new MessageSharedEvent(m));
|
transaction.attach(new MessageSharedEvent(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1082,6 +1137,30 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
db.setTransportKeysActive(txn, t, k);
|
db.setTransportKeysActive(txn, t, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long startCleanupTimer(Transaction transaction, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
long deadline = db.startCleanupTimer(txn, m);
|
||||||
|
if (deadline != TIMER_NOT_STARTED) {
|
||||||
|
transaction.attach(new CleanupTimerStartedEvent(m, deadline));
|
||||||
|
}
|
||||||
|
return deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopCleanupTimer(Transaction transaction, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
db.stopCleanupTimer(txn, m);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateTransportKeys(Transaction transaction,
|
public void updateTransportKeys(Transaction transaction,
|
||||||
Collection<TransportKeySet> keys) throws DbException {
|
Collection<TransportKeySet> keys) throws DbException {
|
||||||
|
|||||||
@@ -37,4 +37,10 @@ interface DatabaseConstants {
|
|||||||
* has passed since the last compaction.
|
* has passed since the last compaction.
|
||||||
*/
|
*/
|
||||||
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
|
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Settings} key under which the flag is stored indicating
|
||||||
|
* whether the database is marked as dirty.
|
||||||
|
*/
|
||||||
|
String DIRTY_KEY = "dirty";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,14 @@ class H2Database extends JdbcDatabase {
|
|||||||
@Override
|
@Override
|
||||||
public void close() throws DbException {
|
public void close() throws DbException {
|
||||||
// H2 will close the database when the last connection closes
|
// H2 will close the database when the last connection closes
|
||||||
|
Connection c = null;
|
||||||
try {
|
try {
|
||||||
|
c = createConnection();
|
||||||
super.closeAllConnections();
|
super.closeAllConnections();
|
||||||
|
setDirty(c, false);
|
||||||
|
c.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
tryToClose(c, LOG, WARNING);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class HyperSqlDatabase extends JdbcDatabase {
|
|||||||
try {
|
try {
|
||||||
super.closeAllConnections();
|
super.closeAllConnections();
|
||||||
c = createConnection();
|
c = createConnection();
|
||||||
|
setDirty(c, false);
|
||||||
s = c.createStatement();
|
s = c.createStatement();
|
||||||
s.executeQuery("SHUTDOWN");
|
s.executeQuery("SHUTDOWN");
|
||||||
s.close();
|
s.close();
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ import static java.util.Arrays.asList;
|
|||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
@@ -81,6 +83,7 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERE
|
|||||||
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
|
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
|
||||||
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
|
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
|
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
|
||||||
|
import static org.briarproject.bramble.db.DatabaseConstants.DIRTY_KEY;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
|
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
|
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
|
||||||
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
|
||||||
@@ -98,7 +101,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
|
|||||||
abstract class JdbcDatabase implements Database<Connection> {
|
abstract class JdbcDatabase implements Database<Connection> {
|
||||||
|
|
||||||
// Package access for testing
|
// Package access for testing
|
||||||
static final int CODE_SCHEMA_VERSION = 47;
|
static final int CODE_SCHEMA_VERSION = 48;
|
||||||
|
|
||||||
// Time period offsets for incoming transport keys
|
// Time period offsets for incoming transport keys
|
||||||
private static final int OFFSET_PREV = -1;
|
private static final int OFFSET_PREV = -1;
|
||||||
@@ -180,6 +183,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
+ " state INT NOT NULL,"
|
+ " state INT NOT NULL,"
|
||||||
+ " shared BOOLEAN NOT NULL,"
|
+ " shared BOOLEAN NOT NULL,"
|
||||||
+ " temporary BOOLEAN NOT NULL,"
|
+ " temporary BOOLEAN NOT NULL,"
|
||||||
|
// Null if no timer duration has been set
|
||||||
|
+ " cleanupTimerDuration BIGINT,"
|
||||||
|
// Null if no timer duration has been set or the timer
|
||||||
|
// hasn't started
|
||||||
|
+ " cleanupDeadline BIGINT,"
|
||||||
+ " length INT NOT NULL,"
|
+ " length INT NOT NULL,"
|
||||||
+ " raw BLOB," // Null if message has been deleted
|
+ " raw BLOB," // Null if message has been deleted
|
||||||
+ " PRIMARY KEY (messageId),"
|
+ " PRIMARY KEY (messageId),"
|
||||||
@@ -336,6 +344,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
|
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
|
||||||
+ " ON statuses (contactId, timestamp)";
|
+ " ON statuses (contactId, timestamp)";
|
||||||
|
|
||||||
|
private static final String INDEX_MESSAGES_BY_CLEANUP_DEADLINE =
|
||||||
|
"CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
|
||||||
|
+ " ON messages (cleanupDeadline)";
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(JdbcDatabase.class.getName());
|
getLogger(JdbcDatabase.class.getName());
|
||||||
|
|
||||||
@@ -354,9 +366,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
@GuardedBy("connectionsLock")
|
@GuardedBy("connectionsLock")
|
||||||
private boolean closed = false;
|
private boolean closed = false;
|
||||||
|
|
||||||
|
private volatile boolean wasDirtyOnInitialisation = false;
|
||||||
|
|
||||||
protected abstract Connection createConnection()
|
protected abstract Connection createConnection()
|
||||||
throws DbException, SQLException;
|
throws DbException, SQLException;
|
||||||
|
|
||||||
|
// Used exclusively during open to compact the database after schema
|
||||||
|
// migrations or after DatabaseConstants#MAX_COMPACTION_INTERVAL_MS has
|
||||||
|
// elapsed
|
||||||
protected abstract void compactAndClose() throws DbException;
|
protected abstract void compactAndClose() throws DbException;
|
||||||
|
|
||||||
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
|
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
|
||||||
@@ -381,13 +398,19 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
try {
|
try {
|
||||||
if (reopen) {
|
if (reopen) {
|
||||||
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
|
||||||
|
wasDirtyOnInitialisation = isDirty(s);
|
||||||
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
|
compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
|
||||||
} else {
|
} else {
|
||||||
|
wasDirtyOnInitialisation = false;
|
||||||
createTables(txn);
|
createTables(txn);
|
||||||
initialiseSettings(txn);
|
initialiseSettings(txn);
|
||||||
compact = false;
|
compact = false;
|
||||||
}
|
}
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("db dirty? " + wasDirtyOnInitialisation);
|
||||||
|
}
|
||||||
createIndexes(txn);
|
createIndexes(txn);
|
||||||
|
setDirty(txn, true);
|
||||||
commitTransaction(txn);
|
commitTransaction(txn);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
abortTransaction(txn);
|
abortTransaction(txn);
|
||||||
@@ -414,6 +437,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wasDirtyOnInitialisation() {
|
||||||
|
return wasDirtyOnInitialisation;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares the schema version stored in the database with the schema
|
* Compares the schema version stored in the database with the schema
|
||||||
* version used by the current code and applies any suitable migrations to
|
* version used by the current code and applies any suitable migrations to
|
||||||
@@ -463,7 +491,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
new Migration43_44(dbTypes),
|
new Migration43_44(dbTypes),
|
||||||
new Migration44_45(),
|
new Migration44_45(),
|
||||||
new Migration45_46(),
|
new Migration45_46(),
|
||||||
new Migration46_47(dbTypes)
|
new Migration46_47(dbTypes),
|
||||||
|
new Migration47_48()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +517,16 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isDirty(Settings s) {
|
||||||
|
return s.getBoolean(DIRTY_KEY, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setDirty(Connection txn, boolean dirty) throws DbException {
|
||||||
|
Settings s = new Settings();
|
||||||
|
s.putBoolean(DIRTY_KEY, dirty);
|
||||||
|
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
|
||||||
|
}
|
||||||
|
|
||||||
private void initialiseSettings(Connection txn) throws DbException {
|
private void initialiseSettings(Connection txn) throws DbException {
|
||||||
Settings s = new Settings();
|
Settings s = new Settings();
|
||||||
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
|
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
|
||||||
@@ -531,6 +570,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
|
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
|
||||||
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
|
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
|
||||||
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
|
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
|
||||||
|
s.executeUpdate(INDEX_MESSAGES_BY_CLEANUP_DEADLINE);
|
||||||
s.close();
|
s.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(s, LOG, WARNING);
|
tryToClose(s, LOG, WARNING);
|
||||||
@@ -1290,7 +1330,9 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
public void deleteMessage(Connection txn, MessageId m) throws DbException {
|
public void deleteMessage(Connection txn, MessageId m) throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?";
|
String sql = "UPDATE messages"
|
||||||
|
+ " SET raw = NULL, cleanupDeadline = NULL"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, m.getBytes());
|
ps.setBytes(1, m.getBytes());
|
||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
@@ -1769,7 +1811,6 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
// Return early if there are no matches
|
// Return early if there are no matches
|
||||||
if (intersection.isEmpty()) return Collections.emptySet();
|
if (intersection.isEmpty()) return Collections.emptySet();
|
||||||
}
|
}
|
||||||
if (intersection == null) throw new AssertionError();
|
|
||||||
return intersection;
|
return intersection;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(rs, LOG, WARNING);
|
tryToClose(rs, LOG, WARNING);
|
||||||
@@ -2226,6 +2267,39 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
|
||||||
|
Connection txn) throws DbException {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT messageId, groupId FROM messages"
|
||||||
|
+ " WHERE cleanupDeadline <= ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setLong(1, now);
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
Map<GroupId, Collection<MessageId>> ids = new HashMap<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
MessageId m = new MessageId(rs.getBytes(1));
|
||||||
|
GroupId g = new GroupId(rs.getBytes(2));
|
||||||
|
Collection<MessageId> messageIds = ids.get(g);
|
||||||
|
if (messageIds == null) {
|
||||||
|
messageIds = new ArrayList<>();
|
||||||
|
ids.put(g, messageIds);
|
||||||
|
}
|
||||||
|
messageIds.add(m);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return ids;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs, LOG, WARNING);
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getNextSendTime(Connection txn, ContactId c)
|
public long getNextSendTime(Connection txn, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -2256,6 +2330,31 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getNextCleanupDeadline(Connection txn) throws DbException {
|
||||||
|
Statement s = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT cleanupDeadline FROM messages"
|
||||||
|
+ " WHERE cleanupDeadline IS NOT NULL"
|
||||||
|
+ " ORDER BY cleanupDeadline LIMIT 1";
|
||||||
|
s = txn.createStatement();
|
||||||
|
rs = s.executeQuery(sql);
|
||||||
|
long nextDeadline = NO_CLEANUP_DEADLINE;
|
||||||
|
if (rs.next()) {
|
||||||
|
nextDeadline = rs.getLong(1);
|
||||||
|
if (rs.next()) throw new AssertionError();
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
s.close();
|
||||||
|
return nextDeadline;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs, LOG, WARNING);
|
||||||
|
tryToClose(s, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PendingContact getPendingContact(Connection txn, PendingContactId p)
|
public PendingContact getPendingContact(Connection txn, PendingContactId p)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -2776,7 +2875,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void raiseSeenFlag(Connection txn, ContactId c, MessageId m)
|
public boolean raiseSeenFlag(Connection txn, ContactId c, MessageId m)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
@@ -2788,6 +2887,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
|
return affected == 1;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(ps, LOG, WARNING);
|
tryToClose(ps, LOG, WARNING);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
@@ -3021,6 +3121,25 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCleanupTimerDuration(Connection txn, MessageId m,
|
||||||
|
long duration) throws DbException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "UPDATE messages SET cleanupTimerDuration = ?"
|
||||||
|
+ " WHERE messageId = ? AND cleanupTimerDuration IS NULL";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setLong(1, duration);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContactVerified(Connection txn, ContactId c)
|
public void setContactVerified(Connection txn, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -3128,22 +3247,24 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageShared(Connection txn, MessageId m)
|
public void setMessageShared(Connection txn, MessageId m, boolean shared)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
String sql = "UPDATE messages SET shared = TRUE"
|
String sql = "UPDATE messages SET shared = ?"
|
||||||
+ " WHERE messageId = ?";
|
+ " WHERE messageId = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, m.getBytes());
|
ps.setBoolean(1, shared);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
// Update denormalised column in statuses
|
// Update denormalised column in statuses
|
||||||
sql = "UPDATE statuses SET messageShared = TRUE"
|
sql = "UPDATE statuses SET messageShared = ?"
|
||||||
+ " WHERE messageId = ?";
|
+ " WHERE messageId = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, m.getBytes());
|
ps.setBoolean(1, shared);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
affected = ps.executeUpdate();
|
affected = ps.executeUpdate();
|
||||||
if (affected < 0) throw new DbStateException();
|
if (affected < 0) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
@@ -3272,6 +3393,60 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long startCleanupTimer(Connection txn, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "UPDATE messages"
|
||||||
|
+ " SET cleanupDeadline = ? + cleanupTimerDuration"
|
||||||
|
+ " WHERE messageId = ?"
|
||||||
|
+ " AND cleanupTimerDuration IS NOT NULL"
|
||||||
|
+ " AND cleanupDeadline IS NULL";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setLong(1, now);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
if (affected == 0) return TIMER_NOT_STARTED;
|
||||||
|
sql = "SELECT cleanupDeadline FROM messages WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
long deadline = rs.getLong(1);
|
||||||
|
if (rs.next()) throw new DbStateException();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return deadline;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs, LOG, WARNING);
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopCleanupTimer(Connection txn, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "UPDATE messages SET cleanupDeadline = NULL"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
|
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
|
||||||
int maxLatency) throws DbException {
|
int maxLatency) throws DbException {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
|
||||||
|
|
||||||
|
class Migration47_48 implements Migration<Connection> {
|
||||||
|
|
||||||
|
private static final Logger LOG = getLogger(Migration47_48.class.getName());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStartVersion() {
|
||||||
|
return 47;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getEndVersion() {
|
||||||
|
return 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(Connection txn) throws DbException {
|
||||||
|
Statement s = null;
|
||||||
|
try {
|
||||||
|
s = txn.createStatement();
|
||||||
|
// Null if no timer duration has been set
|
||||||
|
s.execute("ALTER TABLE messages"
|
||||||
|
+ " ADD COLUMN cleanupTimerDuration BIGINT");
|
||||||
|
// Null if no timer duration has been set or the timer
|
||||||
|
// hasn't started
|
||||||
|
s.execute("ALTER TABLE messages"
|
||||||
|
+ " ADD COLUMN cleanupDeadline BIGINT");
|
||||||
|
s.execute("CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
|
||||||
|
+ " ON messages (cleanupDeadline)");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(s, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,12 +34,12 @@ class KeyAgreementTransport {
|
|||||||
Logger.getLogger(KeyAgreementTransport.class.getName());
|
Logger.getLogger(KeyAgreementTransport.class.getName());
|
||||||
|
|
||||||
// Accept records with current protocol version, known record type
|
// Accept records with current protocol version, known record type
|
||||||
private static Predicate<Record> ACCEPT = r ->
|
private static final Predicate<Record> ACCEPT = r ->
|
||||||
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
||||||
isKnownRecordType(r.getRecordType());
|
isKnownRecordType(r.getRecordType());
|
||||||
|
|
||||||
// Ignore records with current protocol version, unknown record type
|
// Ignore records with current protocol version, unknown record type
|
||||||
private static Predicate<Record> IGNORE = r ->
|
private static final Predicate<Record> IGNORE = r ->
|
||||||
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
r.getProtocolVersion() == PROTOCOL_VERSION &&
|
||||||
!isKnownRecordType(r.getRecordType());
|
!isKnownRecordType(r.getRecordType());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,633 @@
|
|||||||
|
package org.briarproject.bramble.plugin.bluetooth;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.FormatException;
|
||||||
|
import org.briarproject.bramble.api.Multiset;
|
||||||
|
import org.briarproject.bramble.api.Pair;
|
||||||
|
import org.briarproject.bramble.api.data.BdfList;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.plugin.Backoff;
|
||||||
|
import org.briarproject.bramble.api.plugin.ConnectionHandler;
|
||||||
|
import org.briarproject.bramble.api.plugin.PluginCallback;
|
||||||
|
import org.briarproject.bramble.api.plugin.PluginException;
|
||||||
|
import org.briarproject.bramble.api.plugin.TransportId;
|
||||||
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
|
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||||
|
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
|
||||||
|
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
|
||||||
|
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
|
||||||
|
import org.briarproject.bramble.api.settings.Settings;
|
||||||
|
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.INFO;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
|
||||||
|
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.macToBytes;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.macToString;
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
|
abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
|
||||||
|
EventListener {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
getLogger(AbstractBluetoothPlugin.class.getName());
|
||||||
|
|
||||||
|
private final BluetoothConnectionLimiter connectionLimiter;
|
||||||
|
final BluetoothConnectionFactory<S> connectionFactory;
|
||||||
|
|
||||||
|
private final Executor ioExecutor, wakefulIoExecutor;
|
||||||
|
private final SecureRandom secureRandom;
|
||||||
|
private final Backoff backoff;
|
||||||
|
private final PluginCallback callback;
|
||||||
|
private final int maxLatency, maxIdleTime;
|
||||||
|
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean everConnected = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
protected final PluginState state = new PluginState();
|
||||||
|
protected final Semaphore discoverSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
|
private volatile String contactConnectionsUuid = null;
|
||||||
|
|
||||||
|
abstract void initialiseAdapter() throws IOException;
|
||||||
|
|
||||||
|
abstract boolean isAdapterEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local Bluetooth address, or null if no valid address can
|
||||||
|
* be found.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
abstract String getBluetoothAddress();
|
||||||
|
|
||||||
|
abstract SS openServerSocket(String uuid) throws IOException;
|
||||||
|
|
||||||
|
abstract void tryToClose(@Nullable SS ss);
|
||||||
|
|
||||||
|
abstract DuplexTransportConnection acceptConnection(SS ss)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
abstract boolean isValidAddress(String address);
|
||||||
|
|
||||||
|
abstract DuplexTransportConnection connectTo(String address, String uuid)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
abstract DuplexTransportConnection discoverAndConnect(String uuid);
|
||||||
|
|
||||||
|
AbstractBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
||||||
|
BluetoothConnectionFactory<S> connectionFactory,
|
||||||
|
Executor ioExecutor,
|
||||||
|
Executor wakefulIoExecutor,
|
||||||
|
SecureRandom secureRandom,
|
||||||
|
Backoff backoff,
|
||||||
|
PluginCallback callback,
|
||||||
|
int maxLatency,
|
||||||
|
int maxIdleTime) {
|
||||||
|
this.connectionLimiter = connectionLimiter;
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
this.ioExecutor = ioExecutor;
|
||||||
|
this.wakefulIoExecutor = wakefulIoExecutor;
|
||||||
|
this.secureRandom = secureRandom;
|
||||||
|
this.backoff = backoff;
|
||||||
|
this.callback = callback;
|
||||||
|
this.maxLatency = maxLatency;
|
||||||
|
this.maxIdleTime = maxIdleTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAdapterEnabled() {
|
||||||
|
LOG.info("Bluetooth enabled");
|
||||||
|
// We may not have been able to get the local address before
|
||||||
|
ioExecutor.execute(this::updateProperties);
|
||||||
|
if (getState() == INACTIVE) bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAdapterDisabled() {
|
||||||
|
LOG.info("Bluetooth disabled");
|
||||||
|
connectionLimiter.allConnectionsClosed();
|
||||||
|
// The server socket may not have been closed automatically
|
||||||
|
SS ss = state.clearServerSocket();
|
||||||
|
if (ss != null) {
|
||||||
|
LOG.info("Closing server socket");
|
||||||
|
tryToClose(ss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransportId getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxLatency() {
|
||||||
|
return maxLatency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxIdleTime() {
|
||||||
|
return maxIdleTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws PluginException {
|
||||||
|
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||||
|
Settings settings = callback.getSettings();
|
||||||
|
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
||||||
|
DEFAULT_PREF_PLUGIN_ENABLE);
|
||||||
|
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
|
||||||
|
DEFAULT_PREF_EVER_CONNECTED));
|
||||||
|
state.setStarted(enabledByUser);
|
||||||
|
try {
|
||||||
|
initialiseAdapter();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PluginException(e);
|
||||||
|
}
|
||||||
|
updateProperties();
|
||||||
|
if (enabledByUser && isAdapterEnabled()) bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind() {
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
if (getState() != INACTIVE) return;
|
||||||
|
// Bind a server socket to accept connections from contacts
|
||||||
|
SS ss;
|
||||||
|
try {
|
||||||
|
ss = openServerSocket(contactConnectionsUuid);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.setServerSocket(ss)) {
|
||||||
|
LOG.info("Closing redundant server socket");
|
||||||
|
tryToClose(ss);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backoff.reset();
|
||||||
|
acceptContactConnections(ss);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateProperties() {
|
||||||
|
TransportProperties p = callback.getLocalProperties();
|
||||||
|
String address = p.get(PROP_ADDRESS);
|
||||||
|
String uuid = p.get(PROP_UUID);
|
||||||
|
Settings s = callback.getSettings();
|
||||||
|
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
|
||||||
|
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
|
||||||
|
boolean changed = false;
|
||||||
|
if (address == null || isReflected) {
|
||||||
|
address = getBluetoothAddress();
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Local address " + scrubMacAddress(address));
|
||||||
|
}
|
||||||
|
if (address == null) {
|
||||||
|
if (everConnected.get()) {
|
||||||
|
address = getReflectedAddress();
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Reflected address " +
|
||||||
|
scrubMacAddress(address));
|
||||||
|
}
|
||||||
|
if (address != null) {
|
||||||
|
changed = true;
|
||||||
|
isReflected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changed = true;
|
||||||
|
isReflected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uuid == null) {
|
||||||
|
byte[] random = new byte[UUID_BYTES];
|
||||||
|
secureRandom.nextBytes(random);
|
||||||
|
uuid = UUID.nameUUIDFromBytes(random).toString();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
contactConnectionsUuid = uuid;
|
||||||
|
if (changed) {
|
||||||
|
p = new TransportProperties();
|
||||||
|
// If we previously used a reflected address and there's no longer
|
||||||
|
// a reflected address with enough votes to be used, we'll continue
|
||||||
|
// to use the old reflected address until there's a new winner
|
||||||
|
if (address != null) p.put(PROP_ADDRESS, address);
|
||||||
|
p.put(PROP_UUID, uuid);
|
||||||
|
callback.mergeLocalProperties(p);
|
||||||
|
s = new Settings();
|
||||||
|
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
|
||||||
|
callback.mergeSettings(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String getReflectedAddress() {
|
||||||
|
// Count the number of votes for each reflected address
|
||||||
|
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
|
||||||
|
Multiset<String> votes = new Multiset<>();
|
||||||
|
for (TransportProperties p : callback.getRemoteProperties()) {
|
||||||
|
String address = p.get(key);
|
||||||
|
if (address != null && isValidAddress(address)) votes.add(address);
|
||||||
|
}
|
||||||
|
// If an address gets more than half of the votes, accept it
|
||||||
|
int total = votes.getTotal();
|
||||||
|
for (String address : votes.keySet()) {
|
||||||
|
if (votes.getCount(address) * 2 > total) return address;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptContactConnections(SS ss) {
|
||||||
|
while (true) {
|
||||||
|
DuplexTransportConnection conn;
|
||||||
|
try {
|
||||||
|
conn = acceptConnection(ss);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This is expected when the server socket is closed
|
||||||
|
LOG.info("Server socket closed");
|
||||||
|
state.clearServerSocket();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG.info("Connection received");
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
backoff.reset();
|
||||||
|
setEverConnected();
|
||||||
|
callback.handleConnection(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setEverConnected() {
|
||||||
|
if (!everConnected.getAndSet(true)) {
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
Settings s = new Settings();
|
||||||
|
s.putBoolean(PREF_EVER_CONNECTED, true);
|
||||||
|
callback.mergeSettings(s);
|
||||||
|
// Contacts may already have sent a reflected address
|
||||||
|
updateProperties();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
SS ss = state.setStopped();
|
||||||
|
tryToClose(ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getState() {
|
||||||
|
return state.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getReasonsDisabled() {
|
||||||
|
return state.getReasonsDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldPoll() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPollingInterval() {
|
||||||
|
return backoff.getPollingInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
|
||||||
|
properties) {
|
||||||
|
if (getState() != ACTIVE) return;
|
||||||
|
backoff.increment();
|
||||||
|
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
|
||||||
|
connect(p.getFirst(), p.getSecond());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect(TransportProperties p, ConnectionHandler h) {
|
||||||
|
String address = p.get(PROP_ADDRESS);
|
||||||
|
if (isNullOrEmpty(address)) return;
|
||||||
|
String uuid = p.get(PROP_UUID);
|
||||||
|
if (isNullOrEmpty(uuid)) return;
|
||||||
|
wakefulIoExecutor.execute(() -> {
|
||||||
|
DuplexTransportConnection d = createConnection(p);
|
||||||
|
if (d != null) {
|
||||||
|
backoff.reset();
|
||||||
|
setEverConnected();
|
||||||
|
h.handleConnection(d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private DuplexTransportConnection connect(String address, String uuid) {
|
||||||
|
// Validate the address
|
||||||
|
if (!isValidAddress(address)) {
|
||||||
|
if (LOG.isLoggable(WARNING))
|
||||||
|
// Not scrubbing here to be able to figure out the problem
|
||||||
|
LOG.warning("Invalid address " + address);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Validate the UUID
|
||||||
|
try {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connecting to " + scrubMacAddress(address));
|
||||||
|
try {
|
||||||
|
DuplexTransportConnection conn = connectTo(address, uuid);
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connected to " + scrubMacAddress(address));
|
||||||
|
return conn;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Could not connect to " + scrubMacAddress(address));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DuplexTransportConnection createConnection(TransportProperties p) {
|
||||||
|
if (getState() != ACTIVE) return null;
|
||||||
|
if (!connectionLimiter.canOpenContactConnection()) return null;
|
||||||
|
String address = p.get(PROP_ADDRESS);
|
||||||
|
if (isNullOrEmpty(address)) return null;
|
||||||
|
String uuid = p.get(PROP_UUID);
|
||||||
|
if (isNullOrEmpty(uuid)) return null;
|
||||||
|
DuplexTransportConnection conn = connect(address, uuid);
|
||||||
|
if (conn != null) connectionLimiter.connectionOpened(conn);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsKeyAgreement() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
|
||||||
|
if (getState() != ACTIVE) return null;
|
||||||
|
// No truncation necessary because COMMIT_LENGTH = 16
|
||||||
|
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
||||||
|
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
||||||
|
// Bind a server socket for receiving key agreement connections
|
||||||
|
SS ss;
|
||||||
|
try {
|
||||||
|
ss = openServerSocket(uuid);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (getState() != ACTIVE) {
|
||||||
|
tryToClose(ss);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BdfList descriptor = new BdfList();
|
||||||
|
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
||||||
|
String address = getBluetoothAddress();
|
||||||
|
if (address != null) descriptor.add(macToBytes(address));
|
||||||
|
return new BluetoothKeyAgreementListener(descriptor, ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DuplexTransportConnection createKeyAgreementConnection(
|
||||||
|
byte[] commitment, BdfList descriptor) {
|
||||||
|
if (getState() != ACTIVE) return null;
|
||||||
|
// No truncation necessary because COMMIT_LENGTH = 16
|
||||||
|
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
||||||
|
DuplexTransportConnection conn;
|
||||||
|
if (descriptor.size() == 1) {
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Discovering address for key agreement UUID " +
|
||||||
|
uuid);
|
||||||
|
}
|
||||||
|
conn = discoverAndConnect(uuid);
|
||||||
|
} else {
|
||||||
|
String address;
|
||||||
|
try {
|
||||||
|
address = parseAddress(descriptor);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
LOG.info("Invalid address in key agreement descriptor");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connecting to key agreement UUID " + uuid);
|
||||||
|
conn = connect(address, uuid);
|
||||||
|
}
|
||||||
|
if (conn != null) {
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
setEverConnected();
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseAddress(BdfList descriptor) throws FormatException {
|
||||||
|
byte[] mac = descriptor.getRaw(1);
|
||||||
|
if (mac.length != 6) throw new FormatException();
|
||||||
|
return macToString(mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDiscovering() {
|
||||||
|
return discoverSemaphore.availablePermits() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disablePolling() {
|
||||||
|
connectionLimiter.startLimiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enablePolling() {
|
||||||
|
connectionLimiter.endLimiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DuplexTransportConnection discoverAndConnectForSetup(String uuid) {
|
||||||
|
DuplexTransportConnection conn = discoverAndConnect(uuid);
|
||||||
|
if (conn != null) {
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
setEverConnected();
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRendezvous() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
|
||||||
|
boolean alice, ConnectionHandler incoming) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventOccurred(Event e) {
|
||||||
|
if (e instanceof SettingsUpdatedEvent) {
|
||||||
|
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
|
||||||
|
if (s.getNamespace().equals(ID.getString()))
|
||||||
|
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
|
||||||
|
} else if (e instanceof KeyAgreementListeningEvent) {
|
||||||
|
connectionLimiter.startLimiting();
|
||||||
|
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
|
||||||
|
connectionLimiter.endLimiting();
|
||||||
|
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
|
||||||
|
RemoteTransportPropertiesUpdatedEvent r =
|
||||||
|
(RemoteTransportPropertiesUpdatedEvent) e;
|
||||||
|
if (r.getTransportId().equals(ID)) {
|
||||||
|
ioExecutor.execute(this::updateProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IoExecutor
|
||||||
|
private void onSettingsUpdated(Settings settings) {
|
||||||
|
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
||||||
|
DEFAULT_PREF_PLUGIN_ENABLE);
|
||||||
|
SS ss = state.setEnabledByUser(enabledByUser);
|
||||||
|
State s = getState();
|
||||||
|
if (ss != null) {
|
||||||
|
LOG.info("Disabled by user, closing server socket");
|
||||||
|
tryToClose(ss);
|
||||||
|
} else if (s == INACTIVE) {
|
||||||
|
if (isAdapterEnabled()) {
|
||||||
|
LOG.info("Enabled by user, opening server socket");
|
||||||
|
bind();
|
||||||
|
} else {
|
||||||
|
LOG.info("Enabled by user but adapter is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
||||||
|
|
||||||
|
private final SS ss;
|
||||||
|
|
||||||
|
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
|
||||||
|
super(descriptor);
|
||||||
|
this.ss = ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyAgreementConnection accept() throws IOException {
|
||||||
|
DuplexTransportConnection conn = acceptConnection(ss);
|
||||||
|
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
return new KeyAgreementConnection(conn, ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
tryToClose(ss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ThreadSafe
|
||||||
|
@NotNullByDefault
|
||||||
|
private class PluginState {
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
private boolean started = false,
|
||||||
|
stopped = false,
|
||||||
|
enabledByUser = false;
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private SS serverSocket = null;
|
||||||
|
|
||||||
|
private synchronized void setStarted(boolean enabledByUser) {
|
||||||
|
started = true;
|
||||||
|
this.enabledByUser = enabledByUser;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private synchronized SS setStopped() {
|
||||||
|
stopped = true;
|
||||||
|
SS ss = serverSocket;
|
||||||
|
serverSocket = null;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private synchronized SS setEnabledByUser(boolean enabledByUser) {
|
||||||
|
this.enabledByUser = enabledByUser;
|
||||||
|
SS ss = null;
|
||||||
|
if (!enabledByUser) {
|
||||||
|
ss = serverSocket;
|
||||||
|
serverSocket = null;
|
||||||
|
}
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean setServerSocket(SS ss) {
|
||||||
|
if (stopped || serverSocket != null) return false;
|
||||||
|
serverSocket = ss;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private synchronized SS clearServerSocket() {
|
||||||
|
SS ss = serverSocket;
|
||||||
|
serverSocket = null;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized State getState() {
|
||||||
|
if (!started || stopped) return STARTING_STOPPING;
|
||||||
|
if (!enabledByUser) return DISABLED;
|
||||||
|
return serverSocket == null ? INACTIVE : ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized int getReasonsDisabled() {
|
||||||
|
return getState() == DISABLED ? REASON_USER : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,15 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
|||||||
interface BluetoothConnectionLimiter {
|
interface BluetoothConnectionLimiter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Informs the limiter that key agreement has started.
|
* Tells the limiter to not allow regular polling connections (because we
|
||||||
|
* are about to do key agreement, or connect via BT for setup).
|
||||||
*/
|
*/
|
||||||
void keyAgreementStarted();
|
void startLimiting();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Informs the limiter that key agreement has ended.
|
* Tells the limiter to no longer limit regular polling connections.
|
||||||
*/
|
*/
|
||||||
void keyAgreementEnded();
|
void endLimiting();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if a contact connection can be opened. This method does not
|
* Returns true if a contact connection can be opened. This method does not
|
||||||
|
|||||||
@@ -30,34 +30,37 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
|
|||||||
private final List<DuplexTransportConnection> connections =
|
private final List<DuplexTransportConnection> connections =
|
||||||
new LinkedList<>();
|
new LinkedList<>();
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private boolean keyAgreementInProgress = false;
|
private int limitingInProgress = 0;
|
||||||
|
|
||||||
BluetoothConnectionLimiterImpl(EventBus eventBus) {
|
BluetoothConnectionLimiterImpl(EventBus eventBus) {
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void keyAgreementStarted() {
|
public void startLimiting() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
keyAgreementInProgress = true;
|
limitingInProgress++;
|
||||||
}
|
}
|
||||||
LOG.info("Key agreement started");
|
LOG.info("Limiting started");
|
||||||
eventBus.broadcast(new CloseSyncConnectionsEvent(ID));
|
eventBus.broadcast(new CloseSyncConnectionsEvent(ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void keyAgreementEnded() {
|
public void endLimiting() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
keyAgreementInProgress = false;
|
limitingInProgress--;
|
||||||
|
if (limitingInProgress < 0) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LOG.info("Key agreement ended");
|
LOG.info("Limiting ended");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canOpenContactConnection() {
|
public boolean canOpenContactConnection() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (keyAgreementInProgress) {
|
if (limitingInProgress > 0) {
|
||||||
LOG.info("Can't open contact connection during key agreement");
|
LOG.info("Can't open contact connection while limiting");
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
LOG.info("Can open contact connection");
|
LOG.info("Can open contact connection");
|
||||||
|
|||||||
@@ -1,606 +1,22 @@
|
|||||||
package org.briarproject.bramble.plugin.bluetooth;
|
package org.briarproject.bramble.plugin.bluetooth;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
|
||||||
import org.briarproject.bramble.api.Multiset;
|
|
||||||
import org.briarproject.bramble.api.Pair;
|
|
||||||
import org.briarproject.bramble.api.data.BdfList;
|
|
||||||
import org.briarproject.bramble.api.event.Event;
|
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.plugin.Backoff;
|
|
||||||
import org.briarproject.bramble.api.plugin.ConnectionHandler;
|
|
||||||
import org.briarproject.bramble.api.plugin.PluginCallback;
|
|
||||||
import org.briarproject.bramble.api.plugin.PluginException;
|
|
||||||
import org.briarproject.bramble.api.plugin.TransportId;
|
|
||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
|
||||||
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
|
|
||||||
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
|
|
||||||
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
|
|
||||||
import org.briarproject.bramble.api.settings.Settings;
|
|
||||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.GuardedBy;
|
|
||||||
import javax.annotation.concurrent.ThreadSafe;
|
|
||||||
|
|
||||||
import static java.util.logging.Level.INFO;
|
@NotNullByDefault
|
||||||
import static java.util.logging.Level.WARNING;
|
public interface BluetoothPlugin extends DuplexPlugin {
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
|
|
||||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
|
||||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.macToBytes;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.macToString;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
boolean isDiscovering();
|
||||||
@ParametersNotNullByDefault
|
|
||||||
abstract class BluetoothPlugin<S, SS> implements DuplexPlugin, EventListener {
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
void disablePolling();
|
||||||
getLogger(BluetoothPlugin.class.getName());
|
|
||||||
|
|
||||||
final BluetoothConnectionLimiter connectionLimiter;
|
void enablePolling();
|
||||||
final BluetoothConnectionFactory<S> connectionFactory;
|
|
||||||
|
|
||||||
private final Executor ioExecutor, wakefulIoExecutor;
|
|
||||||
private final SecureRandom secureRandom;
|
|
||||||
private final Backoff backoff;
|
|
||||||
private final PluginCallback callback;
|
|
||||||
private final int maxLatency, maxIdleTime;
|
|
||||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
|
||||||
private final AtomicBoolean everConnected = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
protected final PluginState state = new PluginState();
|
|
||||||
|
|
||||||
private volatile String contactConnectionsUuid = null;
|
|
||||||
|
|
||||||
abstract void initialiseAdapter() throws IOException;
|
|
||||||
|
|
||||||
abstract boolean isAdapterEnabled();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the local Bluetooth address, or null if no valid address can
|
|
||||||
* be found.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
abstract String getBluetoothAddress();
|
|
||||||
|
|
||||||
abstract SS openServerSocket(String uuid) throws IOException;
|
|
||||||
|
|
||||||
abstract void tryToClose(@Nullable SS ss);
|
|
||||||
|
|
||||||
abstract DuplexTransportConnection acceptConnection(SS ss)
|
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
abstract boolean isValidAddress(String address);
|
|
||||||
|
|
||||||
abstract DuplexTransportConnection connectTo(String address, String uuid)
|
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
abstract DuplexTransportConnection discoverAndConnect(String uuid);
|
DuplexTransportConnection discoverAndConnectForSetup(String uuid);
|
||||||
|
|
||||||
BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
void stopDiscoverAndConnect();
|
||||||
BluetoothConnectionFactory<S> connectionFactory,
|
|
||||||
Executor ioExecutor,
|
|
||||||
Executor wakefulIoExecutor,
|
|
||||||
SecureRandom secureRandom,
|
|
||||||
Backoff backoff,
|
|
||||||
PluginCallback callback,
|
|
||||||
int maxLatency,
|
|
||||||
int maxIdleTime) {
|
|
||||||
this.connectionLimiter = connectionLimiter;
|
|
||||||
this.connectionFactory = connectionFactory;
|
|
||||||
this.ioExecutor = ioExecutor;
|
|
||||||
this.wakefulIoExecutor = wakefulIoExecutor;
|
|
||||||
this.secureRandom = secureRandom;
|
|
||||||
this.backoff = backoff;
|
|
||||||
this.callback = callback;
|
|
||||||
this.maxLatency = maxLatency;
|
|
||||||
this.maxIdleTime = maxIdleTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAdapterEnabled() {
|
|
||||||
LOG.info("Bluetooth enabled");
|
|
||||||
// We may not have been able to get the local address before
|
|
||||||
ioExecutor.execute(this::updateProperties);
|
|
||||||
if (getState() == INACTIVE) bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAdapterDisabled() {
|
|
||||||
LOG.info("Bluetooth disabled");
|
|
||||||
connectionLimiter.allConnectionsClosed();
|
|
||||||
// The server socket may not have been closed automatically
|
|
||||||
SS ss = state.clearServerSocket();
|
|
||||||
if (ss != null) {
|
|
||||||
LOG.info("Closing server socket");
|
|
||||||
tryToClose(ss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TransportId getId() {
|
|
||||||
return ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMaxLatency() {
|
|
||||||
return maxLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMaxIdleTime() {
|
|
||||||
return maxIdleTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws PluginException {
|
|
||||||
if (used.getAndSet(true)) throw new IllegalStateException();
|
|
||||||
Settings settings = callback.getSettings();
|
|
||||||
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
|
||||||
DEFAULT_PREF_PLUGIN_ENABLE);
|
|
||||||
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
|
|
||||||
DEFAULT_PREF_EVER_CONNECTED));
|
|
||||||
state.setStarted(enabledByUser);
|
|
||||||
try {
|
|
||||||
initialiseAdapter();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new PluginException(e);
|
|
||||||
}
|
|
||||||
updateProperties();
|
|
||||||
if (enabledByUser && isAdapterEnabled()) bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bind() {
|
|
||||||
ioExecutor.execute(() -> {
|
|
||||||
if (getState() != INACTIVE) return;
|
|
||||||
// Bind a server socket to accept connections from contacts
|
|
||||||
SS ss;
|
|
||||||
try {
|
|
||||||
ss = openServerSocket(contactConnectionsUuid);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.setServerSocket(ss)) {
|
|
||||||
LOG.info("Closing redundant server socket");
|
|
||||||
tryToClose(ss);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
backoff.reset();
|
|
||||||
acceptContactConnections(ss);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProperties() {
|
|
||||||
TransportProperties p = callback.getLocalProperties();
|
|
||||||
String address = p.get(PROP_ADDRESS);
|
|
||||||
String uuid = p.get(PROP_UUID);
|
|
||||||
Settings s = callback.getSettings();
|
|
||||||
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
|
|
||||||
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
|
|
||||||
boolean changed = false;
|
|
||||||
if (address == null || isReflected) {
|
|
||||||
address = getBluetoothAddress();
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Local address " + scrubMacAddress(address));
|
|
||||||
}
|
|
||||||
if (address == null) {
|
|
||||||
if (everConnected.get()) {
|
|
||||||
address = getReflectedAddress();
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Reflected address " +
|
|
||||||
scrubMacAddress(address));
|
|
||||||
}
|
|
||||||
if (address != null) {
|
|
||||||
changed = true;
|
|
||||||
isReflected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
changed = true;
|
|
||||||
isReflected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (uuid == null) {
|
|
||||||
byte[] random = new byte[UUID_BYTES];
|
|
||||||
secureRandom.nextBytes(random);
|
|
||||||
uuid = UUID.nameUUIDFromBytes(random).toString();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
contactConnectionsUuid = uuid;
|
|
||||||
if (changed) {
|
|
||||||
p = new TransportProperties();
|
|
||||||
// If we previously used a reflected address and there's no longer
|
|
||||||
// a reflected address with enough votes to be used, we'll continue
|
|
||||||
// to use the old reflected address until there's a new winner
|
|
||||||
if (address != null) p.put(PROP_ADDRESS, address);
|
|
||||||
p.put(PROP_UUID, uuid);
|
|
||||||
callback.mergeLocalProperties(p);
|
|
||||||
s = new Settings();
|
|
||||||
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
|
|
||||||
callback.mergeSettings(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getReflectedAddress() {
|
|
||||||
// Count the number of votes for each reflected address
|
|
||||||
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
|
|
||||||
Multiset<String> votes = new Multiset<>();
|
|
||||||
for (TransportProperties p : callback.getRemoteProperties()) {
|
|
||||||
String address = p.get(key);
|
|
||||||
if (address != null && isValidAddress(address)) votes.add(address);
|
|
||||||
}
|
|
||||||
// If an address gets more than half of the votes, accept it
|
|
||||||
int total = votes.getTotal();
|
|
||||||
for (String address : votes.keySet()) {
|
|
||||||
if (votes.getCount(address) * 2 > total) return address;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void acceptContactConnections(SS ss) {
|
|
||||||
while (true) {
|
|
||||||
DuplexTransportConnection conn;
|
|
||||||
try {
|
|
||||||
conn = acceptConnection(ss);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// This is expected when the server socket is closed
|
|
||||||
LOG.info("Server socket closed");
|
|
||||||
state.clearServerSocket();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LOG.info("Connection received");
|
|
||||||
connectionLimiter.connectionOpened(conn);
|
|
||||||
backoff.reset();
|
|
||||||
setEverConnected();
|
|
||||||
callback.handleConnection(conn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setEverConnected() {
|
|
||||||
if (!everConnected.getAndSet(true)) {
|
|
||||||
ioExecutor.execute(() -> {
|
|
||||||
Settings s = new Settings();
|
|
||||||
s.putBoolean(PREF_EVER_CONNECTED, true);
|
|
||||||
callback.mergeSettings(s);
|
|
||||||
// Contacts may already have sent a reflected address
|
|
||||||
updateProperties();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
SS ss = state.setStopped();
|
|
||||||
tryToClose(ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public State getState() {
|
|
||||||
return state.getState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getReasonsDisabled() {
|
|
||||||
return state.getReasonsDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean shouldPoll() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPollingInterval() {
|
|
||||||
return backoff.getPollingInterval();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
|
|
||||||
properties) {
|
|
||||||
if (getState() != ACTIVE) return;
|
|
||||||
backoff.increment();
|
|
||||||
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
|
|
||||||
connect(p.getFirst(), p.getSecond());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connect(TransportProperties p, ConnectionHandler h) {
|
|
||||||
String address = p.get(PROP_ADDRESS);
|
|
||||||
if (isNullOrEmpty(address)) return;
|
|
||||||
String uuid = p.get(PROP_UUID);
|
|
||||||
if (isNullOrEmpty(uuid)) return;
|
|
||||||
wakefulIoExecutor.execute(() -> {
|
|
||||||
DuplexTransportConnection d = createConnection(p);
|
|
||||||
if (d != null) {
|
|
||||||
backoff.reset();
|
|
||||||
setEverConnected();
|
|
||||||
h.handleConnection(d);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private DuplexTransportConnection connect(String address, String uuid) {
|
|
||||||
// Validate the address
|
|
||||||
if (!isValidAddress(address)) {
|
|
||||||
if (LOG.isLoggable(WARNING))
|
|
||||||
// Not scrubbing here to be able to figure out the problem
|
|
||||||
LOG.warning("Invalid address " + address);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Validate the UUID
|
|
||||||
try {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
UUID.fromString(uuid);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Connecting to " + scrubMacAddress(address));
|
|
||||||
try {
|
|
||||||
DuplexTransportConnection conn = connectTo(address, uuid);
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Connected to " + scrubMacAddress(address));
|
|
||||||
return conn;
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Could not connect to " + scrubMacAddress(address));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DuplexTransportConnection createConnection(TransportProperties p) {
|
|
||||||
if (getState() != ACTIVE) return null;
|
|
||||||
if (!connectionLimiter.canOpenContactConnection()) return null;
|
|
||||||
String address = p.get(PROP_ADDRESS);
|
|
||||||
if (isNullOrEmpty(address)) return null;
|
|
||||||
String uuid = p.get(PROP_UUID);
|
|
||||||
if (isNullOrEmpty(uuid)) return null;
|
|
||||||
DuplexTransportConnection conn = connect(address, uuid);
|
|
||||||
if (conn != null) connectionLimiter.connectionOpened(conn);
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsKeyAgreement() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
|
|
||||||
if (getState() != ACTIVE) return null;
|
|
||||||
// No truncation necessary because COMMIT_LENGTH = 16
|
|
||||||
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
|
||||||
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
|
||||||
// Bind a server socket for receiving key agreement connections
|
|
||||||
SS ss;
|
|
||||||
try {
|
|
||||||
ss = openServerSocket(uuid);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (getState() != ACTIVE) {
|
|
||||||
tryToClose(ss);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
BdfList descriptor = new BdfList();
|
|
||||||
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
|
||||||
String address = getBluetoothAddress();
|
|
||||||
if (address != null) descriptor.add(macToBytes(address));
|
|
||||||
return new BluetoothKeyAgreementListener(descriptor, ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DuplexTransportConnection createKeyAgreementConnection(
|
|
||||||
byte[] commitment, BdfList descriptor) {
|
|
||||||
if (getState() != ACTIVE) return null;
|
|
||||||
// No truncation necessary because COMMIT_LENGTH = 16
|
|
||||||
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
|
||||||
DuplexTransportConnection conn;
|
|
||||||
if (descriptor.size() == 1) {
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Discovering address for key agreement UUID " +
|
|
||||||
uuid);
|
|
||||||
}
|
|
||||||
conn = discoverAndConnect(uuid);
|
|
||||||
} else {
|
|
||||||
String address;
|
|
||||||
try {
|
|
||||||
address = parseAddress(descriptor);
|
|
||||||
} catch (FormatException e) {
|
|
||||||
LOG.info("Invalid address in key agreement descriptor");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Connecting to key agreement UUID " + uuid);
|
|
||||||
conn = connect(address, uuid);
|
|
||||||
}
|
|
||||||
if (conn != null) {
|
|
||||||
connectionLimiter.connectionOpened(conn);
|
|
||||||
setEverConnected();
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseAddress(BdfList descriptor) throws FormatException {
|
|
||||||
byte[] mac = descriptor.getRaw(1);
|
|
||||||
if (mac.length != 6) throw new FormatException();
|
|
||||||
return macToString(mac);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsRendezvous() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
|
|
||||||
boolean alice, ConnectionHandler incoming) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void eventOccurred(Event e) {
|
|
||||||
if (e instanceof SettingsUpdatedEvent) {
|
|
||||||
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
|
|
||||||
if (s.getNamespace().equals(ID.getString()))
|
|
||||||
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
|
|
||||||
} else if (e instanceof KeyAgreementListeningEvent) {
|
|
||||||
ioExecutor.execute(connectionLimiter::keyAgreementStarted);
|
|
||||||
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
|
|
||||||
ioExecutor.execute(connectionLimiter::keyAgreementEnded);
|
|
||||||
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
|
|
||||||
RemoteTransportPropertiesUpdatedEvent r =
|
|
||||||
(RemoteTransportPropertiesUpdatedEvent) e;
|
|
||||||
if (r.getTransportId().equals(ID)) {
|
|
||||||
ioExecutor.execute(this::updateProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IoExecutor
|
|
||||||
private void onSettingsUpdated(Settings settings) {
|
|
||||||
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
|
||||||
DEFAULT_PREF_PLUGIN_ENABLE);
|
|
||||||
SS ss = state.setEnabledByUser(enabledByUser);
|
|
||||||
State s = getState();
|
|
||||||
if (ss != null) {
|
|
||||||
LOG.info("Disabled by user, closing server socket");
|
|
||||||
tryToClose(ss);
|
|
||||||
} else if (s == INACTIVE) {
|
|
||||||
if (isAdapterEnabled()) {
|
|
||||||
LOG.info("Enabled by user, opening server socket");
|
|
||||||
bind();
|
|
||||||
} else {
|
|
||||||
LOG.info("Enabled by user but adapter is disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
|
||||||
|
|
||||||
private final SS ss;
|
|
||||||
|
|
||||||
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
|
|
||||||
super(descriptor);
|
|
||||||
this.ss = ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyAgreementConnection accept() throws IOException {
|
|
||||||
DuplexTransportConnection conn = acceptConnection(ss);
|
|
||||||
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
|
|
||||||
connectionLimiter.connectionOpened(conn);
|
|
||||||
return new KeyAgreementConnection(conn, ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
tryToClose(ss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThreadSafe
|
|
||||||
@NotNullByDefault
|
|
||||||
protected class PluginState {
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
private boolean started = false,
|
|
||||||
stopped = false,
|
|
||||||
enabledByUser = false;
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
@Nullable
|
|
||||||
private SS serverSocket = null;
|
|
||||||
|
|
||||||
synchronized void setStarted(boolean enabledByUser) {
|
|
||||||
started = true;
|
|
||||||
this.enabledByUser = enabledByUser;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
synchronized SS setStopped() {
|
|
||||||
stopped = true;
|
|
||||||
SS ss = serverSocket;
|
|
||||||
serverSocket = null;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
synchronized SS setEnabledByUser(boolean enabledByUser) {
|
|
||||||
this.enabledByUser = enabledByUser;
|
|
||||||
SS ss = null;
|
|
||||||
if (!enabledByUser) {
|
|
||||||
ss = serverSocket;
|
|
||||||
serverSocket = null;
|
|
||||||
}
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean setServerSocket(SS ss) {
|
|
||||||
if (stopped || serverSocket != null) return false;
|
|
||||||
serverSocket = ss;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
synchronized SS clearServerSocket() {
|
|
||||||
SS ss = serverSocket;
|
|
||||||
serverSocket = null;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized State getState() {
|
|
||||||
if (!started || stopped) return STARTING_STOPPING;
|
|
||||||
if (!enabledByUser) return DISABLED;
|
|
||||||
return serverSocket == null ? INACTIVE : ACTIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized int getReasonsDisabled() {
|
|
||||||
return getState() == DISABLED ? REASON_USER : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
|
|||||||
// Metadata keys
|
// Metadata keys
|
||||||
String MSG_KEY_UPDATE_VERSION = "version";
|
String MSG_KEY_UPDATE_VERSION = "version";
|
||||||
String MSG_KEY_LOCAL = "local";
|
String MSG_KEY_LOCAL = "local";
|
||||||
String GROUP_KEY_CONTACT_ID = "contactId";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ import static java.util.Collections.emptyList;
|
|||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||||
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_LOCAL;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||||
|
|
||||||
@@ -161,13 +160,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
db.addGroup(txn, g);
|
db.addGroup(txn, g);
|
||||||
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
||||||
// Attach the contact ID to the group
|
// Attach the contact ID to the group
|
||||||
BdfDictionary meta = new BdfDictionary();
|
clientHelper.setContactId(txn, g.getId(), c.getId());
|
||||||
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
|
||||||
try {
|
|
||||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
|
||||||
} catch (FormatException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
// Create and store the first local update
|
// Create and store the first local update
|
||||||
List<ClientVersion> versions = new ArrayList<>(clients);
|
List<ClientVersion> versions = new ArrayList<>(clients);
|
||||||
Collections.sort(versions);
|
Collections.sort(versions);
|
||||||
@@ -229,7 +222,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
Map<ClientMajorVersion, Visibility> after =
|
Map<ClientMajorVersion, Visibility> after =
|
||||||
getVisibilities(newLocalStates, newRemoteStates);
|
getVisibilities(newLocalStates, newRemoteStates);
|
||||||
// Call hooks for any visibilities that have changed
|
// Call hooks for any visibilities that have changed
|
||||||
ContactId c = getContactId(txn, m.getGroupId());
|
ContactId c = clientHelper.getContactId(txn, m.getGroupId());
|
||||||
if (!before.equals(after)) {
|
if (!before.equals(after)) {
|
||||||
Contact contact = db.getContact(txn, c);
|
Contact contact = db.getContact(txn, c);
|
||||||
callVisibilityHooks(txn, contact, before, after);
|
callVisibilityHooks(txn, contact, before, after);
|
||||||
@@ -521,17 +514,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
storeUpdate(txn, g, states, 1);
|
storeUpdate(txn, g, states, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContactId getContactId(Transaction txn, GroupId g)
|
|
||||||
throws DbException {
|
|
||||||
try {
|
|
||||||
BdfDictionary meta =
|
|
||||||
clientHelper.getGroupMetadataAsDictionary(txn, g);
|
|
||||||
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
|
||||||
} catch (FormatException e) {
|
|
||||||
throw new DbException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ClientState> updateStatesFromRemoteStates(
|
private List<ClientState> updateStatesFromRemoteStates(
|
||||||
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
||||||
Set<ClientMajorVersion> remoteSet = new HashSet<>();
|
Set<ClientMajorVersion> remoteSet = new HashSet<>();
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
|
|
||||||
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
|
|
||||||
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
|
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
|
||||||
Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0
|
|
||||||
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
|
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
|
||||||
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
|
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
|
||||||
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
|
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
|
||||||
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
|
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
|
||||||
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
|
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
|
||||||
Bridge obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0
|
|
||||||
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
|
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
|
||||||
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
|
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
|
||||||
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
|
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package org.briarproject.bramble;
|
package org.briarproject.bramble;
|
||||||
|
|
||||||
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
|
import org.briarproject.bramble.system.TimeTravelModule;
|
||||||
|
|
||||||
public interface BrambleCoreIntegrationTestEagerSingletons
|
public interface BrambleCoreIntegrationTestEagerSingletons
|
||||||
extends BrambleCoreEagerSingletons {
|
extends BrambleCoreEagerSingletons {
|
||||||
|
|
||||||
void inject(DefaultTaskSchedulerModule.EagerSingletons init);
|
void inject(TimeTravelModule.EagerSingletons init);
|
||||||
|
|
||||||
class Helper {
|
class Helper {
|
||||||
|
|
||||||
public static void injectEagerSingletons(
|
public static void injectEagerSingletons(
|
||||||
BrambleCoreIntegrationTestEagerSingletons c) {
|
BrambleCoreIntegrationTestEagerSingletons c) {
|
||||||
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
|
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
|
||||||
c.inject(new DefaultTaskSchedulerModule.EagerSingletons());
|
c.inject(new TimeTravelModule.EagerSingletons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble.db;
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.PendingContactId;
|
import org.briarproject.bramble.api.contact.PendingContactId;
|
||||||
@@ -69,6 +70,8 @@ import static java.util.Arrays.asList;
|
|||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.emptyMap;
|
import static java.util.Collections.emptyMap;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
|
import static java.util.concurrent.TimeUnit.HOURS;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||||
@@ -510,11 +513,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
// Check whether the group is in the DB (which it's not)
|
// Check whether the group is in the DB (which it's not)
|
||||||
exactly(8).of(database).startTransaction();
|
exactly(10).of(database).startTransaction();
|
||||||
will(returnValue(txn));
|
will(returnValue(txn));
|
||||||
exactly(8).of(database).containsGroup(txn, groupId);
|
exactly(10).of(database).containsGroup(txn, groupId);
|
||||||
will(returnValue(false));
|
will(returnValue(false));
|
||||||
exactly(8).of(database).abortTransaction(txn);
|
exactly(10).of(database).abortTransaction(txn);
|
||||||
// Allow other checks to pass
|
// Allow other checks to pass
|
||||||
allowing(database).containsContact(txn, contactId);
|
allowing(database).containsContact(txn, contactId);
|
||||||
will(returnValue(true));
|
will(returnValue(true));
|
||||||
@@ -523,7 +526,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
eventExecutor, shutdownManager);
|
eventExecutor, shutdownManager);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getGroup(transaction, groupId));
|
db.getGroup(transaction, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -531,7 +534,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getGroupMetadata(transaction, groupId));
|
db.getGroupMetadata(transaction, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -539,7 +542,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
|
db.getMessageIds(transaction, groupId));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchGroupException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(true, transaction ->
|
||||||
|
db.getMessageIds(transaction, groupId, new Metadata()));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchGroupException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(true, transaction ->
|
||||||
db.getMessageMetadata(transaction, groupId));
|
db.getMessageMetadata(transaction, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -547,7 +566,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageMetadata(transaction, groupId,
|
db.getMessageMetadata(transaction, groupId,
|
||||||
new Metadata()));
|
new Metadata()));
|
||||||
fail();
|
fail();
|
||||||
@@ -556,7 +575,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageStatus(transaction, contactId, groupId));
|
db.getMessageStatus(transaction, contactId, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -594,11 +613,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
// Check whether the message is in the DB (which it's not)
|
// Check whether the message is in the DB (which it's not)
|
||||||
exactly(12).of(database).startTransaction();
|
exactly(15).of(database).startTransaction();
|
||||||
will(returnValue(txn));
|
will(returnValue(txn));
|
||||||
exactly(12).of(database).containsMessage(txn, messageId);
|
exactly(15).of(database).containsMessage(txn, messageId);
|
||||||
will(returnValue(false));
|
will(returnValue(false));
|
||||||
exactly(12).of(database).abortTransaction(txn);
|
exactly(15).of(database).abortTransaction(txn);
|
||||||
// Allow other checks to pass
|
// Allow other checks to pass
|
||||||
allowing(database).containsContact(txn, contactId);
|
allowing(database).containsContact(txn, contactId);
|
||||||
will(returnValue(true));
|
will(returnValue(true));
|
||||||
@@ -623,7 +642,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessage(transaction, messageId));
|
db.getMessage(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -631,7 +650,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageMetadata(transaction, messageId));
|
db.getMessageMetadata(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -639,7 +658,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageState(transaction, messageId));
|
db.getMessageState(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -647,7 +666,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageStatus(transaction, contactId, messageId));
|
db.getMessageStatus(transaction, contactId, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -662,6 +681,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
// Expected
|
// Expected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(false, transaction ->
|
||||||
|
db.setCleanupTimerDuration(transaction, message.getId(),
|
||||||
|
HOURS.toMillis(1)));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchMessageException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(false, transaction ->
|
||||||
db.setMessagePermanent(transaction, message.getId()));
|
db.setMessagePermanent(transaction, message.getId()));
|
||||||
@@ -687,7 +715,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageDependencies(transaction, messageId));
|
db.getMessageDependencies(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -695,12 +723,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageDependents(transaction, messageId));
|
db.getMessageDependents(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
// Expected
|
// Expected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(false, transaction ->
|
||||||
|
db.startCleanupTimer(transaction, messageId));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchMessageException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(false, transaction ->
|
||||||
|
db.stopCleanupTimer(transaction, messageId));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchMessageException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -981,6 +1025,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
||||||
will(returnValue(true));
|
will(returnValue(true));
|
||||||
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).startCleanupTimer(txn, messageId);
|
||||||
|
will(returnValue(TIMER_NOT_STARTED)); // No cleanup duration was set
|
||||||
oneOf(database).commitTransaction(txn);
|
oneOf(database).commitTransaction(txn);
|
||||||
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
|
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
|
||||||
}});
|
}});
|
||||||
@@ -993,6 +1040,56 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReceiveDuplicateAck() throws Exception {
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(database).startTransaction();
|
||||||
|
will(returnValue(txn));
|
||||||
|
oneOf(database).containsContact(txn, contactId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
||||||
|
will(returnValue(false)); // Already acked
|
||||||
|
oneOf(database).commitTransaction(txn);
|
||||||
|
}});
|
||||||
|
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||||
|
eventExecutor, shutdownManager);
|
||||||
|
|
||||||
|
db.transaction(false, transaction -> {
|
||||||
|
Ack a = new Ack(singletonList(messageId));
|
||||||
|
db.receiveAck(transaction, contactId, a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReceiveAckWithCleanupTimer() throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis();
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(database).startTransaction();
|
||||||
|
will(returnValue(txn));
|
||||||
|
oneOf(database).containsContact(txn, contactId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).startCleanupTimer(txn, messageId);
|
||||||
|
will(returnValue(deadline));
|
||||||
|
oneOf(database).commitTransaction(txn);
|
||||||
|
oneOf(eventBus).broadcast(with(any(
|
||||||
|
CleanupTimerStartedEvent.class)));
|
||||||
|
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
|
||||||
|
}});
|
||||||
|
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||||
|
eventExecutor, shutdownManager);
|
||||||
|
|
||||||
|
db.transaction(false, transaction -> {
|
||||||
|
Ack a = new Ack(singletonList(messageId));
|
||||||
|
db.receiveAck(transaction, contactId, a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReceiveMessage() throws Exception {
|
public void testReceiveMessage() throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.sql.Driver;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
@@ -53,10 +57,11 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.emptyMap;
|
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static java.util.Collections.singletonMap;
|
import static java.util.Collections.singletonMap;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
@@ -351,7 +356,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
assertTrue(ids.isEmpty());
|
assertTrue(ids.isEmpty());
|
||||||
|
|
||||||
// Sharing the message should make it sendable
|
// Sharing the message should make it sendable
|
||||||
db.setMessageShared(txn, messageId);
|
db.setMessageShared(txn, messageId, true);
|
||||||
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
||||||
assertEquals(singletonList(messageId), ids);
|
assertEquals(singletonList(messageId), ids);
|
||||||
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
|
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
|
||||||
@@ -631,8 +636,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
|
|
||||||
// The group should not be visible to the contact
|
// The group should not be visible to the contact
|
||||||
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||||
assertEquals(emptyMap(),
|
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
|
||||||
db.getGroupVisibility(txn, groupId));
|
|
||||||
|
|
||||||
// Make the group visible to the contact
|
// Make the group visible to the contact
|
||||||
db.addGroupVisibility(txn, contactId, groupId, false);
|
db.addGroupVisibility(txn, contactId, groupId, false);
|
||||||
@@ -655,8 +659,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
// Make the group invisible again
|
// Make the group invisible again
|
||||||
db.removeGroupVisibility(txn, contactId, groupId);
|
db.removeGroupVisibility(txn, contactId, groupId);
|
||||||
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||||
assertEquals(emptyMap(),
|
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
|
||||||
db.getGroupVisibility(txn, groupId));
|
|
||||||
|
|
||||||
db.commitTransaction(txn);
|
db.commitTransaction(txn);
|
||||||
db.close();
|
db.close();
|
||||||
@@ -2040,7 +2043,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||||
|
|
||||||
// Share the message - now it should be sendable immediately
|
// Share the message - now it should be sendable immediately
|
||||||
db.setMessageShared(txn, messageId);
|
db.setMessageShared(txn, messageId, true);
|
||||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||||
|
|
||||||
// Mark the message as requested - it should still be sendable
|
// Mark the message as requested - it should still be sendable
|
||||||
@@ -2347,6 +2350,148 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShutdownGracefully() throws Exception {
|
||||||
|
Database<Connection> db = open(false);
|
||||||
|
db.close();
|
||||||
|
open(true);
|
||||||
|
assertFalse(db.wasDirtyOnInitialisation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShutdownDirty() throws Exception {
|
||||||
|
Database<Connection> db = open(false);
|
||||||
|
|
||||||
|
// We want to simulate a dirty shutdown here which would normally be
|
||||||
|
// caused by an empty battery or by force closing the Android app.
|
||||||
|
// As there is no obvious way to simulate this, we're artificially
|
||||||
|
// causing an SqlException during close() here by unloading the JDBC
|
||||||
|
// drivers.
|
||||||
|
List<String> unloadedDrivers = unloadDrivers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.close();
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// continue
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reloading drivers to continue so that we're able to work with the
|
||||||
|
// database again.
|
||||||
|
reloadDrivers(unloadedDrivers);
|
||||||
|
|
||||||
|
db = open(true);
|
||||||
|
assertTrue(db.wasDirtyOnInitialisation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShutdownDirtyThenGracefully() throws Exception {
|
||||||
|
Database<Connection> db = open(false);
|
||||||
|
|
||||||
|
// Simulating a dirty shutdown here, look at #testShutdownDirty for
|
||||||
|
// details.
|
||||||
|
List<String> unloadedDrivers = unloadDrivers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.close();
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadDrivers(unloadedDrivers);
|
||||||
|
|
||||||
|
db = open(true);
|
||||||
|
assertTrue(db.wasDirtyOnInitialisation());
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
db = open(true);
|
||||||
|
assertFalse(db.wasDirtyOnInitialisation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanupTimer() throws Exception {
|
||||||
|
long duration = 60_000;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
AtomicLong time = new AtomicLong(now);
|
||||||
|
Database<Connection> db =
|
||||||
|
open(false, new TestMessageFactory(), new SettableClock(time));
|
||||||
|
Connection txn = db.startTransaction();
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Add a group and a message
|
||||||
|
db.addGroup(txn, group);
|
||||||
|
db.addMessage(txn, message, DELIVERED, false, false, null);
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Set the message's cleanup timer duration
|
||||||
|
db.setCleanupTimerDuration(txn, messageId, duration);
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Start the message's cleanup timer
|
||||||
|
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
|
||||||
|
|
||||||
|
// The timer can't be started again
|
||||||
|
assertEquals(TIMER_NOT_STARTED, db.startCleanupTimer(txn, messageId));
|
||||||
|
|
||||||
|
// No messages should be due for deletion, but the message should be
|
||||||
|
// scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Stop the timer
|
||||||
|
db.stopCleanupTimer(txn, messageId);
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Start the timer again
|
||||||
|
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
|
||||||
|
|
||||||
|
// No messages should be due for deletion, but the message should be
|
||||||
|
// scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// 1 ms before the timer expires, no messages should be due for
|
||||||
|
// deletion but the message should be scheduled for deletion
|
||||||
|
time.set(now + duration - 1);
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// When the timer expires, the message should be due and scheduled for
|
||||||
|
// deletion
|
||||||
|
time.set(now + duration);
|
||||||
|
assertEquals(singletonMap(groupId, singletonList(messageId)),
|
||||||
|
db.getMessagesToDelete(txn));
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// 1 ms after the timer expires, the message should be due and
|
||||||
|
// scheduled for deletion
|
||||||
|
time.set(now + duration + 1);
|
||||||
|
assertEquals(singletonMap(groupId, singletonList(messageId)),
|
||||||
|
db.getMessagesToDelete(txn));
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Once the message has been deleted, it should no longer be due
|
||||||
|
// or scheduled for deletion
|
||||||
|
db.deleteMessage(txn, messageId);
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
}
|
||||||
|
|
||||||
private Database<Connection> open(boolean resume) throws Exception {
|
private Database<Connection> open(boolean resume) throws Exception {
|
||||||
return open(resume, new TestMessageFactory(), new SystemClock());
|
return open(resume, new TestMessageFactory(), new SystemClock());
|
||||||
}
|
}
|
||||||
@@ -2402,6 +2547,31 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
rootKey, alice);
|
rootKey, alice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> unloadDrivers() {
|
||||||
|
Enumeration<Driver> drivers = DriverManager.getDrivers();
|
||||||
|
List<String> unloaded = new ArrayList<>();
|
||||||
|
while (drivers.hasMoreElements()) {
|
||||||
|
Driver d = drivers.nextElement();
|
||||||
|
try {
|
||||||
|
DriverManager.deregisterDriver(d);
|
||||||
|
unloaded.add(d.getClass().getName());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadDrivers(List<String> unloadedDrivers)
|
||||||
|
throws ClassNotFoundException, IllegalAccessException,
|
||||||
|
InstantiationException, SQLException {
|
||||||
|
for (String driverName : unloadedDrivers) {
|
||||||
|
DriverManager.registerDriver(
|
||||||
|
(Driver) Class.forName(driverName).newInstance());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
deleteTestDirectory(testDir);
|
deleteTestDirectory(testDir);
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package org.briarproject.bramble.system;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.PriorityBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link TaskScheduler} for use in tests. The scheduler keeps all scheduled
|
||||||
|
* tasks in a queue until {@link #runTasks()} is called.
|
||||||
|
*/
|
||||||
|
@NotNullByDefault
|
||||||
|
class TestTaskScheduler implements TaskScheduler {
|
||||||
|
|
||||||
|
private final Queue<Task> queue = new PriorityBlockingQueue<>();
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
TestTaskScheduler(Clock clock) {
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cancellable schedule(Runnable task, Executor executor, long delay,
|
||||||
|
TimeUnit unit) {
|
||||||
|
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||||
|
return schedule(task, executor, delay, unit, cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
|
||||||
|
long delay, long interval, TimeUnit unit) {
|
||||||
|
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||||
|
return scheduleWithFixedDelay(task, executor, delay, interval, unit,
|
||||||
|
cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cancellable schedule(Runnable task, Executor executor, long delay,
|
||||||
|
TimeUnit unit, AtomicBoolean cancelled) {
|
||||||
|
long delayMillis = MILLISECONDS.convert(delay, unit);
|
||||||
|
long dueMillis = clock.currentTimeMillis() + delayMillis;
|
||||||
|
Task t = new Task(task, executor, dueMillis, cancelled);
|
||||||
|
queue.add(t);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
|
||||||
|
long delay, long interval, TimeUnit unit, AtomicBoolean cancelled) {
|
||||||
|
// All executions of this periodic task share a cancelled flag
|
||||||
|
Runnable wrapped = () -> {
|
||||||
|
task.run();
|
||||||
|
scheduleWithFixedDelay(task, executor, interval, interval, unit,
|
||||||
|
cancelled);
|
||||||
|
};
|
||||||
|
return schedule(wrapped, executor, delay, unit, cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs any scheduled tasks that are due.
|
||||||
|
*/
|
||||||
|
void runTasks() throws InterruptedException {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
while (true) {
|
||||||
|
Task t = queue.peek();
|
||||||
|
if (t == null || t.dueMillis > now) return;
|
||||||
|
t = queue.poll();
|
||||||
|
// Submit the task to its executor and wait for it to finish
|
||||||
|
if (!t.run().await(1, MINUTES)) fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Task
|
||||||
|
implements Cancellable, Comparable<Task> {
|
||||||
|
|
||||||
|
private final Runnable task;
|
||||||
|
private final Executor executor;
|
||||||
|
private final long dueMillis;
|
||||||
|
private final AtomicBoolean cancelled;
|
||||||
|
|
||||||
|
private Task(Runnable task, Executor executor, long dueMillis,
|
||||||
|
AtomicBoolean cancelled) {
|
||||||
|
this.task = task;
|
||||||
|
this.executor = executor;
|
||||||
|
this.dueMillis = dueMillis;
|
||||||
|
this.cancelled = cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UseCompareMethod") // Animal Sniffer
|
||||||
|
@Override
|
||||||
|
public int compareTo(Task task) {
|
||||||
|
return Long.valueOf(dueMillis).compareTo(task.dueMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the task to its executor and returns a latch that will be
|
||||||
|
* released when the task finishes.
|
||||||
|
*/
|
||||||
|
public CountDownLatch run() {
|
||||||
|
if (cancelled.get()) return new CountDownLatch(0);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
executor.execute(() -> {
|
||||||
|
task.run();
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
return latch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel() {
|
||||||
|
cancelled.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package org.briarproject.bramble.system;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||||
|
import org.briarproject.bramble.test.SettableClock;
|
||||||
|
import org.briarproject.bramble.test.TimeTravel;
|
||||||
|
|
||||||
|
import java.util.concurrent.RejectedExecutionHandler;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class TimeTravelModule {
|
||||||
|
|
||||||
|
public static class EagerSingletons {
|
||||||
|
@Inject
|
||||||
|
TaskScheduler scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduledExecutorService;
|
||||||
|
private final Clock clock;
|
||||||
|
private final TaskScheduler taskScheduler;
|
||||||
|
private final TimeTravel timeTravel;
|
||||||
|
|
||||||
|
public TimeTravelModule() {
|
||||||
|
this(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeTravelModule(boolean travel) {
|
||||||
|
// Discard tasks that are submitted during shutdown
|
||||||
|
RejectedExecutionHandler policy =
|
||||||
|
new ScheduledThreadPoolExecutor.DiscardPolicy();
|
||||||
|
scheduledExecutorService =
|
||||||
|
new ScheduledThreadPoolExecutor(1, policy);
|
||||||
|
if (travel) {
|
||||||
|
// Use a SettableClock and TestTaskScheduler to allow time travel
|
||||||
|
AtomicLong time = new AtomicLong(System.currentTimeMillis());
|
||||||
|
clock = new SettableClock(time);
|
||||||
|
TestTaskScheduler testTaskScheduler = new TestTaskScheduler(clock);
|
||||||
|
taskScheduler = testTaskScheduler;
|
||||||
|
timeTravel = new TimeTravel() {
|
||||||
|
@Override
|
||||||
|
public void setCurrentTimeMillis(long now)
|
||||||
|
throws InterruptedException {
|
||||||
|
time.set(now);
|
||||||
|
testTaskScheduler.runTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCurrentTimeMillis(long add)
|
||||||
|
throws InterruptedException {
|
||||||
|
time.addAndGet(add);
|
||||||
|
testTaskScheduler.runTasks();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Use the default clock and task scheduler
|
||||||
|
clock = new SystemClock();
|
||||||
|
taskScheduler = new TaskSchedulerImpl(scheduledExecutorService);
|
||||||
|
timeTravel = new TimeTravel() {
|
||||||
|
@Override
|
||||||
|
public void setCurrentTimeMillis(long now) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCurrentTimeMillis(long add) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
Clock provideClock() {
|
||||||
|
return clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager) {
|
||||||
|
lifecycleManager.registerForShutdown(scheduledExecutorService);
|
||||||
|
return taskScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
TimeTravel provideTimeTravel() {
|
||||||
|
return timeTravel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package org.briarproject.bramble.test;
|
|||||||
import org.briarproject.bramble.api.FeatureFlags;
|
import org.briarproject.bramble.api.FeatureFlags;
|
||||||
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
|
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
|
||||||
import org.briarproject.bramble.event.DefaultEventExecutorModule;
|
import org.briarproject.bramble.event.DefaultEventExecutorModule;
|
||||||
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
|
|
||||||
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
|
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
|
||||||
|
import org.briarproject.bramble.system.TimeTravelModule;
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
@@ -12,11 +12,11 @@ import dagger.Provides;
|
|||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
DefaultBatteryManagerModule.class,
|
DefaultBatteryManagerModule.class,
|
||||||
DefaultEventExecutorModule.class,
|
DefaultEventExecutorModule.class,
|
||||||
DefaultTaskSchedulerModule.class,
|
|
||||||
DefaultWakefulIoExecutorModule.class,
|
DefaultWakefulIoExecutorModule.class,
|
||||||
TestDatabaseConfigModule.class,
|
TestDatabaseConfigModule.class,
|
||||||
TestPluginConfigModule.class,
|
TestPluginConfigModule.class,
|
||||||
TestSecureRandomModule.class
|
TestSecureRandomModule.class,
|
||||||
|
TimeTravelModule.class
|
||||||
})
|
})
|
||||||
public class BrambleCoreIntegrationTestModule {
|
public class BrambleCoreIntegrationTestModule {
|
||||||
|
|
||||||
@@ -33,6 +33,16 @@ public class BrambleCoreIntegrationTestModule {
|
|||||||
public boolean shouldEnableProfilePictures() {
|
public boolean shouldEnableProfilePictures() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldEnableDisappearingMessages() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldEnableConnectViaBluetooth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import static org.briarproject.bramble.test.TestUtils.getContact;
|
|||||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||||
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_LOCAL;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -60,8 +59,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
private final ClientId clientId = getClientId();
|
private final ClientId clientId = getClientId();
|
||||||
private final long now = System.currentTimeMillis();
|
private final long now = System.currentTimeMillis();
|
||||||
private final Transaction txn = new Transaction(null, false);
|
private final Transaction txn = new Transaction(null, false);
|
||||||
private final BdfDictionary groupMeta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
|
||||||
|
|
||||||
private ClientVersioningManagerImpl createInstance() {
|
private ClientVersioningManagerImpl createInstance() {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
@@ -123,8 +120,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(db).addGroup(txn, contactGroup);
|
oneOf(db).addGroup(txn, contactGroup);
|
||||||
oneOf(db).setGroupVisibility(txn, contact.getId(),
|
oneOf(db).setGroupVisibility(txn, contact.getId(),
|
||||||
contactGroup.getId(), SHARED);
|
contactGroup.getId(), SHARED);
|
||||||
oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(),
|
oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
|
||||||
groupMeta);
|
contact.getId());
|
||||||
oneOf(clock).currentTimeMillis();
|
oneOf(clock).currentTimeMillis();
|
||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
|
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
|
||||||
@@ -460,9 +457,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
|
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
|
||||||
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
|
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
|
||||||
// Get contact ID
|
// Get contact ID
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
// No states or visibilities have changed
|
// No states or visibilities have changed
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -492,10 +488,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
// Load the latest local update
|
// Load the latest local update
|
||||||
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
|
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
|
||||||
will(returnValue(oldLocalUpdateBody));
|
will(returnValue(oldLocalUpdateBody));
|
||||||
// Get client ID
|
// Get contact ID
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
// No states or visibilities have changed
|
// No states or visibilities have changed
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -546,8 +541,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
||||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
||||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||||
BdfDictionary groupMeta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(clientHelper).toList(newRemoteUpdate);
|
oneOf(clientHelper).toList(newRemoteUpdate);
|
||||||
@@ -577,9 +570,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
||||||
newLocalUpdateMeta, true, false);
|
newLocalUpdateMeta, true, false);
|
||||||
// The client's visibility has changed
|
// The client's visibility has changed
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
oneOf(db).getContact(txn, contact.getId());
|
oneOf(db).getContact(txn, contact.getId());
|
||||||
will(returnValue(contact));
|
will(returnValue(contact));
|
||||||
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
|
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
|
||||||
@@ -619,8 +611,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
||||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
||||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||||
BdfDictionary groupMeta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(clientHelper).toList(newRemoteUpdate);
|
oneOf(clientHelper).toList(newRemoteUpdate);
|
||||||
@@ -650,9 +640,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
||||||
newLocalUpdateMeta, true, false);
|
newLocalUpdateMeta, true, false);
|
||||||
// The client's visibility has changed
|
// The client's visibility has changed
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
oneOf(db).getContact(txn, contact.getId());
|
oneOf(db).getContact(txn, contact.getId());
|
||||||
will(returnValue(contact));
|
will(returnValue(contact));
|
||||||
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
|
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import org.briarproject.bramble.system.JavaSystemModule;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
|
||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
|
CircumventionModule.class,
|
||||||
JavaNetworkModule.class,
|
JavaNetworkModule.class,
|
||||||
JavaSystemModule.class,
|
JavaSystemModule.class,
|
||||||
CircumventionModule.class,
|
|
||||||
SocksModule.class
|
SocksModule.class
|
||||||
})
|
})
|
||||||
public class BrambleJavaModule {
|
public class BrambleJavaModule {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import static org.briarproject.bramble.util.StringUtils.isValidMac;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class JavaBluetoothPlugin
|
class JavaBluetoothPlugin extends
|
||||||
extends BluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
|
AbstractBluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(JavaBluetoothPlugin.class.getName());
|
getLogger(JavaBluetoothPlugin.class.getName());
|
||||||
@@ -108,6 +108,11 @@ class JavaBluetoothPlugin
|
|||||||
return null; // TODO
|
return null; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopDiscoverAndConnect() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
private String makeUrl(String address, String uuid) {
|
private String makeUrl(String address, String uuid) {
|
||||||
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ import org.junit.runners.Parameterized;
|
|||||||
import org.junit.runners.Parameterized.Parameters;
|
import org.junit.runners.Parameterized.Parameters;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -45,15 +47,22 @@ import static org.junit.Assume.assumeTrue;
|
|||||||
public class BridgeTest extends BrambleTestCase {
|
public class BridgeTest extends BrambleTestCase {
|
||||||
|
|
||||||
@Parameters
|
@Parameters
|
||||||
public static Iterable<String> data() {
|
public static Iterable<Params> data() {
|
||||||
BrambleJavaIntegrationTestComponent component =
|
BrambleJavaIntegrationTestComponent component =
|
||||||
DaggerBrambleJavaIntegrationTestComponent.builder().build();
|
DaggerBrambleJavaIntegrationTestComponent.builder().build();
|
||||||
BrambleCoreIntegrationTestEagerSingletons.Helper
|
BrambleCoreIntegrationTestEagerSingletons.Helper
|
||||||
.injectEagerSingletons(component);
|
.injectEagerSingletons(component);
|
||||||
return component.getCircumventionProvider().getBridges(false);
|
// Share a failure counter among all the test instances
|
||||||
|
AtomicInteger failures = new AtomicInteger(0);
|
||||||
|
List<String> bridges =
|
||||||
|
component.getCircumventionProvider().getBridges(false);
|
||||||
|
List<Params> states = new ArrayList<>(bridges.size());
|
||||||
|
for (String bridge : bridges) states.add(new Params(bridge, failures));
|
||||||
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static long TIMEOUT = SECONDS.toMillis(60);
|
private final static long TIMEOUT = SECONDS.toMillis(60);
|
||||||
|
private final static int NUM_FAILURES_ALLOWED = 1;
|
||||||
|
|
||||||
private final static Logger LOG = getLogger(BridgeTest.class.getName());
|
private final static Logger LOG = getLogger(BridgeTest.class.getName());
|
||||||
|
|
||||||
@@ -80,11 +89,13 @@ public class BridgeTest extends BrambleTestCase {
|
|||||||
|
|
||||||
private final File torDir = getTestDirectory();
|
private final File torDir = getTestDirectory();
|
||||||
private final String bridge;
|
private final String bridge;
|
||||||
|
private final AtomicInteger failures;
|
||||||
|
|
||||||
private UnixTorPluginFactory factory;
|
private UnixTorPluginFactory factory;
|
||||||
|
|
||||||
public BridgeTest(String bridge) {
|
public BridgeTest(Params params) {
|
||||||
this.bridge = bridge;
|
bridge = params.bridge;
|
||||||
|
failures = params.failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -152,10 +163,24 @@ public class BridgeTest extends BrambleTestCase {
|
|||||||
clock.sleep(500);
|
clock.sleep(500);
|
||||||
}
|
}
|
||||||
if (plugin.getState() != ACTIVE) {
|
if (plugin.getState() != ACTIVE) {
|
||||||
fail("Could not connect to Tor within timeout.");
|
LOG.warning("Could not connect to Tor within timeout");
|
||||||
|
if (failures.incrementAndGet() > NUM_FAILURES_ALLOWED) {
|
||||||
|
fail(failures.get() + " bridges are unreachable");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
plugin.stop();
|
plugin.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class Params {
|
||||||
|
|
||||||
|
private final String bridge;
|
||||||
|
private final AtomicInteger failures;
|
||||||
|
|
||||||
|
private Params(String bridge, AtomicInteger failures) {
|
||||||
|
this.bridge = bridge;
|
||||||
|
this.failures = failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
briar-android/.gitignore
vendored
2
briar-android/.gitignore
vendored
@@ -11,3 +11,5 @@ src/main/res/values-iw
|
|||||||
/fastlane/metadata/android/*/images
|
/fastlane/metadata/android/*/images
|
||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
/fastlane/README.md
|
/fastlane/README.md
|
||||||
|
|
||||||
|
/fastlane/metadata/android/*/changelogs
|
||||||
@@ -9,3 +9,19 @@ source_lang = en
|
|||||||
type = ANDROID
|
type = ANDROID
|
||||||
minimum_perc = 80
|
minimum_perc = 80
|
||||||
|
|
||||||
|
[briar.google-play-short-description]
|
||||||
|
# https://support.google.com/googleplay/android-developer/answer/9844778?hl=en#zippy=%2Cview-list-of-available-languages
|
||||||
|
lang_map = en: en-US, de: de-DE, es: es-ES, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
|
||||||
|
file_filter = fastlane/metadata/android/<lang>/short_description.txt
|
||||||
|
source_file = fastlane/metadata/android/en-US/short_description.txt
|
||||||
|
source_lang = en
|
||||||
|
type = TXT
|
||||||
|
minimum_perc = 100
|
||||||
|
|
||||||
|
[briar.google-play-full-description]
|
||||||
|
lang_map = en: en-US, de: de-DE, es: es-ES, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
|
||||||
|
file_filter = fastlane/metadata/android/<lang>/full_description.txt
|
||||||
|
source_file = fastlane/metadata/android/en-US/full_description.txt
|
||||||
|
source_lang = en
|
||||||
|
type = TXT
|
||||||
|
minimum_perc = 100
|
||||||
|
|||||||
@@ -17,13 +17,17 @@ def getStdout = { command, defaultValue ->
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.2'
|
buildToolsVersion '30.0.3'
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
doNotStrip '**/*.so'
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode 10217
|
versionCode 10304
|
||||||
versionName "1.2.17"
|
versionName "1.3.4"
|
||||||
applicationId "org.briarproject.briar.android"
|
applicationId "org.briarproject.briar.android"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@@ -76,6 +80,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
unitTests {
|
unitTests {
|
||||||
includeAndroidResources = true
|
includeAndroidResources = true
|
||||||
}
|
}
|
||||||
@@ -144,6 +149,8 @@ dependencies {
|
|||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
|
androidTestUtil 'androidx.test:orchestrator:1.3.0'
|
||||||
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
|
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
|
||||||
androidTestCompileOnly 'javax.annotation:jsr250-api:1.0'
|
androidTestCompileOnly 'javax.annotation:jsr250-api:1.0'
|
||||||
androidTestImplementation 'junit:junit:4.13.1'
|
androidTestImplementation 'junit:junit:4.13.1'
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
json_key_file(ENV["BRIAR_GOOGLE_PLAY_JSON_KEY_FILE"])
|
||||||
package_name("org.briarproject.briar.android")
|
package_name("org.briarproject.briar.android")
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
# Uncomment the line if you want fastlane to automatically update itself
|
# Uncomment the line if you want fastlane to automatically update itself
|
||||||
# update_fastlane
|
# update_fastlane
|
||||||
|
|
||||||
|
opt_out_usage
|
||||||
|
|
||||||
default_platform(:android)
|
default_platform(:android)
|
||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
@@ -24,6 +26,21 @@ platform :android do
|
|||||||
system './demo-mode-deactivate.sh'
|
system './demo-mode-deactivate.sh'
|
||||||
system './rename_screenshots.py'
|
system './rename_screenshots.py'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Updates Google Play metadata (title, descriptions, video, etc.)"
|
||||||
|
lane :metadata do
|
||||||
|
system './update-metadata.sh'
|
||||||
|
upload_to_play_store(
|
||||||
|
skip_upload_apk: true,
|
||||||
|
skip_upload_aab: true,
|
||||||
|
skip_upload_metadata: false,
|
||||||
|
skip_upload_changelogs: true,
|
||||||
|
skip_upload_images: true,
|
||||||
|
skip_upload_screenshots: true,
|
||||||
|
validate_only: false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
El Briar és una aplicació de missatgeria dissenyada per a activistes, periodistes i qualsevol persona en general que necessiti una manera segura, fàcil i robsuta de comunicar-se. Al contrari que les aplicacions tradicionals, el Briar no depèn d'un servidor central: els missatges se sincronitzen directament entre els dispositius dels usuaris. Si Internet cau, el Briar pot continuar funcionant mitjançant Bluetooth o Wi-Fi, mantenint el flux fins i tot amb aquesta situació caòtica. Amb un funcionament normal d'Internet, el Briar es pot sincronitzar a través de la xarxa Tor, protegint els usuaris i les seves comunicacions d'ulls indiscrets.
|
||||||
|
|
||||||
|
L'aplicació té com a característica principal l'enviament de missatges privats en grups, fòrums o blocs, atès que es basa en la xarxa Tor. Qualsevol interacció al Briar s'emmagatzema només al vostre dispositiu fins que vulgueu compartir-ho amb altres usuaris.
|
||||||
|
|
||||||
|
No hi han anuncis ni seguiment El codi font d'aquesta aplicació està disponible per a tothom que vulgui inspeccionar-ho, com ja l'han auditat empreses professionals. Totes les versions del Briar són reproduïbles, la qual cosa permet verificar que el codi font publicat coincideix exactament amb l'aplicació publicada aquí. El desenvolupament és a càrrec d'un petit equip sense ànim lucratiu.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Missatgeria segura, arreu.
|
||||||
1
briar-android/fastlane/metadata/android/ca/title.txt
Normal file
1
briar-android/fastlane/metadata/android/ca/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar ist eine Messaging-App für Aktivisten, Journalisten und jeden, der eine sichere, einfache und robuste Art der Kommunikation benötigt. Im Gegensatz zu herkömmlichen Messaging-Apps benötigt Briar keinen zentralen Server. Nachrichten werden direkt zwischen den Endgeräten der Benutzer ausgetauscht. Wenn das Internet ausfällt, kann Briar diese auch über Bluetooth oder WLAN austauschen, um den Informationsaustausch in einer Krise aufrecht zu erhalten. Mit einer Internet Verbindung synchronisiert sich Briar über das Tor-Netzwerk und schützt so die Benutzer und ihre Kontakte vor Überwachung.
|
||||||
|
|
||||||
|
Die App bietet private Nachrichten, Gruppen und Foren sowie Blogs. Die Unterstützung für das Tor-Netzwerk ist in die App integriert. Alles, was du in Briar machst, wird nur auf deinem Gerät gespeichert, es sei denn, du entscheidest dich, es mit anderen Benutzern zu teilen.
|
||||||
|
|
||||||
|
Es gibt keine Werbung und kein Tracking. Der Quellcode der App ist komplett offen für jeden einsehbar und wurde bereits professionell auditiert. Alle Versionen von Briar sind reproduzierbar, so dass überprüft werden kann, ob der veröffentlichte Quellcode genau mit der hier veröffentlichten App übereinstimmt. Die Entwicklung wird von einem kleinen Non-Profit-Team durchgeführt.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Sicher kommunizieren, überall
|
||||||
1
briar-android/fastlane/metadata/android/de-DE/title.txt
Normal file
1
briar-android/fastlane/metadata/android/de-DE/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar is a messaging app designed for activists, journalists, and anyone else who needs a safe, easy and robust way to communicate. Unlike traditional messaging apps, Briar doesn't rely on a central server - messages are synchronized directly between the users' devices. If the internet's down, Briar can sync via Bluetooth or Wi-Fi, keeping the information flowing in a crisis. If the internet's up, Briar can sync via the Tor network, protecting users and their relationships from surveillance.
|
||||||
|
|
||||||
|
The app features private messages, groups and forums as well as blogs. Support for Tor network is built into the app. Everything you do in Briar is only stored on your device unless you decide to share it with other users.
|
||||||
|
|
||||||
|
There are no advertisements and no tracking. The source code of the app is completely open for anyone to inspect and has already been professionally audited. All releases of Briar are reproducible, making it possible to verify that the published source code matches exactly the app published here. Development is done by a small non-profit team.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Secure messaging, anywhere.
|
||||||
1
briar-android/fastlane/metadata/android/en-US/title.txt
Normal file
1
briar-android/fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
1
briar-android/fastlane/metadata/android/en-US/video.txt
Normal file
1
briar-android/fastlane/metadata/android/en-US/video.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://www.youtube.com/watch?v=ark7Y4u__SM
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar es una aplicación de mensajería diseñada para activistas, periodistas y cualquier otra persona que necesita una forma segura, fácil y robusta de comunicación. A diferencia de las aplicaciones de mensajería tradicionales, Briar no necesita un servidor central – los mensajes son sincronizados directamente entre los dispositivos de los usuarios. Si no hay conexión a internet disponible, Briar puede sincronizarse vía Bluetooth o WiFi, manteniendo la información fluyendo en crisis. Si Internet está disponible, Briar puede sincronizarse vía red Tor, protegiendo a los usuarios y sus relaciones de la vigilancia.
|
||||||
|
|
||||||
|
La aplicación tiene como características mensajes, grupos y foros privados, como así también blogs. El soporte para red Tor está incorporado en la aplicación. Todo lo que haces en Briar solamente es almacenado en tu dispositivo, a menos que decidas compartirlo con otros usuarios.
|
||||||
|
|
||||||
|
No hay publicidades ni rastreo. El código fuente de la aplicación está completamente abierto para que cualquiera lo inspeccione, y ya ha sido auditado profesionalmente. Todas las versiones de Briar son reproducibles, haciendo posible verificar que el código fuente publicado se corresponda exactamente con la aplicación publicada aquí. El desarrollo es realizado por un reducido equipo sin fines de lucro.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Mensajería segura, en cualquier lado.
|
||||||
1
briar-android/fastlane/metadata/android/es-ES/title.txt
Normal file
1
briar-android/fastlane/metadata/android/es-ES/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
1
briar-android/fastlane/metadata/android/es-ES/video.txt
Normal file
1
briar-android/fastlane/metadata/android/es-ES/video.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://www.youtube.com/watch?v=OuJjWdDfKuY
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar یک برنامه پیام رسان می باشد که برای فعالان، روزنامه نگاران و هر کسی که نیازمند یک راه امن، راحت و پیشرفته برای ارتباط با دیگران است می باشد. برخلاف برنامه های پیامرسان مرسوم، Briar به سرور متمرکز اتکا ندارد - پیام ها به صورت مستقیم بین دستگاه کاربران همگام می شود. اگر اینترنت کار نکند، Briar میتواند از طریق بلوتوث یا وای-فای همگام سازی کرده، جریان اطلاعات را در زمان بحران نگه دارد. اگر اینترنت کار کند، Briar میتواند برای محافظت کاربران و وابط آن ها از از شنود، از طریق شبکه تور همگام سازی کند.
|
||||||
|
|
||||||
|
این برنامه امکان ارسال پیام خصوصی، گروهی، تالارهای گفتمان و وبلاگ را در اختیار شما میگذارد. در این برنامه از شبکهی تور پشتیبانی میشود. هر آنچه در برایر (Briar) انجام میدهید، تنها در دستگاه شما ذخیره میشود مگر اینکه خودتان آن را با دیگران به اشتراک بگذارید.
|
||||||
|
|
||||||
|
این برنامه هیچ تبلیغ و ردیابی از کاربران خود انجام نمیدهد. کد منبع برنامه به صورت کاملا باز در اختیار همه قرار دارد و کاملا حرفهای بررسی شده است. تمامی نسخههای جدید برایر (Briar) قابل تولید مجدد هستند که امکان تائید یکسان بودن برنامه منتشر شده با کد منبع را میدهد. توسعه این برنامه توسط یک تیم غیرانتفاعی کوچک صورت میپذیرد.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
پیامرسان امن، در هر کجا
|
||||||
1
briar-android/fastlane/metadata/android/fa/title.txt
Normal file
1
briar-android/fastlane/metadata/android/fa/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
1
briar-android/fastlane/metadata/android/fa/video.txt
Normal file
1
briar-android/fastlane/metadata/android/fa/video.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://www.youtube.com/watch?v=ogDywPyPrB4
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar é unha app de mensaxería deseñada para activistas, xornalistas e calquera que precise comunicarse de xeito robusto, simple e seguro. Ao contrario que as apps de mensaxería tradicionais, Briar non depende dun servidor central - as mensaxes sincronízanse directamente entre os dispositivos das persoas usuarias. Se caese internet, Briar pode sincronizar a través de Bluetooth ou Wi-Fi, mantendo o fluxo de información en períodos de crise. Se hai internet, Briar pode sincronizar a través da rede Tor, protexendo ás usuarias e ás súas relacións da vixilancia.
|
||||||
|
|
||||||
|
A app proporciona mensaxes privadas, grupos e foros así como blogs. O soporte para a rede Tor está incluído na propia app. Todo o que fas en Briar só se garda no teu dispositivo a non ser que decidas compartilo con outras usuarias.
|
||||||
|
|
||||||
|
Non hai publicidade nen rastrexo da túa actividade. O código fonte da app está completamente aberto a calquera que queira revisalo e xa foi auditado profesionalmente. Todas as versións de Briar son reproducibles, facendo posible verificar que o código fonte publicado coincide exactamente coa app publicada aquí. O desenvolvemento realízase por un pequeno equipo de voluntarias.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Mensaxería segura, en todo lugar.
|
||||||
1
briar-android/fastlane/metadata/android/gl-ES/title.txt
Normal file
1
briar-android/fastlane/metadata/android/gl-ES/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar yra žinučių perdavimo programėlė, skirta aktyvistams, žurnalistams ir visiems kitiems, kam reikia saugaus, lengvo ir veiksmingo bendravimo būdo. Kitaip nei kitos tradicinės žinučių perdavimo programėlės, Briar nepasikliauja centriniu serveriu - žinutės yra sinchronizuojamos tiesiogiai tarp naudotojų įrenginių. Jei nėra interneto, Briar gali sinchronizuotis per Bluetooth ar belaidį ryšį (Wi-Fi), kad palaikytų informacijos perdavimą kriziniais atvejais. Jei internetas veikia, Briar gali sinchronizuotis per Tor tinklą, apsaugodama naudotojus ir jų tarpusavio ryšius nuo stebėjimo.
|
||||||
|
|
||||||
|
Programėlėje yra galimybė naudotis privačiomis žinutėmis, grupėmis, forumais, o taip pat tinklaraščiais. Tor tinklo palaikymas yra įtaisytas į programėlę. Viskas, ką atliekate Briar programėlėje, yra laikoma jūsų įrenginyje, nebent nuspręsite pasidalinti tuo su kitais naudotojais.
|
||||||
|
|
||||||
|
Nėra jokių reklamų ir jokio sekimo. Pradinis programėlės kodas yra visiškai atviras kiekvienam, norinčiam jį ištirti, ir jau buvo profesionaliai patikrintas. Visos Briar laidos yra atgaminamos, todėl galima patikrinti ir įsitikinti, kad paskelbtas pradinis kodas tiksliai atitinka čia paskelbtą programėlę. Programėlės plėtojimą vykdo maža nepelno siekianti komanda.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Saugus susirašinėjimas, bet kur.
|
||||||
1
briar-android/fastlane/metadata/android/lt/title.txt
Normal file
1
briar-android/fastlane/metadata/android/lt/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar aktivistler ve gazeteciler başta olmak üzere güvenli, kolay ve sağlam bir iletişim isteyen herkes için tasarlanmış bir ileti sistemidir. Geleneksel ileti sistemlerinin aksine Briar merkezi bir sunucu kullanmaz, iletiler doğrudan kullanıcıların aygıtları arasında eşleştirilir. Briar, eğer İnternet yoksa Bluetooth veya Wi-Fi aracılığıyla da iletileri iletebilir, böylece kriz durumlarında bilgi akışını sürdürür. İnternet varken Briar Tor ağı aracılığıyla iletim sağlar, böylece kullanıcıları ve ilişkilerini gözetimden korumuş olur.
|
||||||
|
|
||||||
|
Bu uygulama özel iletiler, gruplar ve forumlarla birlikte blog özelliklerine sahiptir. Tor ağı desteği uygulamada gömülüdür. Briar üzerinde yaptığınız her şey, siz başka kullanıcılarla paylaşmayı seçmediğiniz sürece, sadece kendi aygıtınızda saklanır.
|
||||||
|
|
||||||
|
Ne reklam var, ne de sizi izleme. Uygulamanın kaynak kodu, incelemek isteyen herkese tamamen açıktır ve zaten profesyonel olarak da denetlenmiştir. Briar'ın tüm sürümleri yeniden üretilebilir, böylece yayınlanan kaynak kodun, burada yayınlanan uygulamayla tam olarak eşlendiğini doğrulamak mümkündür. Uygulama kâr amacı gütmeyen küçük bir ekip tarafından geliştirilmektedir.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Güvenli mesajlaşma, nerede olursa olsun.
|
||||||
1
briar-android/fastlane/metadata/android/tr-TR/title.txt
Normal file
1
briar-android/fastlane/metadata/android/tr-TR/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Briar 是一款为活动人士、记者和其他需要安全、轻松和强大的通信方式的人设计的消息应用。与传统的消息应用不同,Briar 不依赖中央服务器——消息在用户设备之间直接同步。即便互联网中断,Briar 也可以通过蓝牙或 Wi-Fi 进行同步,在危机中保持信息流通。如果互联网连接正常,Briar 可以通过 Tor 网络进行同步,从而保护用户及其关系不受监视。
|
||||||
|
|
||||||
|
这款应用的功能包括私人信息、群组、论坛和博客。应用程序内置了对Tor网络的支持。你在Briar中所做的一切都只存储在你的设备上,除非你决定与其他用户共享它。
|
||||||
|
|
||||||
|
没有广告,也没有跟踪。该应用程序的源代码是完全开放的,任何人都可以检查,并且已经经过专业审计。Briar的所有版本都是可复制的,这使得验证发布的源代码与这里发布的应用程序完全匹配成为可能。开发是由一个小型非营利团队完成的。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
安全收发信息,不论何处
|
||||||
1
briar-android/fastlane/metadata/android/zh-CN/title.txt
Normal file
1
briar-android/fastlane/metadata/android/zh-CN/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Briar
|
||||||
18
briar-android/fastlane/update-metadata.sh
Executable file
18
briar-android/fastlane/update-metadata.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
tx pull -a -r briar.google-play-full-description,briar.google-play-short-description
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
||||||
|
|
||||||
|
for LANG_DIR in "$DIR"/metadata/android/*; do
|
||||||
|
if [[ "$LANG_DIR" == *en-US ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -f "$LANG_DIR/full_description.txt" ]] && [[ -f "$LANG_DIR/short_description.txt" ]]; then
|
||||||
|
# every language uses the same app title
|
||||||
|
cp "$DIR/metadata/android/en-US/title.txt" "$LANG_DIR/title.txt"
|
||||||
|
echo "$LANG_DIR"
|
||||||
|
else
|
||||||
|
# not complete, remove
|
||||||
|
rm -r "$LANG_DIR"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -19,4 +19,9 @@ public class BriarTestComponentApplication extends BriarApplicationImpl {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInstrumentationTest() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.junit.rules.TestRule;
|
||||||
|
import org.junit.runner.Description;
|
||||||
|
import org.junit.runners.model.Statement;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import androidx.test.espresso.Espresso;
|
||||||
|
import androidx.test.espresso.FailureHandler;
|
||||||
|
import androidx.test.espresso.base.DefaultFailureHandler;
|
||||||
|
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor;
|
||||||
|
import androidx.test.runner.screenshot.ScreenCapture;
|
||||||
|
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
|
||||||
|
import androidx.test.runner.screenshot.Screenshot;
|
||||||
|
|
||||||
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public class ScreenshotOnFailureRule implements TestRule {
|
||||||
|
|
||||||
|
FailureHandler defaultFailureHandler =
|
||||||
|
new DefaultFailureHandler(getApplicationContext());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Statement apply(Statement base, Description description) {
|
||||||
|
HashSet<ScreenCaptureProcessor> processors = new HashSet<>(1);
|
||||||
|
processors.add(new BasicScreenCaptureProcessor());
|
||||||
|
Screenshot.addScreenCaptureProcessors(processors);
|
||||||
|
return new Statement() {
|
||||||
|
@Override
|
||||||
|
public void evaluate() throws Throwable {
|
||||||
|
AtomicBoolean errorHandled = new AtomicBoolean(false);
|
||||||
|
Espresso.setFailureHandler((throwable, matcher) -> {
|
||||||
|
takeScreenshot(description);
|
||||||
|
errorHandled.set(true);
|
||||||
|
defaultFailureHandler.handle(throwable, matcher);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
base.evaluate();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (!errorHandled.get()) {
|
||||||
|
takeScreenshot(description);
|
||||||
|
}
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void takeScreenshot(Description description) {
|
||||||
|
String name = description.getTestClass().getSimpleName();
|
||||||
|
ScreenCapture capture = Screenshot.capture();
|
||||||
|
capture.setName(name);
|
||||||
|
try {
|
||||||
|
capture.process();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,19 +6,25 @@ import android.content.Intent;
|
|||||||
import org.briarproject.bramble.api.account.AccountManager;
|
import org.briarproject.bramble.api.account.AccountManager;
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.test.espresso.intent.rule.IntentsTestRule;
|
import androidx.test.espresso.intent.rule.IntentsTestRule;
|
||||||
|
|
||||||
|
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public abstract class UiTest {
|
public abstract class UiTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static final ScreenshotOnFailureRule screenshotOnFailureRule =
|
||||||
|
new ScreenshotOnFailureRule();
|
||||||
|
|
||||||
protected final String USERNAME =
|
protected final String USERNAME =
|
||||||
getApplicationContext().getString(R.string.screenshot_alice);
|
getApplicationContext().getString(R.string.screenshot_alice);
|
||||||
protected static final String PASSWORD = "123456";
|
protected static final String PASSWORD = "123456";
|
||||||
@@ -27,6 +33,8 @@ public abstract class UiTest {
|
|||||||
protected AccountManager accountManager;
|
protected AccountManager accountManager;
|
||||||
@Inject
|
@Inject
|
||||||
protected LifecycleManager lifecycleManager;
|
protected LifecycleManager lifecycleManager;
|
||||||
|
@Inject
|
||||||
|
protected SettingsManager settingsManager;
|
||||||
|
|
||||||
public UiTest() {
|
public UiTest() {
|
||||||
BriarTestComponentApplication app = getApplicationContext();
|
BriarTestComponentApplication app = getApplicationContext();
|
||||||
@@ -35,26 +43,18 @@ public abstract class UiTest {
|
|||||||
|
|
||||||
protected abstract void inject(BriarUiTestComponent component);
|
protected abstract void inject(BriarUiTestComponent component);
|
||||||
|
|
||||||
|
protected void startActivity(Class<? extends Activity> clazz) {
|
||||||
|
Intent i = new Intent(getApplicationContext(), clazz);
|
||||||
|
i.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
getApplicationContext().startActivity(i);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
protected class CleanAccountTestRule<A extends Activity>
|
protected class CleanAccountTestRule<A extends Activity>
|
||||||
extends IntentsTestRule<A> {
|
extends IntentsTestRule<A> {
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private final Runnable runnable;
|
|
||||||
|
|
||||||
public CleanAccountTestRule(Class<A> activityClass) {
|
public CleanAccountTestRule(Class<A> activityClass) {
|
||||||
super(activityClass);
|
super(activityClass);
|
||||||
this.runnable = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this if you need to run code before launching the activity.
|
|
||||||
* Note: You need to use {@link #launchActivity(Intent)} yourself
|
|
||||||
* to start the activity.
|
|
||||||
*/
|
|
||||||
public CleanAccountTestRule(Class<A> activityClass, Runnable runnable) {
|
|
||||||
super(activityClass, false, false);
|
|
||||||
this.runnable = runnable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -62,16 +62,13 @@ public abstract class UiTest {
|
|||||||
super.beforeActivityLaunched();
|
super.beforeActivityLaunched();
|
||||||
accountManager.deleteAccount();
|
accountManager.deleteAccount();
|
||||||
accountManager.createAccount(USERNAME, PASSWORD);
|
accountManager.createAccount(USERNAME, PASSWORD);
|
||||||
if (runnable != null) {
|
Intent serviceIntent =
|
||||||
Intent serviceIntent =
|
new Intent(getApplicationContext(), BriarService.class);
|
||||||
new Intent(getApplicationContext(), BriarService.class);
|
getApplicationContext().startService(serviceIntent);
|
||||||
getApplicationContext().startService(serviceIntent);
|
try {
|
||||||
try {
|
lifecycleManager.waitForStartup();
|
||||||
lifecycleManager.waitForStartup();
|
} catch (InterruptedException e) {
|
||||||
} catch (InterruptedException e) {
|
throw new AssertionError(e);
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
runnable.run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.briar.android;
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
@@ -14,9 +15,13 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
|
|||||||
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
|
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
|
||||||
import androidx.test.runner.lifecycle.Stage;
|
import androidx.test.runner.lifecycle.Stage;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||||
import static androidx.test.espresso.util.HumanReadables.describe;
|
import static androidx.test.espresso.util.HumanReadables.describe;
|
||||||
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
|
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
|
||||||
|
import static androidx.test.runner.lifecycle.Stage.RESUMED;
|
||||||
import static java.lang.System.currentTimeMillis;
|
import static java.lang.System.currentTimeMillis;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
@@ -25,13 +30,26 @@ public class ViewActions {
|
|||||||
private final static long TIMEOUT_MS = SECONDS.toMillis(10);
|
private final static long TIMEOUT_MS = SECONDS.toMillis(10);
|
||||||
private final static long WAIT_MS = 50;
|
private final static long WAIT_MS = 50;
|
||||||
|
|
||||||
|
public static void waitFor(final Matcher<View> viewMatcher) {
|
||||||
|
onView(isRoot()).perform(waitUntilMatches(hasDescendant(viewMatcher)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void waitFor(final Class<? extends Activity> clazz) {
|
||||||
|
onView(isRoot()).perform(waitForActivity(clazz, RESUMED, TIMEOUT_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void waitFor(final Class<? extends Activity> clazz,
|
||||||
|
long timeout) {
|
||||||
|
onView(isRoot()).perform(waitForActivity(clazz, RESUMED, timeout));
|
||||||
|
}
|
||||||
|
|
||||||
public static ViewAction waitUntilMatches(Matcher<View> viewMatcher) {
|
public static ViewAction waitUntilMatches(Matcher<View> viewMatcher) {
|
||||||
return waitUntilMatches(viewMatcher, TIMEOUT_MS);
|
return waitUntilMatches(viewMatcher, TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ViewAction waitUntilMatches(Matcher<View> viewMatcher,
|
private static ViewAction waitUntilMatches(Matcher<View> viewMatcher,
|
||||||
long timeout) {
|
long timeout) {
|
||||||
return new CustomViewAction() {
|
return new CustomViewAction(timeout) {
|
||||||
@Override
|
@Override
|
||||||
protected boolean exitConditionTrue(View view) {
|
protected boolean exitConditionTrue(View view) {
|
||||||
for (View child : breadthFirstViewTraversal(view)) {
|
for (View child : breadthFirstViewTraversal(view)) {
|
||||||
@@ -48,24 +66,62 @@ public class ViewActions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ViewAction waitForActivity(Activity activity, Stage stage) {
|
public static ViewAction waitForActivity(Class<? extends Activity> clazz,
|
||||||
return new CustomViewAction() {
|
Stage stage, long timeout) {
|
||||||
|
return new CustomViewAction(timeout) {
|
||||||
@Override
|
@Override
|
||||||
protected boolean exitConditionTrue(View view) {
|
protected boolean exitConditionTrue(View view) {
|
||||||
|
boolean found = false;
|
||||||
ActivityLifecycleMonitor lifecycleMonitor =
|
ActivityLifecycleMonitor lifecycleMonitor =
|
||||||
ActivityLifecycleMonitorRegistry.getInstance();
|
ActivityLifecycleMonitorRegistry.getInstance();
|
||||||
return lifecycleMonitor.getLifecycleStageOf(activity) == stage;
|
log(lifecycleMonitor);
|
||||||
|
for (Activity a : lifecycleMonitor
|
||||||
|
.getActivitiesInStage(stage)) {
|
||||||
|
if (a.getClass().equals(clazz)) found = true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(ActivityLifecycleMonitor lifecycleMonitor) {
|
||||||
|
log(lifecycleMonitor, Stage.PRE_ON_CREATE);
|
||||||
|
log(lifecycleMonitor, Stage.CREATED);
|
||||||
|
log(lifecycleMonitor, Stage.STARTED);
|
||||||
|
log(lifecycleMonitor, Stage.RESUMED);
|
||||||
|
log(lifecycleMonitor, Stage.PAUSED);
|
||||||
|
log(lifecycleMonitor, Stage.STOPPED);
|
||||||
|
log(lifecycleMonitor, Stage.RESTARTED);
|
||||||
|
log(lifecycleMonitor, Stage.DESTROYED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(ActivityLifecycleMonitor lifecycleMonitor,
|
||||||
|
Stage stage) {
|
||||||
|
for (Activity a : lifecycleMonitor
|
||||||
|
.getActivitiesInStage(stage)) {
|
||||||
|
Log.e("TEST", a.getClass().getSimpleName() +
|
||||||
|
" is in state " + stage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return "Wait for activity " + activity.getClass().getName() +
|
return "Wait for activity " + clazz.getName() + " in stage " +
|
||||||
" to resume within " + TIMEOUT_MS + " milliseconds.";
|
stage.name() + " within " + timeout +
|
||||||
|
" milliseconds.";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static abstract class CustomViewAction implements ViewAction {
|
private static abstract class CustomViewAction implements ViewAction {
|
||||||
|
private final long timeout;
|
||||||
|
|
||||||
|
public CustomViewAction() {
|
||||||
|
this(TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomViewAction(long timeout) {
|
||||||
|
this.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Matcher<View> getConstraints() {
|
public Matcher<View> getConstraints() {
|
||||||
return isDisplayed();
|
return isDisplayed();
|
||||||
@@ -74,7 +130,7 @@ public class ViewActions {
|
|||||||
@Override
|
@Override
|
||||||
public void perform(UiController uiController, View view) {
|
public void perform(UiController uiController, View view) {
|
||||||
uiController.loopMainThreadUntilIdle();
|
uiController.loopMainThreadUntilIdle();
|
||||||
long endTime = currentTimeMillis() + TIMEOUT_MS;
|
long endTime = currentTimeMillis() + timeout;
|
||||||
do {
|
do {
|
||||||
if (exitConditionTrue(view)) return;
|
if (exitConditionTrue(view)) return;
|
||||||
uiController.loopMainThreadForAtLeast(WAIT_MS);
|
uiController.loopMainThreadForAtLeast(WAIT_MS);
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package org.briarproject.briar.android;
|
|||||||
import org.briarproject.bramble.BrambleAndroidModule;
|
import org.briarproject.bramble.BrambleAndroidModule;
|
||||||
import org.briarproject.bramble.BrambleCoreModule;
|
import org.briarproject.bramble.BrambleCoreModule;
|
||||||
import org.briarproject.bramble.account.BriarAccountModule;
|
import org.briarproject.bramble.account.BriarAccountModule;
|
||||||
|
import org.briarproject.bramble.system.ClockModule;
|
||||||
import org.briarproject.briar.BriarCoreModule;
|
import org.briarproject.briar.BriarCoreModule;
|
||||||
|
import org.briarproject.briar.android.account.SignInTestCreateAccount;
|
||||||
|
import org.briarproject.briar.android.account.SignInTestSignIn;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||||
import org.briarproject.briar.android.attachment.media.MediaModule;
|
import org.briarproject.briar.android.attachment.media.MediaModule;
|
||||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
|
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
|
||||||
@@ -16,6 +19,7 @@ import dagger.Component;
|
|||||||
@Component(modules = {
|
@Component(modules = {
|
||||||
AppModule.class,
|
AppModule.class,
|
||||||
AttachmentModule.class,
|
AttachmentModule.class,
|
||||||
|
ClockModule.class,
|
||||||
MediaModule.class,
|
MediaModule.class,
|
||||||
BriarCoreModule.class,
|
BriarCoreModule.class,
|
||||||
BrambleAndroidModule.class,
|
BrambleAndroidModule.class,
|
||||||
@@ -26,4 +30,8 @@ public interface BriarUiTestComponent extends AndroidComponent {
|
|||||||
|
|
||||||
void inject(NavDrawerActivityTest test);
|
void inject(NavDrawerActivityTest test);
|
||||||
|
|
||||||
|
void inject(SignInTestCreateAccount test);
|
||||||
|
|
||||||
|
void inject(SignInTestSignIn test);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.briarproject.briar.android.account;
|
||||||
|
|
||||||
|
import android.view.Gravity;
|
||||||
|
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.BriarUiTestComponent;
|
||||||
|
import org.briarproject.briar.android.UiTest;
|
||||||
|
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||||
|
import org.briarproject.briar.android.splash.SplashScreenActivity;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import androidx.test.espresso.contrib.DrawerActions;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
|
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||||
|
import static org.briarproject.briar.android.ViewActions.waitFor;
|
||||||
|
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
|
||||||
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class SignInTestCreateAccount extends UiTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void inject(BriarUiTestComponent component) {
|
||||||
|
component.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createAccount() throws Exception {
|
||||||
|
accountManager.deleteAccount();
|
||||||
|
accountManager.createAccount(USERNAME, PASSWORD);
|
||||||
|
|
||||||
|
startActivity(SplashScreenActivity.class);
|
||||||
|
lifecycleManager.waitForStartup();
|
||||||
|
waitFor(NavDrawerActivity.class);
|
||||||
|
|
||||||
|
// open nav drawer
|
||||||
|
onView(withId(R.id.drawer_layout))
|
||||||
|
.check(matches(isClosed(Gravity.START)))
|
||||||
|
.perform(DrawerActions.open());
|
||||||
|
|
||||||
|
// click onboarding away (once shown)
|
||||||
|
onView(isRoot()).perform(waitUntilMatches(hasDescendant(
|
||||||
|
withClassName(endsWith("PromptView")))));
|
||||||
|
onView(withClassName(endsWith("PromptView")))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
// sign-out manually
|
||||||
|
onView(withText(R.string.sign_out_button))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(click());
|
||||||
|
lifecycleManager.waitForShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package org.briarproject.briar.android.account;
|
||||||
|
|
||||||
|
import android.view.Gravity;
|
||||||
|
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.BriarUiTestComponent;
|
||||||
|
import org.briarproject.briar.android.UiTest;
|
||||||
|
import org.briarproject.briar.android.login.StartupActivity;
|
||||||
|
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||||
|
import org.briarproject.briar.android.splash.SplashScreenActivity;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||||
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
|
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static org.briarproject.briar.android.ViewActions.waitFor;
|
||||||
|
import static org.hamcrest.CoreMatchers.allOf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This relies on class sorting to run after {@link SignInTestCreateAccount}.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class SignInTestSignIn extends UiTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void inject(BriarUiTestComponent component) {
|
||||||
|
component.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void signIn() throws Exception {
|
||||||
|
startActivity(SplashScreenActivity.class);
|
||||||
|
|
||||||
|
waitFor(StartupActivity.class);
|
||||||
|
|
||||||
|
// enter password
|
||||||
|
onView(withId(R.id.edit_password))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(replaceText(PASSWORD));
|
||||||
|
onView(withId(R.id.btn_sign_in))
|
||||||
|
.check(matches(allOf(isDisplayed(), isEnabled())))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
lifecycleManager.waitForStartup();
|
||||||
|
waitFor(NavDrawerActivity.class);
|
||||||
|
|
||||||
|
// ensure nav drawer is visible
|
||||||
|
onView(withId(R.id.drawer_layout))
|
||||||
|
.check(matches(isClosed(Gravity.START)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,15 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||||
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class PngSuiteImageCompressorTest
|
public class PngSuiteImageCompressorTest
|
||||||
extends AbstractImageCompressorTest {
|
extends AbstractImageCompressorTest {
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||||
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class PngSuiteImageSizeCalculatorTest
|
public class PngSuiteImageSizeCalculatorTest
|
||||||
extends AbstractImageSizeCalculatorTest {
|
extends AbstractImageSizeCalculatorTest {
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package org.briarproject.briar.android.conversation;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import org.briarproject.briar.R;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import androidx.test.rule.ActivityTestRule;
|
|
||||||
|
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
|
||||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
|
|||||||
import org.briarproject.bramble.BrambleAndroidModule;
|
import org.briarproject.bramble.BrambleAndroidModule;
|
||||||
import org.briarproject.bramble.BrambleCoreModule;
|
import org.briarproject.bramble.BrambleCoreModule;
|
||||||
import org.briarproject.bramble.account.BriarAccountModule;
|
import org.briarproject.bramble.account.BriarAccountModule;
|
||||||
|
import org.briarproject.bramble.system.ClockModule;
|
||||||
import org.briarproject.briar.BriarCoreModule;
|
import org.briarproject.briar.BriarCoreModule;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||||
import org.briarproject.briar.android.attachment.media.MediaModule;
|
import org.briarproject.briar.android.attachment.media.MediaModule;
|
||||||
@@ -17,6 +18,7 @@ import dagger.Component;
|
|||||||
@Component(modules = {
|
@Component(modules = {
|
||||||
AppModule.class,
|
AppModule.class,
|
||||||
AttachmentModule.class,
|
AttachmentModule.class,
|
||||||
|
ClockModule.class,
|
||||||
MediaModule.class,
|
MediaModule.class,
|
||||||
BriarCoreModule.class,
|
BriarCoreModule.class,
|
||||||
BrambleAndroidModule.class,
|
BrambleAndroidModule.class,
|
||||||
@@ -31,4 +33,6 @@ public interface BriarUiTestComponent extends AndroidComponent {
|
|||||||
|
|
||||||
void inject(SettingsActivityScreenshotTest test);
|
void inject(SettingsActivityScreenshotTest test);
|
||||||
|
|
||||||
|
void inject(PromoVideoTest test);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
|
import androidx.test.espresso.UiController;
|
||||||
|
import androidx.test.espresso.ViewAction;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.action.GeneralLocation.VISIBLE_CENTER;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
|
||||||
|
|
||||||
|
public class OverlayTapViewAction implements ViewAction {
|
||||||
|
|
||||||
|
public static ViewAction visualClick(OverlayView overlayView) {
|
||||||
|
return new OverlayTapViewAction(overlayView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final OverlayView overlayView;
|
||||||
|
|
||||||
|
public OverlayTapViewAction(OverlayView overlayView) {
|
||||||
|
this.overlayView = overlayView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints() {
|
||||||
|
return isDisplayingAtLeast(90);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(UiController uiController, View view) {
|
||||||
|
float[] coordinates = VISIBLE_CENTER.calculateCoordinates(view);
|
||||||
|
overlayView.tap(coordinates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package org.briarproject.briar.android;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static android.content.Context.WINDOW_SERVICE;
|
||||||
|
import static android.provider.Settings.canDrawOverlays;
|
||||||
|
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||||
|
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
||||||
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
|
import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A full-screen overlay used to make taps visible in instrumentation tests.
|
||||||
|
*/
|
||||||
|
public class OverlayView extends View {
|
||||||
|
|
||||||
|
public static OverlayView attach(Context ctx) throws Throwable {
|
||||||
|
assertTrue(canDrawOverlays(ctx));
|
||||||
|
OverlayView view = new OverlayView(getApplicationContext());
|
||||||
|
runOnUiThread(() -> attachInternal(ctx, view));
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void attachInternal(Context ctx, OverlayView view) {
|
||||||
|
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
||||||
|
FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE,
|
||||||
|
PixelFormat.TRANSLUCENT);
|
||||||
|
WindowManager wm = (WindowManager) ctx.getSystemService(WINDOW_SERVICE);
|
||||||
|
wm.addView(view, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Random random = new Random();
|
||||||
|
private final Paint paint;
|
||||||
|
private final int yOffset;
|
||||||
|
@Nullable
|
||||||
|
private float[] coordinates;
|
||||||
|
|
||||||
|
public OverlayView(Context ctx) {
|
||||||
|
super(ctx);
|
||||||
|
int resourceId = getResources()
|
||||||
|
.getIdentifier("status_bar_height", "dimen", "android");
|
||||||
|
yOffset = getResources().getDimensionPixelSize(resourceId);
|
||||||
|
paint = new Paint();
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
paint.setARGB(175, 255, 0, 0);
|
||||||
|
setWillNotDraw(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void tap(float[] coordinates) {
|
||||||
|
this.coordinates = coordinates;
|
||||||
|
invalidate();
|
||||||
|
new Handler().postDelayed(this::untap, 750);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void untap() {
|
||||||
|
this.coordinates = null;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
if (coordinates == null) return;
|
||||||
|
float x = coordinates[0] + random.nextInt(42);
|
||||||
|
float y = coordinates[1] - yOffset + random.nextInt(13);
|
||||||
|
canvas.drawCircle(x, y, 42, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user