Compare commits

..

121 Commits

Author SHA1 Message Date
Sebastian Kürten
4c29c4fa5b Add a bunch of tests highlighting implicit acks 2021-03-19 18:33:48 +01:00
Sebastian Kürten
34948fce13 Implement auto-declining for self-destructed introductions 2021-03-19 18:31:17 +01:00
Torsten Grote
f604320989 Merge branch '1835-auto-decline-self-destructed-incoming-blog-forum-sharing' into '804-self-destructing-messages'
Automatically decline incoming blog/forum invitations when they self-destruct

See merge request briar/briar!1392
2021-03-16 11:23:41 +00:00
Daniel Lublin
ea8c78bfef Make sure invitation accept msg is linking to shareable 2021-03-16 11:07:44 +01:00
Daniel Lublin
37d8a34c69 Add test for invitee responding after sharer deleted invitation 2021-03-16 11:07:44 +01:00
Daniel Lublin
45e3c56eb8 Keep members in subclasses and use getters 2021-03-16 11:07:44 +01:00
Daniel Lublin
647467a51a Assert that expected event is broadcasted 2021-03-16 11:07:43 +01:00
Daniel Lublin
144bd7bf1d Remember when invitation was auto-declined due to deletion
And render differently
2021-03-16 11:07:43 +01:00
Daniel Lublin
9d1c49c6b6 Auto-decline/auto-delete Forum & Blog sharing invitations/responses 2021-03-16 11:07:42 +01:00
akwizgran
f5c2bed528 Merge branch '804-auto-decline-unread' into '804-self-destructing-messages'
No notification for auto-decline messages

See merge request briar/briar!1406
2021-03-15 13:20:54 +00:00
Torsten Grote
b4de1424ac Don't show notification for own auto-decline responses 2021-03-15 08:58:08 -03:00
akwizgran
f184bfe9ac Merge branch 'test-event-listener' into '804-self-destructing-messages'
Add a way to check for expected events

See merge request briar/briar!1394
2021-03-09 13:35:59 +00:00
Torsten Grote
d3204ab3ee Use AtomicReference in TestEventListener to store event 2021-03-09 10:11:15 -03:00
akwizgran
163565e15c Merge branch '804-real-time-update' into '804-self-destructing-messages'
Update support for disappearing messages in real time

See merge request briar/briar!1397
2021-03-08 17:57:41 +00:00
Torsten Grote
ef46f8ed64 Update support for disappearing messages in real time 2021-03-08 14:22:15 -03:00
Torsten Grote
f42a9a20d8 Add a way to check for expected events
and use it for private group auto-declines
2021-03-08 12:15:11 -03:00
Torsten Grote
03c55311c7 Merge branch 'messaging-module-feature-flags' into '804-self-destructing-messages'
Don't advertise support for messaging features that are disabled by flags

See merge request briar/briar!1390
2021-03-08 11:47:09 +00:00
akwizgran
ad67a0abd6 Merge branch 'count-sent-messages-in-integration-tests' into '804-self-destructing-messages'
Count sent messages in integration tests

See merge request briar/briar!1393
2021-03-04 14:50:37 +00:00
akwizgran
9bc3a2c73d Count sent messages in integration tests. 2021-03-03 17:00:25 +00:00
akwizgran
af55d181e9 Merge branch '1836-delete-group-messages' into '804-self-destructing-messages'
Automatically decline incoming private group invitations when they self-destruct

See merge request briar/briar!1389
2021-03-03 16:56:16 +00:00
Torsten Grote
dde94baebd Use stored session metadata instead of fetching it again 2021-03-03 11:32:38 -03:00
Torsten Grote
64e3940f77 Render automatic declines differently in the UI
and show them as they happen via an Event
2021-03-02 14:06:06 -03:00
Torsten Grote
012ab0310f Remember when declines were automatic due to deletion
so they can be shown differently for sender
2021-03-02 14:06:05 -03:00
Torsten Grote
63012d0a72 Add integration tests for auto-deletion of private group invitations and responses 2021-03-02 10:35:00 -03:00
Torsten Grote
ebd233d005 Factor out auto-delete integration test code
so we can re-use it in other tests
2021-03-02 10:35:00 -03:00
akwizgran
f3b4440105 Don't advertise support for messaging features that are disabled by flags. 2021-03-02 13:04:23 +00:00
Torsten Grote
e36f275be7 Auto-delete PrivateGroup invitations and responses as well 2021-03-02 08:35:42 -03:00
akwizgran
3dc3d384d3 Merge branch '1833-update-onboarding-ui' into '804-self-destructing-messages'
Update self-destructing onboarding

See merge request briar/briar!1385
2021-03-02 10:37:13 +00:00
Torsten Grote
0c0c34696a Turn 'Learn more' link into a button to have a larger tap area
and a selectable background
2021-03-01 18:09:13 -03:00
Torsten Grote
7047f7d3d6 Turn ConversationSettingsLearnMoreDialog into a generic Onboarding fragment 2021-03-01 18:09:12 -03:00
Torsten Grote
abc8c86eaa Update auto-delete onboarding text 2021-03-01 18:09:12 -03:00
akwizgran
fa14448aa7 Show disabled menu item if we support feature but contact doesn't. 2021-03-01 17:57:06 -03:00
akwizgran
654603cfad Don't advertise support for disappearing messages unless flag is enabled. 2021-03-01 17:57:06 -03:00
akwizgran
112dace96c Hide disappearing messages menu item unless feature flag is enabled. 2021-03-01 17:57:06 -03:00
akwizgran
35bdb8075f Add feature flag for self-destructing messages. 2021-03-01 17:57:05 -03:00
Torsten Grote
f9d312a632 Replace all messages when re-loading
to ensure that messages deleted in the meantime get removed
2021-03-01 17:57:05 -03:00
Torsten Grote
dd93c6852e Remove auto-deleted messages immediately from conversation 2021-03-01 17:57:05 -03:00
Torsten Grote
5c4d971873 Replace MessagesCleanedUpEvent with ConversationMessagesDeletedEvent 2021-03-01 17:57:04 -03:00
akwizgran
1d9acc7425 Wait for events to be delivered before continuing with test. 2021-03-01 17:57:04 -03:00
akwizgran
6738287a83 Log how long it takes to deliver private messages and attachments. 2021-03-01 17:57:04 -03:00
akwizgran
ea4f763a55 Remove redundant call to getAutoDeleteTimer(). 2021-03-01 17:57:03 -03:00
akwizgran
9d027fb250 Check group counts in AutoDeleteIntegrationTest. 2021-03-01 17:57:03 -03:00
akwizgran
b56a9beb1d Include legacy messages when recalculating group count. 2021-03-01 17:57:03 -03:00
akwizgran
1083507752 Delete private messages when their timers expire (needs UI support). 2021-03-01 17:57:02 -03:00
akwizgran
0d2137f0f8 Move ConversationManagerImpl to conversation package. 2021-03-01 17:57:02 -03:00
akwizgran
f9ddb3a3a4 Set default timer duration to 1 minute for testing. 2021-03-01 17:57:02 -03:00
akwizgran
539198026d Update javadoc to explain that a new timer can be set. 2021-03-01 17:57:01 -03:00
akwizgran
d84fb0e761 Pass message IDs to cleanup hooks in batches. 2021-03-01 17:57:01 -03:00
akwizgran
7bf07b3b84 Group messages by group ID when fetching them from database. 2021-03-01 17:57:01 -03:00
akwizgran
e4da7968e3 Throw an exception if no cleanup hook was registered. 2021-03-01 17:57:00 -03:00
akwizgran
f0e4e3c164 Remove copypasta. 2021-03-01 17:57:00 -03:00
akwizgran
fac4132289 Add comment to explain that starting timer may be a no-op. 2021-03-01 17:57:00 -03:00
akwizgran
917da9ce36 Stop the timer if no hook has been registered. 2021-03-01 17:56:59 -03:00
akwizgran
e9249a9463 Add javadocs for CleanupManager and CleanupHook. 2021-03-01 17:56:59 -03:00
akwizgran
ec0a59db01 Simplify deadline comparison logic. 2021-03-01 17:56:59 -03:00
akwizgran
7e62d2aeff Stop the cleanup timer if the hook returns false. 2021-03-01 17:56:58 -03:00
akwizgran
3d8826cef9 Add cleanup manager. 2021-03-01 17:56:58 -03:00
akwizgran
6113b4ebee Query message IDs rather than metadata when only IDs are needed. 2021-03-01 17:56:58 -03:00
Torsten Grote
00e3e64495 Add support for showing auto-delete timers in minutes 2021-03-01 17:56:57 -03:00
Torsten Grote
115724a0a4 Show actual auto-delete timer duration in UI
(only days and hours for now)
2021-03-01 17:56:57 -03:00
Torsten Grote
0a92f0516f Show outgoing message status icon in same color as time 2021-03-01 17:56:57 -03:00
Torsten Grote
cc09a6deb2 Fix bomb icon color
in incoming image messages without text (on old phones)
2021-03-01 17:56:56 -03:00
Torsten Grote
5888775300 Get rid of SENDING state and publish new live data in order on UiThread 2021-03-01 17:56:56 -03:00
Torsten Grote
712f0f7cd9 Return LiveData when sending message 2021-03-01 17:56:56 -03:00
Torsten Grote
0d3f531545 Show warning dialog when auto-delete timer has changed since starting to compose message 2021-03-01 17:56:55 -03:00
Torsten Grote
b02629bf34 Add "Tap to learn more" to message bubbles for timer changes 2021-03-01 17:56:55 -03:00
akwizgran
b6693071f9 Provide clock for UI tests. 2021-03-01 17:56:55 -03:00
akwizgran
ff739e1982 Add some comments. 2021-03-01 17:56:54 -03:00
akwizgran
47fa7ccc81 Sync acks for initial messages when setting up integration tests. 2021-03-01 17:56:54 -03:00
akwizgran
1cf1e8b617 Allow time travel in integration tests. 2021-03-01 17:56:54 -03:00
akwizgran
b012f0991f Inject DefaultTaskSchedulerModule.EagerSingletons at startup in headless app. 2021-03-01 17:56:53 -03:00
akwizgran
1b7a1de881 Refactor integration tests to allow clock to be replaced. 2021-03-01 17:56:53 -03:00
Sebastian Kürten
8510fc80c9 Introduce conversation settings screen 2021-03-01 17:20:33 -03:00
Torsten Grote
5ae2a37d37 Create group invitation with read-write transaction
because the AutoDeleteManager needs to change the DB
and otherwise crashes.

Closes #1863
2021-03-01 17:20:33 -03:00
Torsten Grote
b032a84902 Make view state of text send UI easier to reason about
and fix bugs with bomb badge and hint display
2021-03-01 17:20:32 -03:00
Torsten Grote
f6414b5ca1 Show bomb badge in same style as send button 2021-03-01 17:20:32 -03:00
Torsten Grote
c5669bece5 Show a bomb badge on the send button when disappearing messages is active 2021-03-01 17:20:32 -03:00
Torsten Grote
8c76db6216 Use a different hint in conversation when message will disappear
and keep the hint updated when the auto-delete timer changes
2021-03-01 17:20:31 -03:00
Torsten Grote
370fe7601d Broadcast event when auto delete timer is mirrored 2021-03-01 17:20:31 -03:00
Torsten Grote
55de9859e0 Remove mirrored timer texts
as we can't detect reliably if a timer setting was mirrored or manually changed.

Also remove item update optimization from adapter as this can cause issues when items already exist.
2021-03-01 17:20:31 -03:00
Torsten Grote
06a8086502 Show timer change notices in private conversations 2021-03-01 17:20:30 -03:00
Torsten Grote
5522929b9b Allow setting a self-destruct timer
This is a rough prototype of #1837 meant to make testing the UI easier.
2021-03-01 17:20:30 -03:00
akwizgran
50d4f825c8 Use Collections.sort() to satisfy Animal Sniffer. 2021-03-01 17:20:30 -03:00
akwizgran
6cc225ec22 Add integration tests for timer mirroring. 2021-03-01 17:20:29 -03:00
akwizgran
aaa03cd809 Add method for UI and tests to get current timer. 2021-03-01 17:20:29 -03:00
akwizgran
890c9837b5 Update integration tests. 2021-03-01 17:20:29 -03:00
akwizgran
6ac54af432 Don't receive auto-delete timer from remote accept message as introducee. 2021-03-01 17:20:28 -03:00
akwizgran
0ccbd57d1d Hook up incoming messages to the auto-delete manager. 2021-03-01 17:20:28 -03:00
akwizgran
66d3e8950e Mirror the remote auto-delete timer. 2021-03-01 17:20:28 -03:00
akwizgran
d291b7796e Add integration tests for auto-delete timer. 2021-03-01 17:20:27 -03:00
akwizgran
7af863160d Forwarded accept messages aren't visible to the introducee. 2021-03-01 17:20:27 -03:00
akwizgran
c74e585668 Only use conversation timestamp for messages that will be visible in conversation. 2021-03-01 17:20:26 -03:00
akwizgran
0fd56b6c38 Get timestamp for abort message in same way as other messages. 2021-03-01 17:20:26 -03:00
akwizgran
57264b0f04 Look up auto-delete timer when creating private group invitation. 2021-03-01 17:20:26 -03:00
akwizgran
70225c5380 Use the right timestamp when signing private group invitation. 2021-03-01 17:20:25 -03:00
akwizgran
f0a602a579 Provide TransactionManager. 2021-03-01 17:20:22 -03:00
akwizgran
f9ab242f43 Look up conversation timestamp when creating group invitation messages. 2021-03-01 17:13:52 -03:00
akwizgran
24efc29722 Move lookup of latest conversation timestamp to core for blog and forum sharing. 2021-03-01 17:13:52 -03:00
akwizgran
e72169ecee Move lookup of latest conversation timestamp to core. 2021-03-01 17:13:52 -03:00
akwizgran
47db28a738 Add transactional variant of getGroupCount(). 2021-03-01 17:13:51 -03:00
akwizgran
30261b0dcf Send current minor version of messaging client to contacts. 2021-03-01 17:13:51 -03:00
Torsten Grote
10f9df4dc8 Show bomb icon for messages with auto-destruct timer 2021-03-01 17:13:51 -03:00
akwizgran
60dd260a5a Check that timer argument is legal before storing. 2021-03-01 17:13:51 -03:00
akwizgran
579a72f54b Add unit tests for AutoDeleteManagerImpl. 2021-03-01 17:13:50 -03:00
akwizgran
e270910399 Implement AutoDeleteManager. 2021-03-01 17:13:50 -03:00
akwizgran
c1483ea61a Add dummy implementation of AutoDeleteManager. 2021-03-01 17:13:50 -03:00
akwizgran
35751ecef6 Refactor auto-delete code from Bramble to Briar. 2021-03-01 17:13:49 -03:00
akwizgran
37d058a766 Rewrap lines. 2021-03-01 17:13:49 -03:00
akwizgran
5a6846c972 Factor out methods for storing and retrieving contact ID. 2021-03-01 17:13:49 -03:00
akwizgran
b34e6ee2a7 Factor out method for validating auto-delete timers. 2021-03-01 17:13:48 -03:00
akwizgran
2ae1e9631f Update comments. 2021-03-01 17:13:48 -03:00
akwizgran
76f2859a45 Add unit tests for validating auto-delete timer. 2021-03-01 17:13:48 -03:00
akwizgran
1c25b2da82 Update private group invitation client to include self-destruct timers. 2021-03-01 17:13:47 -03:00
akwizgran
26fe2f804f Update blog and forum sharing clients to include self-destruct timers. 2021-03-01 17:13:47 -03:00
akwizgran
c8d1ee878c Update message parsing and encoding to include auto-delete timer. 2021-03-01 17:13:47 -03:00
akwizgran
7b16e78470 Update introduction validator to support auto-delete timers. 2021-03-01 17:13:46 -03:00
akwizgran
7fb71a6cb9 Add constant for NO_AUTO_DELETE_TIMER, address review comments. 2021-03-01 17:13:46 -03:00
akwizgran
fc076954a3 Add unit tests for private message validation. 2021-03-01 17:13:46 -03:00
akwizgran
6a1d2e65ba Fix comments in PrivateMessageValidator. 2021-03-01 17:13:45 -03:00
akwizgran
d67cbd40bd Add integration test for auto-delete timer in private messages. 2021-03-01 17:13:45 -03:00
akwizgran
dba85debfa Add auto-deletion timer to private messages. 2021-03-01 17:13:45 -03:00
366 changed files with 7001 additions and 11330 deletions

1
.gitignore vendored
View File

@@ -18,7 +18,6 @@ local.properties
# Android Studio # Android Studio
.idea/* .idea/*
!.idea/inspectionProfiles/
!.idea/runConfigurations/ !.idea/runConfigurations/
!.idea/codeStyleSettings.xml !.idea/codeStyleSettings.xml
!.idea/codeStyles !.idea/codeStyles

View File

@@ -1,79 +1,30 @@
image: briar/ci-image-android:latest image: briar/ci-image-android:latest
stages: stages:
- test - test
- optional_tests - optional_tests
- check_reproducibility - check_reproducibility
workflow: test:
# when to create a CI pipeline stage: test
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 and should not be cached # these file change every time but 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
@@ -89,7 +40,6 @@ 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
@@ -102,15 +52,11 @@ 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/
bridge test: manual_tests:
extends: .optional_tests extends: .optional_tests
rules: when: manual
- if: '$CI_PIPELINE_SOURCE == "schedule"' except:
when: on_success - tags
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

View File

@@ -1,14 +0,0 @@
<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>

View File

@@ -1,51 +0,0 @@
<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>

View File

@@ -6,17 +6,13 @@ apply from: 'witness.gradle'
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.3' buildToolsVersion '30.0.2'
packagingOptions {
doNotStrip '**/*.so'
}
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 29
versionCode 10304 versionCode 10216
versionName "1.3.4" versionName "1.2.16"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -59,8 +59,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class AndroidBluetoothPlugin extends class AndroidBluetoothPlugin
AbstractBluetoothPlugin<BluetoothSocket, BluetoothServerSocket> { extends BluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
private static final Logger LOG = private static final Logger LOG =
getLogger(AndroidBluetoothPlugin.class.getName()); getLogger(AndroidBluetoothPlugin.class.getName());
@@ -75,7 +75,6 @@ class AndroidBluetoothPlugin extends
// 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,
@@ -176,11 +175,6 @@ class AndroidBluetoothPlugin extends
} 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);
} }
} }
@@ -188,40 +182,22 @@ class AndroidBluetoothPlugin extends
@Nullable @Nullable
DuplexTransportConnection discoverAndConnect(String uuid) { DuplexTransportConnection discoverAndConnect(String uuid) {
if (adapter == null) return null; if (adapter == null) return null;
if (!discoverSemaphore.tryAcquire()) { for (String address : discoverDevices()) {
LOG.info("Discover already running"); try {
return null; if (LOG.isLoggable(INFO))
} LOG.info("Connecting to " + scrubMacAddress(address));
try { return connectTo(address, uuid);
stopDiscoverAndConnect = false; } catch (IOException e) {
for (String address : discoverDevices()) { if (LOG.isLoggable(INFO)) {
if (stopDiscoverAndConnect) { LOG.info("Could not connect to "
break; + scrubMacAddress(address));
}
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<>();

View File

@@ -10,20 +10,17 @@ 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;
@@ -117,21 +114,12 @@ 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() {
return new String[] {"image/jpeg", "image/png", "image/gif"}; if (SDK_INT < 24) return new String[] {"image/jpeg", "image/png"};
} 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;
}
} }
} }

View File

@@ -10,6 +10,4 @@ public interface FeatureFlags {
boolean shouldEnableProfilePictures(); boolean shouldEnableProfilePictures();
boolean shouldEnableDisappearingMessages(); boolean shouldEnableDisappearingMessages();
boolean shouldEnableConnectViaBluetooth();
} }

View File

@@ -1,35 +0,0 @@
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;
}
}

View File

@@ -57,7 +57,6 @@ 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;
} }

View File

@@ -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 begins. * An event that is broadcast when a BQP protocol completes.
*/ */
public class KeyAgreementStartedEvent extends Event { public class KeyAgreementStartedEvent extends Event {
} }

View File

@@ -21,6 +21,5 @@ 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 = String DEV_ONION_ADDRESS = "cwqmubyvnig3wag3.onion";
"b2nkt5doeamvdmjzfz7g42hk5vdtlnktlgzhel2bgjcc4v4jhnx2qrqd.onion";
} }

View File

@@ -68,13 +68,6 @@ 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.
*/ */

View File

@@ -6,7 +6,6 @@ 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;
@@ -1014,7 +1013,6 @@ 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);
} }

View File

@@ -37,10 +37,4 @@ 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";
} }

View File

@@ -84,14 +84,9 @@ 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);
} }
} }

View File

@@ -81,7 +81,6 @@ 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();

View File

@@ -83,7 +83,6 @@ 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;
@@ -366,14 +365,9 @@ 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,
@@ -398,19 +392,13 @@ 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);
@@ -437,11 +425,6 @@ 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
@@ -517,16 +500,6 @@ 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);

View File

@@ -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 final Predicate<Record> ACCEPT = r -> private static 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 final Predicate<Record> IGNORE = r -> private static Predicate<Record> IGNORE = r ->
r.getProtocolVersion() == PROTOCOL_VERSION && r.getProtocolVersion() == PROTOCOL_VERSION &&
!isKnownRecordType(r.getRecordType()); !isKnownRecordType(r.getRecordType());

View File

@@ -1,633 +0,0 @@
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;
}
}
}

View File

@@ -7,15 +7,14 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
interface BluetoothConnectionLimiter { interface BluetoothConnectionLimiter {
/** /**
* Tells the limiter to not allow regular polling connections (because we * Informs the limiter that key agreement has started.
* are about to do key agreement, or connect via BT for setup).
*/ */
void startLimiting(); void keyAgreementStarted();
/** /**
* Tells the limiter to no longer limit regular polling connections. * Informs the limiter that key agreement has ended.
*/ */
void endLimiting(); void keyAgreementEnded();
/** /**
* 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

View File

@@ -30,37 +30,34 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
private final List<DuplexTransportConnection> connections = private final List<DuplexTransportConnection> connections =
new LinkedList<>(); new LinkedList<>();
@GuardedBy("lock") @GuardedBy("lock")
private int limitingInProgress = 0; private boolean keyAgreementInProgress = false;
BluetoothConnectionLimiterImpl(EventBus eventBus) { BluetoothConnectionLimiterImpl(EventBus eventBus) {
this.eventBus = eventBus; this.eventBus = eventBus;
} }
@Override @Override
public void startLimiting() { public void keyAgreementStarted() {
synchronized (lock) { synchronized (lock) {
limitingInProgress++; keyAgreementInProgress = true;
} }
LOG.info("Limiting started"); LOG.info("Key agreement started");
eventBus.broadcast(new CloseSyncConnectionsEvent(ID)); eventBus.broadcast(new CloseSyncConnectionsEvent(ID));
} }
@Override @Override
public void endLimiting() { public void keyAgreementEnded() {
synchronized (lock) { synchronized (lock) {
limitingInProgress--; keyAgreementInProgress = false;
if (limitingInProgress < 0) {
throw new IllegalStateException();
}
} }
LOG.info("Limiting ended"); LOG.info("Key agreement ended");
} }
@Override @Override
public boolean canOpenContactConnection() { public boolean canOpenContactConnection() {
synchronized (lock) { synchronized (lock) {
if (limitingInProgress > 0) { if (keyAgreementInProgress) {
LOG.info("Can't open contact connection while limiting"); LOG.info("Can't open contact connection during key agreement");
return false; return false;
} else { } else {
LOG.info("Can open contact connection"); LOG.info("Can open contact connection");

View File

@@ -1,22 +1,606 @@
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;
@NotNullByDefault import static java.util.logging.Level.INFO;
public interface BluetoothPlugin extends DuplexPlugin { 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;
boolean isDiscovering(); @MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BluetoothPlugin<S, SS> implements DuplexPlugin, EventListener {
void disablePolling(); private static final Logger LOG =
getLogger(BluetoothPlugin.class.getName());
void enablePolling(); 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();
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
DuplexTransportConnection discoverAndConnectForSetup(String uuid); abstract DuplexTransportConnection discoverAndConnect(String uuid);
void stopDiscoverAndConnect(); BluetoothPlugin(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 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;
}
}
} }

View File

@@ -1,9 +1,13 @@
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

View File

@@ -41,12 +41,8 @@ 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;
@@ -2350,67 +2346,6 @@ 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 @Test
public void testCleanupTimer() throws Exception { public void testCleanupTimer() throws Exception {
long duration = 60_000; long duration = 60_000;
@@ -2547,31 +2482,6 @@ 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);

View File

@@ -38,11 +38,6 @@ public class BrambleCoreIntegrationTestModule {
public boolean shouldEnableDisappearingMessages() { public boolean shouldEnableDisappearingMessages() {
return true; return true;
} }
@Override
public boolean shouldEnableConnectViaBluetooth() {
return true;
}
}; };
} }
} }

View File

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

View File

@@ -25,8 +25,8 @@ import static org.briarproject.bramble.util.StringUtils.isValidMac;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class JavaBluetoothPlugin extends class JavaBluetoothPlugin
AbstractBluetoothPlugin<StreamConnection, StreamConnectionNotifier> { extends BluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
private static final Logger LOG = private static final Logger LOG =
getLogger(JavaBluetoothPlugin.class.getName()); getLogger(JavaBluetoothPlugin.class.getName());
@@ -108,11 +108,6 @@ class JavaBluetoothPlugin extends
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";
} }

View File

@@ -25,7 +25,6 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.net.SocketFactory; import javax.net.SocketFactory;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.OsUtils.isLinux; import static org.briarproject.bramble.util.OsUtils.isLinux;
@@ -97,15 +96,8 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
String architecture = null; String architecture = null;
if (isLinux()) { if (isLinux()) {
String arch = System.getProperty("os.arch"); String arch = System.getProperty("os.arch");
if (LOG.isLoggable(INFO)) {
LOG.info("System's os.arch is " + arch);
}
if (arch.equals("amd64")) { if (arch.equals("amd64")) {
architecture = "linux-x86_64"; architecture = "linux-x86_64";
} else if (arch.equals("aarch64")) {
architecture = "linux-aarch64";
} else if (arch.equals("arm")) {
architecture = "linux-armhf";
} }
} }
if (architecture == null) { if (architecture == null) {
@@ -113,10 +105,6 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
return null; return null;
} }
if (LOG.isLoggable(INFO)) {
LOG.info("The selected architecture for Tor is " + architecture);
}
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl(); TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl();

View File

@@ -23,10 +23,8 @@ 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;
@@ -47,22 +45,15 @@ import static org.junit.Assume.assumeTrue;
public class BridgeTest extends BrambleTestCase { public class BridgeTest extends BrambleTestCase {
@Parameters @Parameters
public static Iterable<Params> data() { public static Iterable<String> data() {
BrambleJavaIntegrationTestComponent component = BrambleJavaIntegrationTestComponent component =
DaggerBrambleJavaIntegrationTestComponent.builder().build(); DaggerBrambleJavaIntegrationTestComponent.builder().build();
BrambleCoreIntegrationTestEagerSingletons.Helper BrambleCoreIntegrationTestEagerSingletons.Helper
.injectEagerSingletons(component); .injectEagerSingletons(component);
// Share a failure counter among all the test instances return component.getCircumventionProvider().getBridges(false);
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());
@@ -89,13 +80,11 @@ 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(Params params) { public BridgeTest(String bridge) {
bridge = params.bridge; this.bridge = bridge;
failures = params.failures;
} }
@Before @Before
@@ -163,24 +152,10 @@ public class BridgeTest extends BrambleTestCase {
clock.sleep(500); clock.sleep(500);
} }
if (plugin.getState() != ACTIVE) { if (plugin.getState() != ACTIVE) {
LOG.warning("Could not connect to Tor within timeout"); fail("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;
}
}
} }

View File

@@ -24,7 +24,7 @@ dependencyVerification {
'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8', 'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8',
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8', 'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766', 'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766',
'org.briarproject:tor:0.3.5.13-1:tor-0.3.5.13-1.zip:ef35c16bf8dc1f4c75ed71d9f55e4514f383d124ec96b859aca647c990927c99', 'org.briarproject:tor:0.3.5.13:tor-0.3.5.13.zip:1c5f0b821ee2aadb0ea04aa96caab3ca0a08370cce8de81c2dfe04d172f8a2a0',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d', 'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a', 'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53', 'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',

View File

@@ -10,6 +10,4 @@ src/main/res/values-iw
/fastlane/metadata/android/screenshots.html /fastlane/metadata/android/screenshots.html
/fastlane/metadata/android/*/images /fastlane/metadata/android/*/images
/fastlane/report.xml /fastlane/report.xml
/fastlane/README.md /fastlane/README.md
/fastlane/metadata/android/*/changelogs

View File

@@ -9,19 +9,3 @@ 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

View File

@@ -17,17 +17,13 @@ def getStdout = { command, defaultValue ->
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.3' buildToolsVersion '30.0.2'
packagingOptions {
doNotStrip '**/*.so'
}
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 29
versionCode 10304 versionCode 10216
versionName "1.3.4" versionName "1.2.16"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -80,7 +76,6 @@ android {
} }
testOptions { testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests { unitTests {
includeAndroidResources = true includeAndroidResources = true
} }
@@ -149,8 +144,6 @@ 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'

View File

@@ -1,2 +1,2 @@
json_key_file(ENV["BRIAR_GOOGLE_PLAY_JSON_KEY_FILE"]) json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("org.briarproject.briar.android") package_name("org.briarproject.briar.android")

View File

@@ -13,8 +13,6 @@
# 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
@@ -26,21 +24,6 @@ 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

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Missatgeria segura, arreu.

View File

@@ -1 +0,0 @@
Briar

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Sicher kommunizieren, überall

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Secure messaging, anywhere.

View File

@@ -1 +0,0 @@
https://www.youtube.com/watch?v=ark7Y4u__SM

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Mensajería segura, en cualquier lado.

View File

@@ -1 +0,0 @@
https://www.youtube.com/watch?v=OuJjWdDfKuY

View File

@@ -1,5 +0,0 @@
Briar یک برنامه پیام رسان می باشد که برای فعالان، روزنامه نگاران و هر کسی که نیازمند یک راه امن، راحت و پیشرفته برای ارتباط با دیگران است می باشد. برخلاف برنامه‌ های پیام‌رسان‌ مرسوم، Briar به سرور متمرکز اتکا ندارد - پیام ها به صورت مستقیم بین دستگاه کاربران همگام می شود. اگر اینترنت کار نکند، Briar می‌تواند از طریق بلوتوث یا وای‌-فای همگام سازی کرده، جریان اطلاعات را در زمان بحران نگه دارد. اگر اینترنت کار کند، Briar می‌تواند برای محافظت کاربران و وابط آن ها از از شنود، از طریق شبکه تور همگام سازی کند.
این برنامه امکان ارسال پیام خصوصی، گروهی، تالارهای گفتمان و وبلاگ را در اختیار شما می‌گذارد. در این برنامه از شبکه‌ی تور پشتیبانی می‌شود. هر آنچه در برایر (Briar) انجام می‌دهید، تنها در دستگاه شما ذخیره می‌شود مگر اینکه خودتان آن را با دیگران به اشتراک بگذارید.
این برنامه هیچ تبلیغ و ردیابی از کاربران خود انجام نمی‌دهد. کد منبع برنامه به صورت کاملا باز در اختیار همه قرار دارد و کاملا حرفه‌ای بررسی شده است. تمامی نسخه‌های جدید برایر (Briar) قابل تولید مجدد هستند که امکان تائید یکسان بودن برنامه منتشر شده با کد منبع را می‌دهد. توسعه این برنامه توسط یک تیم غیرانتفاعی کوچک صورت می‌پذیرد.

View File

@@ -1 +0,0 @@
پیام‌رسان امن، در هر کجا

View File

@@ -1 +0,0 @@
Briar

View File

@@ -1 +0,0 @@
https://www.youtube.com/watch?v=ogDywPyPrB4

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Mensaxería segura, en todo lugar.

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Saugus susirašinėjimas, bet kur.

View File

@@ -1 +0,0 @@
Briar

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
Güvenli mesajlaşma, nerede olursa olsun.

View File

@@ -1,5 +0,0 @@
Briar 是一款为活动人士、记者和其他需要安全、轻松和强大的通信方式的人设计的消息应用。与传统的消息应用不同Briar 不依赖中央服务器——消息在用户设备之间直接同步。即便互联网中断Briar 也可以通过蓝牙或 Wi-Fi 进行同步在危机中保持信息流通。如果互联网连接正常Briar 可以通过 Tor 网络进行同步,从而保护用户及其关系不受监视。
这款应用的功能包括私人信息、群组、论坛和博客。应用程序内置了对Tor网络的支持。你在Briar中所做的一切都只存储在你的设备上除非你决定与其他用户共享它。
没有广告也没有跟踪。该应用程序的源代码是完全开放的任何人都可以检查并且已经经过专业审计。Briar的所有版本都是可复制的这使得验证发布的源代码与这里发布的应用程序完全匹配成为可能。开发是由一个小型非营利团队完成的。

View File

@@ -1 +0,0 @@
安全收发信息,不论何处

View File

@@ -1,18 +0,0 @@
#!/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

View File

@@ -19,9 +19,4 @@ public class BriarTestComponentApplication extends BriarApplicationImpl {
return component; return component;
} }
@Override
public boolean isInstrumentationTest() {
return true;
}
} }

View File

@@ -1,65 +0,0 @@
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();
}
}
}

View File

@@ -6,25 +6,19 @@ 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";
@@ -33,8 +27,6 @@ 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();
@@ -43,18 +35,26 @@ 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,13 +62,16 @@ public abstract class UiTest {
super.beforeActivityLaunched(); super.beforeActivityLaunched();
accountManager.deleteAccount(); accountManager.deleteAccount();
accountManager.createAccount(USERNAME, PASSWORD); accountManager.createAccount(USERNAME, PASSWORD);
Intent serviceIntent = if (runnable != null) {
new Intent(getApplicationContext(), BriarService.class); Intent serviceIntent =
getApplicationContext().startService(serviceIntent); new Intent(getApplicationContext(), BriarService.class);
try { getApplicationContext().startService(serviceIntent);
lifecycleManager.waitForStartup(); try {
} catch (InterruptedException e) { lifecycleManager.waitForStartup();
throw new AssertionError(e); } catch (InterruptedException e) {
throw new AssertionError(e);
}
runnable.run();
} }
} }
} }

View File

@@ -1,7 +1,6 @@
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;
@@ -15,13 +14,9 @@ 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;
@@ -30,26 +25,13 @@ 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(timeout) { return new CustomViewAction() {
@Override @Override
protected boolean exitConditionTrue(View view) { protected boolean exitConditionTrue(View view) {
for (View child : breadthFirstViewTraversal(view)) { for (View child : breadthFirstViewTraversal(view)) {
@@ -66,62 +48,24 @@ public class ViewActions {
}; };
} }
public static ViewAction waitForActivity(Class<? extends Activity> clazz, public static ViewAction waitForActivity(Activity activity, Stage stage) {
Stage stage, long timeout) { return new CustomViewAction() {
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();
log(lifecycleMonitor); return lifecycleMonitor.getLifecycleStageOf(activity) == stage;
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 " + clazz.getName() + " in stage " + return "Wait for activity " + activity.getClass().getName() +
stage.name() + " within " + timeout + " to resume within " + TIMEOUT_MS + " milliseconds.";
" 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();
@@ -130,7 +74,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; long endTime = currentTimeMillis() + TIMEOUT_MS;
do { do {
if (exitConditionTrue(view)) return; if (exitConditionTrue(view)) return;
uiController.loopMainThreadForAtLeast(WAIT_MS); uiController.loopMainThreadForAtLeast(WAIT_MS);

View File

@@ -5,8 +5,6 @@ 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.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;
@@ -30,8 +28,4 @@ public interface BriarUiTestComponent extends AndroidComponent {
void inject(NavDrawerActivityTest test); void inject(NavDrawerActivityTest test);
void inject(SignInTestCreateAccount test);
void inject(SignInTestSignIn test);
} }

View File

@@ -1,65 +0,0 @@
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();
}
}

View File

@@ -1,59 +0,0 @@
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)));
}
}

View File

@@ -10,15 +10,12 @@ 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 {

View File

@@ -12,14 +12,11 @@ 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 {

View File

@@ -0,0 +1,39 @@
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()));
}
}

View File

@@ -33,6 +33,4 @@ public interface BriarUiTestComponent extends AndroidComponent {
void inject(SettingsActivityScreenshotTest test); void inject(SettingsActivityScreenshotTest test);
void inject(PromoVideoTest test);
} }

View File

@@ -1,41 +0,0 @@
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);
}
}

View File

@@ -1,86 +0,0 @@
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);
}
}

View File

@@ -1,285 +0,0 @@
package org.briarproject.briar.android;
import android.view.View;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactState;
import org.briarproject.briar.R;
import org.briarproject.briar.android.account.SetupActivity;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
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 androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static java.lang.Thread.sleep;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.briar.android.OverlayTapViewAction.visualClick;
import static org.briarproject.briar.android.ViewActions.waitFor;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
import static org.hamcrest.CoreMatchers.allOf;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class PromoVideoTest extends ScreenshotTest {
// we can leave isFilming to false (to speed up CI)
// and only set it to true when doing recordings
private static final boolean isFilming = false;
private static final int DELAY_SMALL = isFilming ? 4_000 : 0;
private static final int DELAY_MEDIUM = isFilming ? 7_500 : 0;
private static final int DELAY_LONG = isFilming ? 10_000 : 0;
@Rule
public ActivityScenarioRule<SplashScreenActivity> testRule =
new ActivityScenarioRule<>(SplashScreenActivity.class);
@Inject
protected ContactManager contactManager;
private OverlayView overlayView;
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
accountManager.deleteAccount();
}
@Test
public void createAccountAddContact() throws Throwable {
if (isFilming) {
// Using this breaks emulator CI tests for some reason.
// Only use it for filming for now until we have time to debug this.
overlayView = OverlayView.attach(getApplicationContext());
}
// Splash screen shows logo
onView(withId(R.id.logoView))
.perform(waitUntilMatches(isDisplayed()));
// It takes a long time for SetupActivity to start after the splash,
// (because it is shown longer for videos), so increase timeout.
if (!isFilming) waitFor(SetupActivity.class, 30_000);
// Note: We use waiting code only when not filming,
// to make the test reliable for CI. Otherwise, we used fixed
// delays to deterministically align with subtitles.
sleep(DELAY_LONG);
// Enter username
onView(withText(R.string.setup_title))
.perform(waitUntilMatches(isDisplayed()));
sleep(DELAY_SMALL);
onView(withId(R.id.nickname_entry))
.check(matches(isDisplayed()))
.perform(replaceText(USERNAME));
closeKeyboard(withId(R.id.nickname_entry));
sleep(DELAY_SMALL);
doClick(withId(R.id.next));
sleep(DELAY_MEDIUM);
// Enter password
doClick(withId(R.id.password_entry), 1000);
onView(withId(R.id.password_entry))
.check(matches(isDisplayed()))
.perform(replaceText(PASSWORD));
sleep(DELAY_SMALL);
doClick(withId(R.id.password_confirm), 1000);
onView(withId(R.id.password_confirm))
.check(matches(isDisplayed()))
.perform(replaceText(PASSWORD));
sleep(DELAY_SMALL);
// click next or create account
doClick(withId(R.id.next));
sleep(DELAY_SMALL);
// White-list Doze if needed
if (needsDozeWhitelisting(getApplicationContext())) {
doClick(withText(R.string.setup_doze_button));
UiDevice device = UiDevice.getInstance(getInstrumentation());
UiObject allowButton = device.findObject(
new UiSelector().className("android.widget.Button")
.index(1));
allowButton.click();
doClick(withId(R.id.next));
}
lifecycleManager.waitForStartup();
assertTrue(accountManager.hasDatabaseKey());
sleep(DELAY_SMALL);
// wait for contact list to be shown
if (!isFilming) waitFor(NavDrawerActivity.class);
// clicking the FAB doesn't work, so we click its inner FAB as well
onView(withId(R.id.speedDial))
.check(matches(isDisplayed()))
.perform(click());
doClick(withId(R.id.fab_main)); // this is inside R.id.speedDial
sleep(DELAY_MEDIUM);
// click adding contact at a distance menu item
doClick(withText(R.string.add_contact_remotely_title));
sleep(DELAY_LONG);
// enter briar:// link
String link =
"briar://ab54fpik6sjyetzjhlwto2fv7tspibx2uhpdnei4tdidkvjpbphvy";
doClick(withId(R.id.pasteButton));
onView(withId(R.id.linkInput))
.perform(waitUntilMatches(isDisplayed()))
.perform(replaceText(link));
sleep(DELAY_MEDIUM);
doClick(withId(R.id.addButton));
sleep(DELAY_MEDIUM);
// enter contact alias
String contactName = getApplicationContext()
.getString(R.string.screenshot_bob);
doClick(withId(R.id.contactNameInput), 1000);
onView(withId(R.id.contactNameInput))
.perform(waitUntilMatches(isDisplayed()))
.perform(replaceText(contactName));
sleep(DELAY_SMALL);
closeKeyboard(withId(R.id.contactNameInput));
sleep(DELAY_SMALL);
// add pending contact
onView(withId(R.id.addButton)).perform(scrollTo());
doClick(withId(R.id.addButton));
sleep(DELAY_LONG);
// wait for pending contact list activity to be shown
if (!isFilming) {
waitFor(PendingContactListActivity.class);
waitFor(allOf(withText(R.string.pending_contact_requests),
isDisplayed()));
}
// remove pending contact
for (Pair<PendingContact, PendingContactState> p : contactManager
.getPendingContacts()) {
contactManager.removePendingContact(p.getFirst().getId());
}
// add contact and make them appear online
Contact bob = testDataCreator.addContact(contactName, false, true);
sleep(DELAY_SMALL);
connectionRegistry.registerIncomingConnection(bob.getId(), ID, () -> {
});
sleep(DELAY_LONG);
// wait for contact list to be shown
if (!isFilming) {
waitFor(NavDrawerActivity.class);
waitFor(allOf(withText(R.string.contact_list_button),
isDisplayed()));
waitFor(allOf(withId(R.id.recyclerView), isDisplayed()));
}
// click on new contact
doItemClick(withId(R.id.recyclerView), 0);
sleep(DELAY_MEDIUM);
// bring up keyboard
doClick(withId(R.id.input_text), DELAY_SMALL);
String msg1 = getApplicationContext()
.getString(R.string.screenshot_message_1);
onView(withId(R.id.input_text))
.perform(waitUntilMatches(isEnabled()))
.perform(replaceText(msg1));
sleep(DELAY_SMALL);
doClick(withId(R.id.compositeSendButton));
sleep(DELAY_SMALL);
// send emoji
doClick(withId(R.id.emoji_toggle), DELAY_SMALL);
onView(withId(R.id.input_text))
.perform(replaceText("\uD83D\uDE0E"));
sleep(DELAY_SMALL);
doClick(withId(R.id.compositeSendButton));
// close keyboard
closeKeyboard(withId(R.id.compositeSendButton));
sleep(DELAY_LONG);
}
private void doClick(final Matcher<View> viewMatcher, long sleepMs)
throws InterruptedException {
doClick(viewMatcher);
if (isFilming) sleep(sleepMs);
}
private void doClick(final Matcher<View> viewMatcher)
throws InterruptedException {
if (isFilming) {
onView(viewMatcher)
.perform(waitUntilMatches(isDisplayed()))
.perform(visualClick(overlayView));
sleep(500);
}
onView(viewMatcher)
.perform(waitUntilMatches(allOf(isDisplayed(), isEnabled())))
.perform(click());
}
private void doItemClick(final Matcher<View> viewMatcher, int pos)
throws InterruptedException {
if (isFilming) {
onView(viewMatcher).perform(
actionOnItemAtPosition(pos, visualClick(overlayView)));
sleep(500);
}
onView(viewMatcher).perform(
actionOnItemAtPosition(pos, click()));
}
private void closeKeyboard(final Matcher<View> viewMatcher)
throws InterruptedException {
if (isFilming) sleep(750);
onView(viewMatcher).perform(closeSoftKeyboard());
}
}

View File

@@ -3,8 +3,6 @@ package org.briarproject.briar.android;
import android.app.Activity; import android.app.Activity;
import android.util.Log; import android.util.Log;
import com.jraska.falcon.Falcon.UnableToTakeScreenshotException;
import org.briarproject.bramble.api.connection.ConnectionRegistry; import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.api.test.TestDataCreator; import org.briarproject.briar.api.test.TestDataCreator;
@@ -12,7 +10,6 @@ import org.junit.ClassRule;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import tools.fastlane.screengrab.FalconScreenshotStrategy; import tools.fastlane.screengrab.FalconScreenshotStrategy;
import tools.fastlane.screengrab.Screengrab; import tools.fastlane.screengrab.Screengrab;
import tools.fastlane.screengrab.locale.LocaleTestRule; import tools.fastlane.screengrab.locale.LocaleTestRule;
@@ -29,15 +26,9 @@ public abstract class ScreenshotTest extends UiTest {
@Inject @Inject
protected Clock clock; protected Clock clock;
protected void screenshot(String name, ActivityScenarioRule<?> rule) {
rule.getScenario().onActivity(activity -> screenshot(name, activity));
}
protected void screenshot(String name, Activity activity) { protected void screenshot(String name, Activity activity) {
try { try {
Screengrab.screenshot(name, new FalconScreenshotStrategy(activity)); Screengrab.screenshot(name, new FalconScreenshotStrategy(activity));
} catch (UnableToTakeScreenshotException e) {
Log.e("Screengrab", "Error taking screenshot", e);
} catch (RuntimeException e) { } catch (RuntimeException e) {
if (e.getMessage() == null || if (e.getMessage() == null ||
!e.getMessage().equals("Unable to capture screenshot.")) !e.getMessage().equals("Unable to capture screenshot."))

View File

@@ -10,7 +10,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject; import androidx.test.uiautomator.UiObject;
@@ -22,27 +22,30 @@ import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 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 androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID; import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches; import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting; import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
import static org.hamcrest.Matchers.allOf;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SetupDataTest extends ScreenshotTest { public class SetupDataTest extends ScreenshotTest {
@Rule @Rule
public ActivityScenarioRule<SetupActivity> testRule = public IntentsTestRule<SetupActivity> testRule =
new ActivityScenarioRule<>(SetupActivity.class); new IntentsTestRule<SetupActivity>(SetupActivity.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
accountManager.deleteAccount();
}
};
@Override @Override
protected void inject(BriarUiTestComponent component) { protected void inject(BriarUiTestComponent component) {
component.inject(this); component.inject(this);
accountManager.deleteAccount();
} }
@Test @Test
@@ -56,7 +59,7 @@ public class SetupDataTest extends ScreenshotTest {
onView(withId(R.id.nickname_entry)) onView(withId(R.id.nickname_entry))
.perform(waitUntilMatches(withText(USERNAME))); .perform(waitUntilMatches(withText(USERNAME)));
screenshot("manual_create_account", testRule); screenshot("manual_create_account", testRule.getActivity());
onView(withId(R.id.next)) onView(withId(R.id.next))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
@@ -70,7 +73,7 @@ public class SetupDataTest extends ScreenshotTest {
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
.perform(typeText(PASSWORD)); .perform(typeText(PASSWORD));
onView(withId(R.id.next)) onView(withId(R.id.next))
.check(matches(allOf(isDisplayed(), isEnabled()))) .check(matches(isDisplayed()))
.perform(click()); .perform(click());
// White-list Doze if needed // White-list Doze if needed
@@ -91,6 +94,14 @@ public class SetupDataTest extends ScreenshotTest {
lifecycleManager.waitForStartup(); lifecycleManager.waitForStartup();
assertTrue(accountManager.hasDatabaseKey()); assertTrue(accountManager.hasDatabaseKey());
createTestData(); createTestData();
// close expiry warning
onView(withId(R.id.expiryWarning))
.perform(waitUntilMatches(isDisplayed()));
onView(withId(R.id.expiryWarningClose))
.check(matches(isDisplayed()));
onView(withId(R.id.expiryWarningClose))
.perform(click());
} }
private void createTestData() { private void createTestData() {
@@ -105,7 +116,7 @@ public class SetupDataTest extends ScreenshotTest {
throws DbException { throws DbException {
Context ctx = getApplicationContext(); Context ctx = getApplicationContext();
String bobName = ctx.getString(R.string.screenshot_bob); String bobName = ctx.getString(R.string.screenshot_bob);
Contact bob = testDataCreator.addContact(bobName, false, true); Contact bob = testDataCreator.addContact(bobName, true);
// TODO add messages // TODO add messages

View File

@@ -11,29 +11,32 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.contrib.DrawerActions; import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed; import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
import static androidx.test.espresso.contrib.RecyclerViewActions.scrollTo;
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.isEnabled; import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withChild; import static androidx.test.espresso.matcher.ViewMatchers.withChild;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches; import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock; import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assume.assumeTrue;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SettingsActivityScreenshotTest extends ScreenshotTest { public class SettingsActivityScreenshotTest extends ScreenshotTest {
@Rule @Rule
public CleanAccountTestRule<SettingsActivity> testRule = public ActivityTestRule<SettingsActivity> testRule =
new CleanAccountTestRule<>(SettingsActivity.class); new ActivityTestRule<>(SettingsActivity.class);
@Override @Override
protected void inject(BriarUiTestComponent component) { protected void inject(BriarUiTestComponent component) {
@@ -45,10 +48,6 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
onView(withText(R.string.settings_button)) onView(withText(R.string.settings_button))
.check(matches(isDisplayed())); .check(matches(isDisplayed()));
onView(withText(R.string.display_settings_title))
.perform(waitUntilMatches(isDisplayed()))
.perform(click(), click());
screenshot("manual_dark_theme_settings", testRule.getActivity()); screenshot("manual_dark_theme_settings", testRule.getActivity());
// switch to dark theme // switch to dark theme
@@ -67,9 +66,6 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
onView(withText(R.string.settings_button)) onView(withText(R.string.settings_button))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
.perform(click()); .perform(click());
onView(withText(R.string.display_settings_title))
.check(matches(isDisplayed()))
.perform(click());
onView(withText(R.string.pref_theme_title)) onView(withText(R.string.pref_theme_title))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
.perform(click()); .perform(click());
@@ -80,17 +76,20 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
@Test @Test
public void appLock() { public void appLock() {
assumeTrue("device has no screen lock", // scroll down
hasScreenLock(getApplicationContext())); onView(withClassName(is(RecyclerView.class.getName())))
.perform(scrollTo(hasDescendant(
// scroll down a bit more to have settings in the middle
withText(R.string.panic_setting))));
onView(withText(R.string.security_settings_title)) // wait for settings to get loaded and enabled
.perform(waitUntilMatches(isDisplayed())) onView(withText(R.string.tor_mobile_data_title))
.perform(click(), click()); .perform(waitUntilMatches(isEnabled()));
// ensure app lock is displayed and enabled // ensure app lock is displayed and enabled
onView(withText(R.string.pref_lock_title)) onView(withText(R.string.pref_lock_title))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
.perform(waitUntilMatches(isEnabled())) .check(matches(isEnabled()))
.perform(click()); .perform(click());
onView(withChild(withText(R.string.pref_lock_timeout_title))) onView(withChild(withText(R.string.pref_lock_timeout_title)))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
@@ -105,10 +104,11 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
@Test @Test
public void torSettings() { public void torSettings() {
// click network/connections settings // scroll down
onView(withText(R.string.network_settings_title)) onView(withClassName(is(RecyclerView.class.getName())))
.perform(waitUntilMatches(isDisplayed())) .perform(scrollTo(hasDescendant(
.perform(click(), click()); // scroll down a bit more to have settings in the middle
withText(R.string.pref_lock_timeout_title))));
// wait for settings to get loaded and enabled // wait for settings to get loaded and enabled
onView(withText(R.string.tor_network_setting)) onView(withText(R.string.tor_network_setting))

View File

@@ -27,10 +27,7 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"
tools:ignore="ScopedStorage" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
@@ -41,7 +38,6 @@
android:name="org.briarproject.briar.android.BriarApplicationImpl" android:name="org.briarproject.briar.android.BriarApplicationImpl"
android:allowBackup="false" android:allowBackup="false"
android:banner="@mipmap/tv_banner" android:banner="@mipmap/tv_banner"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher_round" android:icon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"
android:logo="@mipmap/ic_launcher_round" android:logo="@mipmap/ic_launcher_round"
@@ -104,7 +100,8 @@
<activity <activity
android:name="org.briarproject.briar.android.account.SetupActivity" android:name="org.briarproject.briar.android.account.SetupActivity"
android:label="@string/setup_title" /> android:label="@string/setup_title"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" />
<activity <activity
android:name="org.briarproject.briar.android.splash.SplashScreenActivity" android:name="org.briarproject.briar.android.splash.SplashScreenActivity"
@@ -322,7 +319,26 @@
</activity> </activity>
<activity <activity
android:name="org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity" android:name="org.briarproject.briar.android.blog.RssFeedImportActivity"
android:label="@string/blogs_rss_feeds_import"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" />
</activity>
<activity
android:name="org.briarproject.briar.android.blog.RssFeedManageActivity"
android:label="@string/blogs_rss_feeds_manage"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" />
</activity>
<activity
android:name="org.briarproject.briar.android.keyagreement.ContactExchangeActivity"
android:label="@string/add_contact_title" android:label="@string/add_contact_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"> android:theme="@style/BriarTheme.NoActionBar">
@@ -427,15 +443,6 @@
android:theme="@style/BriarTheme" android:theme="@style/BriarTheme"
android:windowSoftInputMode="adjustResize|stateHidden" /> android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name="org.briarproject.briar.android.blog.RssFeedActivity"
android:label="@string/blogs_rss_feeds"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" />
</activity>
<activity <activity
android:name=".android.contact.add.remote.PendingContactListActivity" android:name=".android.contact.add.remote.PendingContactListActivity"
android:label="@string/pending_contact_requests" android:label="@string/pending_contact_requests"

View File

@@ -37,10 +37,6 @@ import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader; import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.logging.CachingLogHandler; import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.settings.ConnectionsFragment;
import org.briarproject.briar.android.settings.NotificationsFragment;
import org.briarproject.briar.android.settings.SecurityFragment;
import org.briarproject.briar.android.settings.SettingsFragment;
import org.briarproject.briar.android.view.EmojiTextInputView; import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog; import org.briarproject.briar.api.android.DozeWatchdog;
@@ -202,12 +198,4 @@ public interface AndroidComponent
void inject(EmojiTextInputView textInputView); void inject(EmojiTextInputView textInputView);
void inject(BriarModelLoader briarModelLoader); void inject(BriarModelLoader briarModelLoader);
void inject(SettingsFragment settingsFragment);
void inject(ConnectionsFragment connectionsFragment);
void inject(SecurityFragment securityFragment);
void inject(NotificationsFragment notificationsFragment);
} }

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.app.Application; import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.StrictMode; import android.os.StrictMode;
@@ -31,11 +30,9 @@ import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.DozeHelperModule; import org.briarproject.briar.android.account.DozeHelperModule;
import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule; import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.contact.ContactListModule; import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule;
import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.introduction.IntroductionModule; import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.logging.LoggingModule; import org.briarproject.briar.android.logging.LoggingModule;
import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule;
@@ -77,7 +74,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@Module(includes = { @Module(includes = {
SetupModule.class, SetupModule.class,
DozeHelperModule.class, DozeHelperModule.class,
AddNearbyContactModule.class, ContactExchangeModule.class,
LoggingModule.class, LoggingModule.class,
LoginModule.class, LoginModule.class,
NavDrawerModule.class, NavDrawerModule.class,
@@ -85,9 +82,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
SettingsModule.class, SettingsModule.class,
DevReportModule.class, DevReportModule.class,
ContactListModule.class, ContactListModule.class,
IntroductionModule.class,
// below need to be within same scope as ViewModelProvider.Factory // below need to be within same scope as ViewModelProvider.Factory
BlogModule.class,
ForumModule.class, ForumModule.class,
GroupListModule.class, GroupListModule.class,
GroupConversationModule.class, GroupConversationModule.class,
@@ -116,11 +111,6 @@ public class AppModule {
this.application = application; this.application = application;
} }
public static AndroidComponent getAndroidComponent(Context ctx) {
BriarApplication app = (BriarApplication) ctx.getApplicationContext();
return app.getApplicationComponent();
}
@Provides @Provides
@Singleton @Singleton
Application providesApplication() { Application providesApplication() {
@@ -286,21 +276,16 @@ public class AppModule {
@Override @Override
public boolean shouldEnableImageAttachments() { public boolean shouldEnableImageAttachments() {
return true; return IS_DEBUG_BUILD;
} }
@Override @Override
public boolean shouldEnableProfilePictures() { public boolean shouldEnableProfilePictures() {
return true; return IS_DEBUG_BUILD;
} }
@Override @Override
public boolean shouldEnableDisappearingMessages() { public boolean shouldEnableDisappearingMessages() {
return true;
}
@Override
public boolean shouldEnableConnectViaBluetooth() {
return IS_DEBUG_BUILD; return IS_DEBUG_BUILD;
} }
}; };

View File

@@ -19,6 +19,4 @@ public interface BriarApplication extends BrambleApplication {
SharedPreferences getDefaultSharedPreferences(); SharedPreferences getDefaultSharedPreferences();
boolean isRunningInBackground(); boolean isRunningInBackground();
boolean isInstrumentationTest();
} }

View File

@@ -151,9 +151,4 @@ public class BriarApplicationImpl extends Application
ActivityManager.getMyMemoryState(info); ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND); return (info.importance != IMPORTANCE_FOREGROUND);
} }
@Override
public boolean isInstrumentationTest() {
return false;
}
} }

View File

@@ -301,9 +301,7 @@ public class BriarService extends Service {
LOG.info("Interrupted while waiting for shutdown"); LOG.info("Interrupted while waiting for shutdown");
} }
LOG.info("Exiting"); LOG.info("Exiting");
if (!app.isInstrumentationTest()) { System.exit(0);
System.exit(0);
}
}, "BackgroundShutdown"); }, "BackgroundShutdown");
}, "BackgroundShutdown"); }, "BackgroundShutdown");
} }

View File

@@ -12,7 +12,7 @@ import java.util.Locale;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.settings.DisplayFragment.PREF_LANGUAGE; import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
@NotNullByDefault @NotNullByDefault
public class Localizer { public class Localizer {
@@ -25,7 +25,7 @@ public class Localizer {
private Localizer(SharedPreferences sharedPreferences) { private Localizer(SharedPreferences sharedPreferences) {
this(Locale.getDefault(), getLocaleFromTag( this(Locale.getDefault(), getLocaleFromTag(
sharedPreferences.getString(PREF_LANGUAGE, "default"))); sharedPreferences.getString(LANGUAGE, "default")));
} }
private Localizer(Locale systemLocale, @Nullable Locale userLocale) { private Localizer(Locale systemLocale, @Nullable Locale userLocale) {

View File

@@ -13,6 +13,7 @@ import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -34,6 +35,11 @@ public class AuthorNameFragment extends SetupFragment {
return new AuthorNameFragment(); return new AuthorNameFragment();
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,

View File

@@ -13,6 +13,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.account.PowerView.OnCheckedChangedListener; import org.briarproject.briar.android.account.PowerView.OnCheckedChangedListener;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -30,16 +31,20 @@ public class DozeFragment extends SetupFragment
private final static String TAG = DozeFragment.class.getName(); private final static String TAG = DozeFragment.class.getName();
private DozeView dozeView; private DozeView dozeView;
private HuaweiProtectedAppsView huaweiProtectedAppsView; private HuaweiView huaweiView;
private HuaweiAppLaunchView huaweiAppLaunchView;
private XiaomiView xiaomiView;
private Button next; private Button next;
private ProgressBar progressBar;
private boolean secondAttempt = false; private boolean secondAttempt = false;
public static DozeFragment newInstance() { public static DozeFragment newInstance() {
return new DozeFragment(); return new DozeFragment();
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@@ -50,26 +55,14 @@ public class DozeFragment extends SetupFragment
false); false);
dozeView = v.findViewById(R.id.dozeView); dozeView = v.findViewById(R.id.dozeView);
dozeView.setOnCheckedChangedListener(this); dozeView.setOnCheckedChangedListener(this);
huaweiProtectedAppsView = v.findViewById(R.id.huaweiProtectedAppsView); huaweiView = v.findViewById(R.id.huaweiView);
huaweiProtectedAppsView.setOnCheckedChangedListener(this); huaweiView.setOnCheckedChangedListener(this);
huaweiAppLaunchView = v.findViewById(R.id.huaweiAppLaunchView);
huaweiAppLaunchView.setOnCheckedChangedListener(this);
xiaomiView = v.findViewById(R.id.xiaomiView);
xiaomiView.setOnCheckedChangedListener(this);
next = v.findViewById(R.id.next); next = v.findViewById(R.id.next);
ProgressBar progressBar = v.findViewById(R.id.progress); progressBar = v.findViewById(R.id.progress);
dozeView.setOnButtonClickListener(this::askForDozeWhitelisting); dozeView.setOnButtonClickListener(this::askForDozeWhitelisting);
next.setOnClickListener(this); next.setOnClickListener(this);
viewModel.getIsCreatingAccount()
.observe(getViewLifecycleOwner(), isCreatingAccount -> {
if (isCreatingAccount) {
next.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
}
});
return v; return v;
} }
@@ -99,10 +92,7 @@ public class DozeFragment extends SetupFragment
@Override @Override
public void onCheckedChanged() { public void onCheckedChanged() {
next.setEnabled(dozeView.isChecked() && next.setEnabled(dozeView.isChecked() && huaweiView.isChecked());
huaweiProtectedAppsView.isChecked() &&
huaweiAppLaunchView.isChecked() &&
xiaomiView.isChecked());
} }
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
@@ -114,6 +104,15 @@ public class DozeFragment extends SetupFragment
@Override @Override
public void onClick(View view) { public void onClick(View view) {
setNextClicked();
viewModel.dozeExceptionConfirmed(); viewModel.dozeExceptionConfirmed();
} }
@Override
void setNextClicked() {
super.setNextClicked();
next.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
}
} }

View File

@@ -2,15 +2,13 @@ package org.briarproject.briar.android.account;
import android.content.Context; import android.content.Context;
import static org.briarproject.briar.android.account.HuaweiView.needsToBeShown;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting; import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
class DozeHelperImpl implements DozeHelper { class DozeHelperImpl implements DozeHelper {
@Override @Override
public boolean needToShowDozeFragment(Context context) { public boolean needToShowDozeFragment(Context context) {
Context appContext = context.getApplicationContext(); return needsDozeWhitelisting(context.getApplicationContext()) ||
return needsDozeWhitelisting(appContext) || needsToBeShown(context.getApplicationContext());
HuaweiProtectedAppsView.needsToBeShown(appContext) ||
HuaweiAppLaunchView.needsToBeShown(appContext) ||
XiaomiView.isXiaomiOrRedmiDevice();
} }
} }

View File

@@ -1,78 +0,0 @@
package org.briarproject.briar.android.account;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.util.AttributeSet;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import java.util.List;
import javax.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.os.Build.VERSION.SDK_INT;
@UiThread
@NotNullByDefault
class HuaweiAppLaunchView extends PowerView {
private final static String PACKAGE_NAME = "com.huawei.systemmanager";
private final static String CLASS_NAME =
PACKAGE_NAME + ".power.ui.HwPowerManagerActivity";
public HuaweiAppLaunchView(Context context) {
this(context, null);
}
public HuaweiAppLaunchView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HuaweiAppLaunchView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
setText(R.string.setup_huawei_app_launch_text);
setButtonText(R.string.setup_huawei_app_launch_button);
}
@Override
public boolean needsToBeShown() {
return needsToBeShown(getContext());
}
public static boolean needsToBeShown(Context context) {
// "App launch" was introduced in EMUI 8 (Android 8.0)
if (SDK_INT < 26) return false;
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(),
MATCH_DEFAULT_ONLY);
return !resolveInfos.isEmpty();
}
@Override
@StringRes
protected int getHelpText() {
return R.string.setup_huawei_app_launch_help;
}
@Override
protected void onButtonClick() {
getContext().startActivity(getIntent());
setChecked(true);
}
private static Intent getIntent() {
Intent intent = new Intent();
intent.setClassName(PACKAGE_NAME, CLASS_NAME);
return intent;
}
}

View File

@@ -17,28 +17,25 @@ import javax.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
class HuaweiProtectedAppsView extends PowerView { class HuaweiView extends PowerView {
private final static String PACKAGE_NAME = "com.huawei.systemmanager"; private final static String PACKAGE_NAME = "com.huawei.systemmanager";
private final static String CLASS_NAME = private final static String CLASS_NAME =
PACKAGE_NAME + ".optimize.process.ProtectActivity"; PACKAGE_NAME + ".optimize.process.ProtectActivity";
public HuaweiProtectedAppsView(Context context) { public HuaweiView(Context context) {
this(context, null); this(context, null);
} }
public HuaweiProtectedAppsView(Context context, public HuaweiView(Context context, @Nullable AttributeSet attrs) {
@Nullable AttributeSet attrs) {
this(context, attrs, 0); this(context, attrs, 0);
} }
public HuaweiProtectedAppsView(Context context, public HuaweiView(Context context, @Nullable AttributeSet attrs,
@Nullable AttributeSet attrs,
int defStyleAttr) { int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
setText(R.string.setup_huawei_text); setText(R.string.setup_huawei_text);
@@ -55,7 +52,7 @@ class HuaweiProtectedAppsView extends PowerView {
if (SDK_INT >= 24) return false; if (SDK_INT >= 24) return false;
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(), List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(),
MATCH_DEFAULT_ONLY); PackageManager.MATCH_DEFAULT_ONLY);
return !resolveInfos.isEmpty(); return !resolveInfos.isEmpty();
} }

View File

@@ -40,8 +40,8 @@ import static android.os.SystemClock.elapsedRealtime;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK; import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK_TIMEOUT; import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock; import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;

View File

@@ -43,6 +43,7 @@ abstract class PowerView extends ConstraintLayout {
this(context, attrs, 0); this(context, attrs, 0);
} }
@SuppressWarnings("ConstantConditions")
public PowerView(Context context, @Nullable AttributeSet attrs, public PowerView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) { int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
@@ -84,7 +85,6 @@ abstract class PowerView extends ConstraintLayout {
setChecked(ss.value[0]); // also calls listener setChecked(ss.value[0]); // also calls listener
} }
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public abstract boolean needsToBeShown(); public abstract boolean needsToBeShown();
public void setChecked(boolean checked) { public void setChecked(boolean checked) {
@@ -146,12 +146,10 @@ abstract class PowerView extends ConstraintLayout {
static final Parcelable.Creator<SavedState> CREATOR static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() { = new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) { public SavedState createFromParcel(Parcel in) {
return new SavedState(in); return new SavedState(in);
} }
@Override
public SavedState[] newArray(int size) { public SavedState[] newArray(int size) {
return new SavedState[size]; return new SavedState[size];
} }

View File

@@ -15,6 +15,7 @@ import com.google.android.material.textfield.TextInputLayout;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.login.StrengthMeter; import org.briarproject.briar.android.login.StrengthMeter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -37,11 +38,17 @@ public class SetPasswordFragment extends SetupFragment {
private TextInputEditText passwordConfirmation; private TextInputEditText passwordConfirmation;
private StrengthMeter strengthMeter; private StrengthMeter strengthMeter;
private Button nextButton; private Button nextButton;
private ProgressBar progressBar;
public static SetPasswordFragment newInstance() { public static SetPasswordFragment newInstance() {
return new SetPasswordFragment(); return new SetPasswordFragment();
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@@ -57,7 +64,7 @@ public class SetPasswordFragment extends SetupFragment {
v.findViewById(R.id.password_confirm_wrapper); v.findViewById(R.id.password_confirm_wrapper);
passwordConfirmation = v.findViewById(R.id.password_confirm); passwordConfirmation = v.findViewById(R.id.password_confirm);
nextButton = v.findViewById(R.id.next); nextButton = v.findViewById(R.id.next);
ProgressBar progressBar = v.findViewById(R.id.progress); progressBar = v.findViewById(R.id.progress);
passwordEntry.addTextChangedListener(this); passwordEntry.addTextChangedListener(this);
passwordConfirmation.addTextChangedListener(this); passwordConfirmation.addTextChangedListener(this);
@@ -68,17 +75,6 @@ public class SetPasswordFragment extends SetupFragment {
passwordConfirmation.setImeOptions(IME_ACTION_DONE); passwordConfirmation.setImeOptions(IME_ACTION_DONE);
} }
viewModel.getIsCreatingAccount()
.observe(getViewLifecycleOwner(), isCreatingAccount -> {
if (isCreatingAccount) {
nextButton.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
// this also avoids the keyboard popping up
passwordEntry.setFocusable(false);
passwordConfirmation.setFocusable(false);
}
});
return v; return v;
} }
@@ -118,8 +114,22 @@ public class SetPasswordFragment extends SetupFragment {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
IBinder token = passwordEntry.getWindowToken(); IBinder token = passwordEntry.getWindowToken();
Object o = requireContext().getSystemService(INPUT_METHOD_SERVICE); Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0); ((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
setNextClicked();
viewModel.setPassword(passwordEntry.getText().toString()); viewModel.setPassword(passwordEntry.getText().toString());
} }
@Override
void setNextClicked() {
super.setNextClicked();
passwordEntry.setFocusable(false);
passwordConfirmation.setFocusable(false);
if (!viewModel.needToShowDozeFragment()) {
nextButton.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
}
}
} }

View File

@@ -26,8 +26,6 @@ import static org.briarproject.briar.android.account.SetupViewModel.State.CREATE
import static org.briarproject.briar.android.account.SetupViewModel.State.DOZE; import static org.briarproject.briar.android.account.SetupViewModel.State.DOZE;
import static org.briarproject.briar.android.account.SetupViewModel.State.FAILED; import static org.briarproject.briar.android.account.SetupViewModel.State.FAILED;
import static org.briarproject.briar.android.account.SetupViewModel.State.SET_PASSWORD; import static org.briarproject.briar.android.account.SetupViewModel.State.SET_PASSWORD;
import static org.briarproject.briar.android.util.UiUtils.setInputStateAlwaysVisible;
import static org.briarproject.briar.android.util.UiUtils.setInputStateHidden;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -57,13 +55,10 @@ public class SetupActivity extends BaseActivity
private void onStateChanged(SetupViewModel.State state) { private void onStateChanged(SetupViewModel.State state) {
if (state == AUTHOR_NAME) { if (state == AUTHOR_NAME) {
setInputStateAlwaysVisible(this);
showInitialFragment(AuthorNameFragment.newInstance()); showInitialFragment(AuthorNameFragment.newInstance());
} else if (state == SET_PASSWORD) { } else if (state == SET_PASSWORD) {
setInputStateAlwaysVisible(this);
showPasswordFragment(); showPasswordFragment();
} else if (state == DOZE) { } else if (state == DOZE) {
setInputStateHidden(this);
showDozeFragment(); showDozeFragment();
} else if (state == CREATED || state == FAILED) { } else if (state == CREATED || state == FAILED) {
// TODO: Show an error if failed // TODO: Show an error if failed

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