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
358 changed files with 13491 additions and 4676 deletions

View File

@@ -1,52 +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
.base-test: test:
stage: 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
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.notAnnotation=androidx.test.filters.LargeTest
artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths:
- kernel.log
- logcat.txt
expire_in: 3 days
when: on_failure
when: manual
except:
- tags
tags:
- kvm
test_reproducible: test_reproducible:
stage: check_reproducibility stage: check_reproducibility
@@ -62,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

View File

@@ -7,6 +7,7 @@
<w>encrypter</w> <w>encrypter</w>
<w>identicon</w> <w>identicon</w>
<w>introducee</w> <w>introducee</w>
<w>introducees</w>
<w>introducer</w> <w>introducer</w>
<w>onboarding</w> <w>onboarding</w>
</words> </words>

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

@@ -8,15 +8,11 @@ android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.2' buildToolsVersion '30.0.2'
packagingOptions {
doNotStrip '**/*.so'
}
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 10220 versionCode 10216
versionName "1.2.20" versionName "1.2.16"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -175,11 +175,6 @@ class AndroidBluetoothPlugin
} catch (IOException e) { } catch (IOException e) {
IoUtils.tryToClose(s, LOG, WARNING); IoUtils.tryToClose(s, LOG, WARNING);
throw e; throw e;
} catch (NullPointerException e) {
// BluetoothSocket#connect() may throw an NPE under unknown
// circumstances
IoUtils.tryToClose(s, LOG, WARNING);
throw new IOException(e);
} }
} }

View File

@@ -114,8 +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"};
} }
} }

View File

@@ -9,4 +9,5 @@ public interface FeatureFlags {
boolean shouldEnableProfilePictures(); boolean shouldEnableProfilePictures();
boolean shouldEnableDisappearingMessages();
} }

View File

@@ -0,0 +1,29 @@
package org.briarproject.bramble.api.cleanup;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.Collection;
/**
* An interface for registering a hook with the {@link CleanupManager}
* that will be called when a message's cleanup deadline is reached.
*/
@NotNullByDefault
public interface CleanupHook {
/**
* Called when the cleanup deadlines of one or more messages are reached.
* <p>
* The callee is not required to delete the messages, but the hook won't be
* called again for these messages unless another cleanup timer is set (see
* {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)}
* and {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)}).
*/
void deleteMessages(Transaction txn, GroupId g,
Collection<MessageId> messageIds) throws DbException;
}

View File

@@ -0,0 +1,42 @@
package org.briarproject.bramble.api.cleanup;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.MessageId;
/**
* The CleanupManager is responsible for tracking the cleanup deadlines of
* messages and passing them to their respective
* {@link CleanupHook CleanupHooks} when the deadlines are reached.
* <p>
* The CleanupManager responds to
* {@link CleanupTimerStartedEvent CleanupTimerStartedEvents} broadcast by the
* {@link DatabaseComponent}.
* <p>
* See {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)},
* {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)},
* {@link DatabaseComponent#stopCleanupTimer(Transaction, MessageId)}.
*/
@NotNullByDefault
public interface CleanupManager {
/**
* When scheduling a cleanup task we overshoot the deadline by this many
* milliseconds to reduce the number of tasks that need to be scheduled
* when messages have cleanup deadlines that are close together.
*/
long BATCH_DELAY_MS = 1000;
/**
* Registers a hook to be called when messages are due for cleanup.
* This method should be called before
* {@link LifecycleManager#startServices(SecretKey)}.
*/
void registerCleanupHook(ClientId c, int majorVersion,
CleanupHook hook);
}

View File

@@ -0,0 +1,32 @@
package org.briarproject.bramble.api.cleanup.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a message's cleanup timer is started.
*/
@Immutable
@NotNullByDefault
public class CleanupTimerStartedEvent extends Event {
private final MessageId messageId;
private final long cleanupDeadline;
public CleanupTimerStartedEvent(MessageId messageId,
long cleanupDeadline) {
this.messageId = messageId;
this.cleanupDeadline = cleanupDeadline;
}
public MessageId getMessageId() {
return messageId;
}
public long getCleanupDeadline() {
return cleanupDeadline;
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.api.client; package org.briarproject.bramble.api.client;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PrivateKey; import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
@@ -16,6 +17,7 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.Map; import java.util.Map;
@NotNullByDefault @NotNullByDefault
@@ -50,9 +52,11 @@ public interface ClientHelper {
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g) BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
throws DbException, FormatException; throws DbException, FormatException;
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
BdfDictionary query) throws DbException, FormatException;
BdfDictionary getMessageMetadataAsDictionary(MessageId m) BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException, throws DbException, FormatException;
FormatException;
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m) BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
throws DbException, FormatException; throws DbException, FormatException;
@@ -119,4 +123,17 @@ public interface ClientHelper {
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap( Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
BdfDictionary properties) throws FormatException; BdfDictionary properties) throws FormatException;
/**
* Retrieves the contact ID from the group metadata of the given contact
* group.
*/
ContactId getContactId(Transaction txn, GroupId contactGroupId)
throws DbException, FormatException;
/**
* Stores the given contact ID in the group metadata of the given contact
* group.
*/
void setContactId(Transaction txn, GroupId contactGroupId, ContactId c)
throws DbException;
} }

View File

@@ -0,0 +1,9 @@
package org.briarproject.bramble.api.client;
public interface ContactGroupConstants {
/**
* Group metadata key for associating a contact ID with a contact group.
*/
String GROUP_KEY_CONTACT_ID = "contactId";
}

View File

@@ -41,6 +41,18 @@ import javax.annotation.Nullable;
@NotNullByDefault @NotNullByDefault
public interface DatabaseComponent extends TransactionManager { public interface DatabaseComponent extends TransactionManager {
/**
* Return value for {@link #getNextCleanupDeadline(Transaction)} if
* no messages are scheduled to be deleted.
*/
long NO_CLEANUP_DEADLINE = -1;
/**
* Return value for {@link #startCleanupTimer(Transaction, MessageId)}
* if the cleanup timer was not started.
*/
long TIMER_NOT_STARTED = -1;
/** /**
* Opens the database and returns true if the database already existed. * Opens the database and returns true if the database already existed.
* *
@@ -288,6 +300,16 @@ public interface DatabaseComponent extends TransactionManager {
Collection<MessageId> getMessageIds(Transaction txn, GroupId g) Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
throws DbException; throws DbException;
/**
* Returns the IDs of any delivered messages in the given group with
* metadata that matches all entries in the given query. If the query is
* empty, the IDs of all delivered messages are returned.
* <p/>
* Read-only.
*/
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
Metadata query) throws DbException;
/** /**
* Returns the IDs of any messages that need to be validated. * Returns the IDs of any messages that need to be validated.
* <p/> * <p/>
@@ -314,6 +336,15 @@ public interface DatabaseComponent extends TransactionManager {
Collection<MessageId> getMessagesToShare(Transaction txn) Collection<MessageId> getMessagesToShare(Transaction txn)
throws DbException; throws DbException;
/**
* Returns the IDs of any messages of any messages that are due for
* deletion, along with their group IDs.
* <p/>
* Read-only.
*/
Map<GroupId, Collection<MessageId>> getMessagesToDelete(Transaction txn)
throws DbException;
/** /**
* Returns the metadata for all delivered messages in the given group. * Returns the metadata for all delivered messages in the given group.
* <p/> * <p/>
@@ -395,6 +426,15 @@ public interface DatabaseComponent extends TransactionManager {
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m) MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
throws DbException; throws DbException;
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be deleted, or {@link #NO_CLEANUP_DEADLINE}
* if no messages are scheduled to be deleted.
* <p/>
* Read-only.
*/
long getNextCleanupDeadline(Transaction txn) throws DbException;
/* /*
* Returns the next time (in milliseconds since the Unix epoch) when a * Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may * message is due to be sent to the given contact. The returned value may
@@ -535,6 +575,13 @@ public interface DatabaseComponent extends TransactionManager {
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k) void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
throws DbException; throws DbException;
/**
* Sets the cleanup timer duration for the given message. This does not
* start the message's cleanup timer.
*/
void setCleanupTimerDuration(Transaction txn, MessageId m, long duration)
throws DbException;
/** /**
* Marks the given contact as verified. * Marks the given contact as verified.
*/ */
@@ -557,6 +604,12 @@ public interface DatabaseComponent extends TransactionManager {
*/ */
void setMessagePermanent(Transaction txn, MessageId m) throws DbException; void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
/**
* Marks the given message as not shared. This method is only meant for
* testing.
*/
void setMessageNotShared(Transaction txn, MessageId m) throws DbException;
/** /**
* Marks the given message as shared. * Marks the given message as shared.
*/ */
@@ -599,6 +652,22 @@ public interface DatabaseComponent extends TransactionManager {
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k) void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
throws DbException; throws DbException;
/**
* Starts the cleanup timer for the given message, if a timer duration
* has been set and the timer has not already been started.
*
* @return The cleanup deadline, or {@link #TIMER_NOT_STARTED} if no
* timer duration has been set for this message or its timer has already
* been started.
*/
long startCleanupTimer(Transaction txn, MessageId m) throws DbException;
/**
* Stops the cleanup timer for the given message, if the timer has been
* started.
*/
void stopCleanupTimer(Transaction txn, MessageId m) throws DbException;
/** /**
* Stores the given transport keys, deleting any keys they have replaced. * Stores the given transport keys, deleting any keys they have replaced.
*/ */

View File

@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault @NotNullByDefault
public class ValidationUtils { public class ValidationUtils {
@@ -64,4 +66,9 @@ public class ValidationUtils {
if (dictionary != null && dictionary.size() != size) if (dictionary != null && dictionary.size() != size)
throw new FormatException(); throw new FormatException();
} }
public static void checkRange(@Nullable Long l, long min, long max)
throws FormatException {
if (l != null && (l < min || l > max)) throw new FormatException();
}
} }

View File

@@ -0,0 +1,8 @@
package org.briarproject.bramble.test;
public interface TimeTravel {
void setCurrentTimeMillis(long now) throws InterruptedException;
void addCurrentTimeMillis(long add) throws InterruptedException;
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble; package org.briarproject.bramble;
import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.contact.ContactModule; import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoExecutorModule; import org.briarproject.bramble.crypto.CryptoExecutorModule;
import org.briarproject.bramble.db.DatabaseExecutorModule; import org.briarproject.bramble.db.DatabaseExecutorModule;
@@ -14,6 +15,8 @@ import org.briarproject.bramble.versioning.VersioningModule;
public interface BrambleCoreEagerSingletons { public interface BrambleCoreEagerSingletons {
void inject(CleanupModule.EagerSingletons init);
void inject(ContactModule.EagerSingletons init); void inject(ContactModule.EagerSingletons init);
void inject(CryptoExecutorModule.EagerSingletons init); void inject(CryptoExecutorModule.EagerSingletons init);
@@ -39,6 +42,7 @@ public interface BrambleCoreEagerSingletons {
class Helper { class Helper {
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) { public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
c.inject(new CleanupModule.EagerSingletons());
c.inject(new ContactModule.EagerSingletons()); c.inject(new ContactModule.EagerSingletons());
c.inject(new CryptoExecutorModule.EagerSingletons()); c.inject(new CryptoExecutorModule.EagerSingletons());
c.inject(new DatabaseExecutorModule.EagerSingletons()); c.inject(new DatabaseExecutorModule.EagerSingletons());

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble; package org.briarproject.bramble;
import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.client.ClientModule; import org.briarproject.bramble.client.ClientModule;
import org.briarproject.bramble.connection.ConnectionModule; import org.briarproject.bramble.connection.ConnectionModule;
import org.briarproject.bramble.contact.ContactModule; import org.briarproject.bramble.contact.ContactModule;
@@ -21,15 +22,14 @@ import org.briarproject.bramble.rendezvous.RendezvousModule;
import org.briarproject.bramble.settings.SettingsModule; import org.briarproject.bramble.settings.SettingsModule;
import org.briarproject.bramble.sync.SyncModule; import org.briarproject.bramble.sync.SyncModule;
import org.briarproject.bramble.sync.validation.ValidationModule; import org.briarproject.bramble.sync.validation.ValidationModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.bramble.transport.TransportModule; import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.bramble.versioning.VersioningModule; import org.briarproject.bramble.versioning.VersioningModule;
import dagger.Module; import dagger.Module;
@Module(includes = { @Module(includes = {
CleanupModule.class,
ClientModule.class, ClientModule.class,
ClockModule.class,
ConnectionModule.class, ConnectionModule.class,
ContactModule.class, ContactModule.class,
CryptoModule.class, CryptoModule.class,

View File

@@ -0,0 +1,159 @@
package org.briarproject.bramble.cleanup;
import org.briarproject.bramble.api.cleanup.CleanupHook;
import org.briarproject.bramble.api.cleanup.CleanupManager;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.api.versioning.ClientMajorVersion;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class CleanupManagerImpl implements CleanupManager, Service, EventListener {
private static final Logger LOG =
getLogger(CleanupManagerImpl.class.getName());
private final Executor dbExecutor;
private final DatabaseComponent db;
private final TaskScheduler taskScheduler;
private final Clock clock;
private final Map<ClientMajorVersion, CleanupHook> hooks =
new ConcurrentHashMap<>();
private final Object lock = new Object();
@GuardedBy("lock")
private final Set<CleanupTask> pending = new HashSet<>();
@Inject
CleanupManagerImpl(@DatabaseExecutor Executor dbExecutor,
DatabaseComponent db, TaskScheduler taskScheduler, Clock clock) {
this.dbExecutor = dbExecutor;
this.db = db;
this.taskScheduler = taskScheduler;
this.clock = clock;
}
@Override
public void registerCleanupHook(ClientId c, int majorVersion,
CleanupHook hook) {
hooks.put(new ClientMajorVersion(c, majorVersion), hook);
}
@Override
public void startService() {
maybeScheduleTask(clock.currentTimeMillis());
}
@Override
public void stopService() {
}
@Override
public void eventOccurred(Event e) {
if (e instanceof CleanupTimerStartedEvent) {
CleanupTimerStartedEvent a = (CleanupTimerStartedEvent) e;
maybeScheduleTask(a.getCleanupDeadline());
}
}
private void maybeScheduleTask(long deadline) {
synchronized (lock) {
for (CleanupTask task : pending) {
if (task.deadline <= deadline) return;
}
CleanupTask task = new CleanupTask(deadline);
pending.add(task);
scheduleTask(task);
}
}
private void scheduleTask(CleanupTask task) {
long now = clock.currentTimeMillis();
long delay = max(0, task.deadline - now + BATCH_DELAY_MS);
if (LOG.isLoggable(INFO)) {
LOG.info("Scheduling cleanup task in " + delay + " ms");
}
taskScheduler.schedule(() -> deleteMessagesAndScheduleNextTask(task),
dbExecutor, delay, MILLISECONDS);
}
private void deleteMessagesAndScheduleNextTask(CleanupTask task) {
try {
synchronized (lock) {
pending.remove(task);
}
long deadline = db.transactionWithResult(false, txn -> {
deleteMessages(txn);
return db.getNextCleanupDeadline(txn);
});
if (deadline != NO_CLEANUP_DEADLINE) {
maybeScheduleTask(deadline);
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
private void deleteMessages(Transaction txn) throws DbException {
Map<GroupId, Collection<MessageId>> ids = db.getMessagesToDelete(txn);
for (Entry<GroupId, Collection<MessageId>> e : ids.entrySet()) {
GroupId groupId = e.getKey();
Collection<MessageId> messageIds = e.getValue();
if (LOG.isLoggable(INFO)) {
LOG.info(messageIds.size() + " messages to delete");
}
for (MessageId m : messageIds) db.stopCleanupTimer(txn, m);
Group group = db.getGroup(txn, groupId);
ClientMajorVersion cv = new ClientMajorVersion(group.getClientId(),
group.getMajorVersion());
CleanupHook hook = hooks.get(cv);
if (hook == null) {
throw new IllegalStateException("No cleanup hook for " + cv);
}
hook.deleteMessages(txn, groupId, messageIds);
}
}
private static class CleanupTask {
private final long deadline;
private CleanupTask(long deadline) {
this.deadline = deadline;
}
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.bramble.cleanup;
import org.briarproject.bramble.api.cleanup.CleanupManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class CleanupModule {
public static class EagerSingletons {
@Inject
CleanupManager cleanupManager;
}
@Provides
@Singleton
CleanupManager provideCleanupManager(LifecycleManager lifecycleManager,
EventBus eventBus, CleanupManagerImpl cleanupManager) {
lifecycleManager.registerService(cleanupManager);
eventBus.addListener(cleanupManager);
return cleanupManager;
}
}

View File

@@ -2,11 +2,13 @@ package org.briarproject.bramble.client;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyParser; import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey; import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.BdfReader; import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory; import org.briarproject.bramble.api.data.BdfReaderFactory;
@@ -32,6 +34,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -39,6 +42,7 @@ import java.util.Map.Entry;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION; import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
@@ -151,6 +155,12 @@ class ClientHelperImpl implements ClientHelper {
return metadataParser.parse(metadata); return metadataParser.parse(metadata);
} }
@Override
public Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
BdfDictionary query) throws DbException, FormatException {
return db.getMessageIds(txn, g, metadataEncoder.encode(query));
}
@Override @Override
public BdfDictionary getMessageMetadataAsDictionary(MessageId m) public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException, FormatException { throws DbException, FormatException {
@@ -389,4 +399,27 @@ class ClientHelperImpl implements ClientHelper {
return tpMap; return tpMap;
} }
@Override
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
throws DbException {
try {
BdfDictionary meta =
getGroupMetadataAsDictionary(txn, contactGroupId);
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
} catch (FormatException e) {
throw new DbException(e); // Invalid group metadata
}
}
@Override
public void setContactId(Transaction txn, GroupId contactGroupId,
ContactId c) throws DbException {
BdfDictionary meta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, c.getInt()));
try {
mergeGroupMetadata(txn, contactGroupId, meta);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
} }

View File

@@ -497,6 +497,25 @@ interface Database<T> {
*/ */
Collection<MessageId> getMessagesToShare(T txn) throws DbException; Collection<MessageId> getMessagesToShare(T txn) throws DbException;
/**
* Returns the IDs of any messages of any messages that are due for
* deletion, along with their group IDs.
* <p/>
* Read-only.
*/
Map<GroupId, Collection<MessageId>> getMessagesToDelete(T txn)
throws DbException;
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be deleted, or
* {@link DatabaseComponent#NO_CLEANUP_DEADLINE} if no messages are
* scheduled to be deleted.
* <p/>
* Read-only.
*/
long getNextCleanupDeadline(T txn) throws DbException;
/** /**
* Returns the next time (in milliseconds since the Unix epoch) when a * Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may * message is due to be sent to the given contact. The returned value may
@@ -606,8 +625,10 @@ interface Database<T> {
/** /**
* Marks a message as having been seen by the given contact. * Marks a message as having been seen by the given contact.
*
* @return True if the message was not already marked as seen.
*/ */
void raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException; boolean raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
/** /**
* Removes a contact from the database. * Removes a contact from the database.
@@ -671,6 +692,13 @@ interface Database<T> {
*/ */
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException; void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
/**
* Sets the cleanup timer duration for the given message. This does not
* start the message's cleanup timer.
*/
void setCleanupTimerDuration(T txn, MessageId m, long duration)
throws DbException;
/** /**
* Marks the given contact as verified. * Marks the given contact as verified.
*/ */
@@ -701,9 +729,10 @@ interface Database<T> {
void setMessagePermanent(T txn, MessageId m) throws DbException; void setMessagePermanent(T txn, MessageId m) throws DbException;
/** /**
* Marks the given message as shared. * Marks the given message as shared or not.
*/ */
void setMessageShared(T txn, MessageId m) throws DbException; void setMessageShared(T txn, MessageId m, boolean shared)
throws DbException;
/** /**
* Sets the validation and delivery state of the given message. * Sets the validation and delivery state of the given message.
@@ -730,6 +759,22 @@ interface Database<T> {
void setTransportKeysActive(T txn, TransportId t, KeySetId k) void setTransportKeysActive(T txn, TransportId t, KeySetId k)
throws DbException; throws DbException;
/**
* Starts the cleanup timer for the given message, if a timer duration
* has been set and the timer has not already been started.
*
* @return The cleanup deadline, or
* {@link DatabaseComponent#TIMER_NOT_STARTED} if no timer duration has
* been set for this message or its timer has already been started.
*/
long startCleanupTimer(T txn, MessageId m) throws DbException;
/**
* Stops the cleanup timer for the given message, if the timer has been
* started.
*/
void stopCleanupTimer(T txn, MessageId m) throws DbException;
/** /**
* Updates the transmission count, expiry time and estimated time of arrival * Updates the transmission count, expiry time and estimated time of arrival
* of the given message with respect to the given contact, using the latency * of the given message with respect to the given contact, using the latency

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.db; package org.briarproject.bramble.db;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContact; import org.briarproject.bramble.api.contact.PendingContact;
@@ -576,6 +577,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessageIds(txn, g); return db.getMessageIds(txn, g);
} }
@Override
public Collection<MessageId> getMessageIds(Transaction transaction,
GroupId g, Metadata query) throws DbException {
T txn = unbox(transaction);
if (!db.containsGroup(txn, g))
throw new NoSuchGroupException();
return db.getMessageIds(txn, g, query);
}
@Override @Override
public Collection<MessageId> getMessagesToValidate(Transaction transaction) public Collection<MessageId> getMessagesToValidate(Transaction transaction)
throws DbException { throws DbException {
@@ -597,6 +607,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessagesToShare(txn); return db.getMessagesToShare(txn);
} }
@Override
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
Transaction transaction) throws DbException {
T txn = unbox(transaction);
return db.getMessagesToDelete(txn);
}
@Override @Override
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction, public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
GroupId g) throws DbException { GroupId g) throws DbException {
@@ -692,6 +709,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessageDependents(txn, m); return db.getMessageDependents(txn, m);
} }
@Override
public long getNextCleanupDeadline(Transaction transaction)
throws DbException {
T txn = unbox(transaction);
return db.getNextCleanupDeadline(txn);
}
@Override @Override
public long getNextSendTime(Transaction transaction, ContactId c) public long getNextSendTime(Transaction transaction, ContactId c)
throws DbException { throws DbException {
@@ -795,8 +819,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
Collection<MessageId> acked = new ArrayList<>(); Collection<MessageId> acked = new ArrayList<>();
for (MessageId m : a.getMessageIds()) { for (MessageId m : a.getMessageIds()) {
if (db.containsVisibleMessage(txn, c, m)) { if (db.containsVisibleMessage(txn, c, m)) {
db.raiseSeenFlag(txn, c, m); if (db.raiseSeenFlag(txn, c, m)) {
acked.add(m); // This is the first time the message has been acked by
// this contact. Start the cleanup timer (a no-op unless
// a cleanup deadline has been set for this message)
long deadline = db.startCleanupTimer(txn, m);
if (deadline != TIMER_NOT_STARTED) {
transaction.attach(new CleanupTimerStartedEvent(m,
deadline));
}
acked.add(m);
}
} }
} }
if (acked.size() > 0) { if (acked.size() > 0) {
@@ -952,6 +985,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.removeTransportKeys(txn, t, k); db.removeTransportKeys(txn, t, k);
} }
@Override
public void setCleanupTimerDuration(Transaction transaction, MessageId m,
long duration) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.setCleanupTimerDuration(txn, m, duration);
}
@Override @Override
public void setContactVerified(Transaction transaction, ContactId c) public void setContactVerified(Transaction transaction, ContactId c)
throws DbException { throws DbException {
@@ -1001,6 +1044,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.setMessagePermanent(txn, m); db.setMessagePermanent(txn, m);
} }
@Override
public void setMessageNotShared(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.setMessageShared(txn, m, false);
}
@Override @Override
public void setMessageShared(Transaction transaction, MessageId m) public void setMessageShared(Transaction transaction, MessageId m)
throws DbException { throws DbException {
@@ -1010,7 +1063,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchMessageException(); throw new NoSuchMessageException();
if (db.getMessageState(txn, m) != DELIVERED) if (db.getMessageState(txn, m) != DELIVERED)
throw new IllegalArgumentException("Shared undelivered message"); throw new IllegalArgumentException("Shared undelivered message");
db.setMessageShared(txn, m); db.setMessageShared(txn, m, true);
transaction.attach(new MessageSharedEvent(m)); transaction.attach(new MessageSharedEvent(m));
} }
@@ -1082,6 +1135,30 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.setTransportKeysActive(txn, t, k); db.setTransportKeysActive(txn, t, k);
} }
@Override
public long startCleanupTimer(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
long deadline = db.startCleanupTimer(txn, m);
if (deadline != TIMER_NOT_STARTED) {
transaction.attach(new CleanupTimerStartedEvent(m, deadline));
}
return deadline;
}
@Override
public void stopCleanupTimer(Transaction transaction, MessageId m)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsMessage(txn, m))
throw new NoSuchMessageException();
db.stopCleanupTimer(txn, m);
}
@Override @Override
public void updateTransportKeys(Transaction transaction, public void updateTransportKeys(Transaction transaction,
Collection<TransportKeySet> keys) throws DbException { Collection<TransportKeySet> keys) throws DbException {

View File

@@ -72,6 +72,8 @@ import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.db.Metadata.REMOVE; import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
@@ -98,7 +100,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database<Connection> { abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing // Package access for testing
static final int CODE_SCHEMA_VERSION = 47; static final int CODE_SCHEMA_VERSION = 48;
// Time period offsets for incoming transport keys // Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1; private static final int OFFSET_PREV = -1;
@@ -180,6 +182,11 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " state INT NOT NULL," + " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL," + " shared BOOLEAN NOT NULL,"
+ " temporary BOOLEAN NOT NULL," + " temporary BOOLEAN NOT NULL,"
// Null if no timer duration has been set
+ " cleanupTimerDuration BIGINT,"
// Null if no timer duration has been set or the timer
// hasn't started
+ " cleanupDeadline BIGINT,"
+ " length INT NOT NULL," + " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted + " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId)," + " PRIMARY KEY (messageId),"
@@ -336,6 +343,10 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp" "CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
+ " ON statuses (contactId, timestamp)"; + " ON statuses (contactId, timestamp)";
private static final String INDEX_MESSAGES_BY_CLEANUP_DEADLINE =
"CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
+ " ON messages (cleanupDeadline)";
private static final Logger LOG = private static final Logger LOG =
getLogger(JdbcDatabase.class.getName()); getLogger(JdbcDatabase.class.getName());
@@ -463,7 +474,8 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration43_44(dbTypes), new Migration43_44(dbTypes),
new Migration44_45(), new Migration44_45(),
new Migration45_46(), new Migration45_46(),
new Migration46_47(dbTypes) new Migration46_47(dbTypes),
new Migration47_48()
); );
} }
@@ -531,6 +543,7 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID); s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID); s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP); s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
s.executeUpdate(INDEX_MESSAGES_BY_CLEANUP_DEADLINE);
s.close(); s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
@@ -1290,7 +1303,9 @@ abstract class JdbcDatabase implements Database<Connection> {
public void deleteMessage(Connection txn, MessageId m) throws DbException { public void deleteMessage(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?"; String sql = "UPDATE messages"
+ " SET raw = NULL, cleanupDeadline = NULL"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
@@ -1769,7 +1784,6 @@ abstract class JdbcDatabase implements Database<Connection> {
// Return early if there are no matches // Return early if there are no matches
if (intersection.isEmpty()) return Collections.emptySet(); if (intersection.isEmpty()) return Collections.emptySet();
} }
if (intersection == null) throw new AssertionError();
return intersection; return intersection;
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs, LOG, WARNING); tryToClose(rs, LOG, WARNING);
@@ -2226,6 +2240,39 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
Connection txn) throws DbException {
long now = clock.currentTimeMillis();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT messageId, groupId FROM messages"
+ " WHERE cleanupDeadline <= ?";
ps = txn.prepareStatement(sql);
ps.setLong(1, now);
rs = ps.executeQuery();
Map<GroupId, Collection<MessageId>> ids = new HashMap<>();
while (rs.next()) {
MessageId m = new MessageId(rs.getBytes(1));
GroupId g = new GroupId(rs.getBytes(2));
Collection<MessageId> messageIds = ids.get(g);
if (messageIds == null) {
messageIds = new ArrayList<>();
ids.put(g, messageIds);
}
messageIds.add(m);
}
rs.close();
ps.close();
return ids;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public long getNextSendTime(Connection txn, ContactId c) public long getNextSendTime(Connection txn, ContactId c)
throws DbException { throws DbException {
@@ -2256,6 +2303,31 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public long getNextCleanupDeadline(Connection txn) throws DbException {
Statement s = null;
ResultSet rs = null;
try {
String sql = "SELECT cleanupDeadline FROM messages"
+ " WHERE cleanupDeadline IS NOT NULL"
+ " ORDER BY cleanupDeadline LIMIT 1";
s = txn.createStatement();
rs = s.executeQuery(sql);
long nextDeadline = NO_CLEANUP_DEADLINE;
if (rs.next()) {
nextDeadline = rs.getLong(1);
if (rs.next()) throw new AssertionError();
}
rs.close();
s.close();
return nextDeadline;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public PendingContact getPendingContact(Connection txn, PendingContactId p) public PendingContact getPendingContact(Connection txn, PendingContactId p)
throws DbException { throws DbException {
@@ -2776,7 +2848,7 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void raiseSeenFlag(Connection txn, ContactId c, MessageId m) public boolean raiseSeenFlag(Connection txn, ContactId c, MessageId m)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
@@ -2788,6 +2860,7 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException(); if (affected < 0 || affected > 1) throw new DbStateException();
ps.close(); ps.close();
return affected == 1;
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(ps, LOG, WARNING); tryToClose(ps, LOG, WARNING);
throw new DbException(e); throw new DbException(e);
@@ -3021,6 +3094,25 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public void setCleanupTimerDuration(Connection txn, MessageId m,
long duration) throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET cleanupTimerDuration = ?"
+ " WHERE messageId = ? AND cleanupTimerDuration IS NULL";
ps = txn.prepareStatement(sql);
ps.setLong(1, duration);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public void setContactVerified(Connection txn, ContactId c) public void setContactVerified(Connection txn, ContactId c)
throws DbException { throws DbException {
@@ -3128,22 +3220,24 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void setMessageShared(Connection txn, MessageId m) public void setMessageShared(Connection txn, MessageId m, boolean shared)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE messages SET shared = TRUE" String sql = "UPDATE messages SET shared = ?"
+ " WHERE messageId = ?"; + " WHERE messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBoolean(1, shared);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException(); if (affected < 0 || affected > 1) throw new DbStateException();
ps.close(); ps.close();
// Update denormalised column in statuses // Update denormalised column in statuses
sql = "UPDATE statuses SET messageShared = TRUE" sql = "UPDATE statuses SET messageShared = ?"
+ " WHERE messageId = ?"; + " WHERE messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBoolean(1, shared);
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate(); affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException(); if (affected < 0) throw new DbStateException();
ps.close(); ps.close();
@@ -3272,6 +3366,60 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public long startCleanupTimer(Connection txn, MessageId m)
throws DbException {
long now = clock.currentTimeMillis();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "UPDATE messages"
+ " SET cleanupDeadline = ? + cleanupTimerDuration"
+ " WHERE messageId = ?"
+ " AND cleanupTimerDuration IS NOT NULL"
+ " AND cleanupDeadline IS NULL";
ps = txn.prepareStatement(sql);
ps.setLong(1, now);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
if (affected == 0) return TIMER_NOT_STARTED;
sql = "SELECT cleanupDeadline FROM messages WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
long deadline = rs.getLong(1);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return deadline;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void stopCleanupTimer(Connection txn, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET cleanupDeadline = NULL"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m, public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
int maxLatency) throws DbException { int maxLatency) throws DbException {

View File

@@ -0,0 +1,47 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
class Migration47_48 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration47_48.class.getName());
@Override
public int getStartVersion() {
return 47;
}
@Override
public int getEndVersion() {
return 48;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
// Null if no timer duration has been set
s.execute("ALTER TABLE messages"
+ " ADD COLUMN cleanupTimerDuration BIGINT");
// Null if no timer duration has been set or the timer
// hasn't started
s.execute("ALTER TABLE messages"
+ " ADD COLUMN cleanupDeadline BIGINT");
s.execute("CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
+ " ON messages (cleanupDeadline)");
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
// Metadata keys // Metadata keys
String MSG_KEY_UPDATE_VERSION = "version"; String MSG_KEY_UPDATE_VERSION = "version";
String MSG_KEY_LOCAL = "local"; String MSG_KEY_LOCAL = "local";
String GROUP_KEY_CONTACT_ID = "contactId";
} }

View File

@@ -50,7 +50,6 @@ import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
@@ -161,13 +160,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
db.addGroup(txn, g); db.addGroup(txn, g);
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
// Attach the contact ID to the group // Attach the contact ID to the group
BdfDictionary meta = new BdfDictionary(); clientHelper.setContactId(txn, g.getId(), c.getId());
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
try {
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new AssertionError(e);
}
// Create and store the first local update // Create and store the first local update
List<ClientVersion> versions = new ArrayList<>(clients); List<ClientVersion> versions = new ArrayList<>(clients);
Collections.sort(versions); Collections.sort(versions);
@@ -229,7 +222,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
Map<ClientMajorVersion, Visibility> after = Map<ClientMajorVersion, Visibility> after =
getVisibilities(newLocalStates, newRemoteStates); getVisibilities(newLocalStates, newRemoteStates);
// Call hooks for any visibilities that have changed // Call hooks for any visibilities that have changed
ContactId c = getContactId(txn, m.getGroupId()); ContactId c = clientHelper.getContactId(txn, m.getGroupId());
if (!before.equals(after)) { if (!before.equals(after)) {
Contact contact = db.getContact(txn, c); Contact contact = db.getContact(txn, c);
callVisibilityHooks(txn, contact, before, after); callVisibilityHooks(txn, contact, before, after);
@@ -521,17 +514,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
storeUpdate(txn, g, states, 1); storeUpdate(txn, g, states, 1);
} }
private ContactId getContactId(Transaction txn, GroupId g)
throws DbException {
try {
BdfDictionary meta =
clientHelper.getGroupMetadataAsDictionary(txn, g);
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
} catch (FormatException e) {
throw new DbException(e);
}
}
private List<ClientState> updateStatesFromRemoteStates( private List<ClientState> updateStatesFromRemoteStates(
List<ClientState> oldLocalStates, List<ClientState> remoteStates) { List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
Set<ClientMajorVersion> remoteSet = new HashSet<>(); Set<ClientMajorVersion> remoteSet = new HashSet<>();

View File

@@ -1,12 +1,13 @@
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1 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 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ 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

@@ -1,18 +1,18 @@
package org.briarproject.bramble; package org.briarproject.bramble;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule; import org.briarproject.bramble.system.TimeTravelModule;
public interface BrambleCoreIntegrationTestEagerSingletons public interface BrambleCoreIntegrationTestEagerSingletons
extends BrambleCoreEagerSingletons { extends BrambleCoreEagerSingletons {
void inject(DefaultTaskSchedulerModule.EagerSingletons init); void inject(TimeTravelModule.EagerSingletons init);
class Helper { class Helper {
public static void injectEagerSingletons( public static void injectEagerSingletons(
BrambleCoreIntegrationTestEagerSingletons c) { BrambleCoreIntegrationTestEagerSingletons c) {
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c); BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
c.inject(new DefaultTaskSchedulerModule.EagerSingletons()); c.inject(new TimeTravelModule.EagerSingletons());
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.db; package org.briarproject.bramble.db;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContactId; import org.briarproject.bramble.api.contact.PendingContactId;
@@ -69,6 +70,8 @@ import static java.util.Arrays.asList;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.HOURS;
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
@@ -510,11 +513,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the group is in the DB (which it's not) // Check whether the group is in the DB (which it's not)
exactly(8).of(database).startTransaction(); exactly(10).of(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
exactly(8).of(database).containsGroup(txn, groupId); exactly(10).of(database).containsGroup(txn, groupId);
will(returnValue(false)); will(returnValue(false));
exactly(8).of(database).abortTransaction(txn); exactly(10).of(database).abortTransaction(txn);
// Allow other checks to pass // Allow other checks to pass
allowing(database).containsContact(txn, contactId); allowing(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
@@ -523,7 +526,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getGroup(transaction, groupId)); db.getGroup(transaction, groupId));
fail(); fail();
} catch (NoSuchGroupException expected) { } catch (NoSuchGroupException expected) {
@@ -531,7 +534,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getGroupMetadata(transaction, groupId)); db.getGroupMetadata(transaction, groupId));
fail(); fail();
} catch (NoSuchGroupException expected) { } catch (NoSuchGroupException expected) {
@@ -539,7 +542,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageIds(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
// Expected
}
try {
db.transaction(true, transaction ->
db.getMessageIds(transaction, groupId, new Metadata()));
fail();
} catch (NoSuchGroupException expected) {
// Expected
}
try {
db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId)); db.getMessageMetadata(transaction, groupId));
fail(); fail();
} catch (NoSuchGroupException expected) { } catch (NoSuchGroupException expected) {
@@ -547,7 +566,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId, db.getMessageMetadata(transaction, groupId,
new Metadata())); new Metadata()));
fail(); fail();
@@ -556,7 +575,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, groupId)); db.getMessageStatus(transaction, contactId, groupId));
fail(); fail();
} catch (NoSuchGroupException expected) { } catch (NoSuchGroupException expected) {
@@ -594,11 +613,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the message is in the DB (which it's not) // Check whether the message is in the DB (which it's not)
exactly(12).of(database).startTransaction(); exactly(15).of(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
exactly(12).of(database).containsMessage(txn, messageId); exactly(15).of(database).containsMessage(txn, messageId);
will(returnValue(false)); will(returnValue(false));
exactly(12).of(database).abortTransaction(txn); exactly(15).of(database).abortTransaction(txn);
// Allow other checks to pass // Allow other checks to pass
allowing(database).containsContact(txn, contactId); allowing(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
@@ -623,7 +642,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessage(transaction, messageId)); db.getMessage(transaction, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
@@ -631,7 +650,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageMetadata(transaction, messageId)); db.getMessageMetadata(transaction, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
@@ -639,7 +658,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageState(transaction, messageId)); db.getMessageState(transaction, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
@@ -647,7 +666,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, messageId)); db.getMessageStatus(transaction, contactId, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
@@ -662,6 +681,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected // Expected
} }
try {
db.transaction(false, transaction ->
db.setCleanupTimerDuration(transaction, message.getId(),
HOURS.toMillis(1)));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try { try {
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.setMessagePermanent(transaction, message.getId())); db.setMessagePermanent(transaction, message.getId()));
@@ -687,7 +715,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageDependencies(transaction, messageId)); db.getMessageDependencies(transaction, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
@@ -695,12 +723,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
try { try {
db.transaction(false, transaction -> db.transaction(true, transaction ->
db.getMessageDependents(transaction, messageId)); db.getMessageDependents(transaction, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
// Expected // Expected
} }
try {
db.transaction(false, transaction ->
db.startCleanupTimer(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.stopCleanupTimer(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
} }
@Test @Test
@@ -981,6 +1025,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).containsVisibleMessage(txn, contactId, messageId); oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true)); will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId); oneOf(database).raiseSeenFlag(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).startCleanupTimer(txn, messageId);
will(returnValue(TIMER_NOT_STARTED)); // No cleanup duration was set
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class))); oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
}}); }});
@@ -993,6 +1040,56 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}); });
} }
@Test
public void testReceiveDuplicateAck() throws Exception {
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
will(returnValue(false)); // Already acked
oneOf(database).commitTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
db.transaction(false, transaction -> {
Ack a = new Ack(singletonList(messageId));
db.receiveAck(transaction, contactId, a);
});
}
@Test
public void testReceiveAckWithCleanupTimer() throws Exception {
long deadline = System.currentTimeMillis();
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
will(returnValue(true));
oneOf(database).startCleanupTimer(txn, messageId);
will(returnValue(deadline));
oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(
CleanupTimerStartedEvent.class)));
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
db.transaction(false, transaction -> {
Ack a = new Ack(singletonList(messageId));
db.receiveAck(transaction, contactId, a);
});
}
@Test @Test
public void testReceiveMessage() throws Exception { public void testReceiveMessage() throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{

View File

@@ -53,10 +53,11 @@ import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
import static org.briarproject.bramble.api.db.Metadata.REMOVE; import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
@@ -351,7 +352,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty()); assertTrue(ids.isEmpty());
// Sharing the message should make it sendable // Sharing the message should make it sendable
db.setMessageShared(txn, messageId); db.setMessageShared(txn, messageId, true);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
assertEquals(singletonList(messageId), ids); assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY); ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
@@ -631,8 +632,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// The group should not be visible to the contact // The group should not be visible to the contact
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(emptyMap(), assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
db.getGroupVisibility(txn, groupId));
// Make the group visible to the contact // Make the group visible to the contact
db.addGroupVisibility(txn, contactId, groupId, false); db.addGroupVisibility(txn, contactId, groupId, false);
@@ -655,8 +655,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Make the group invisible again // Make the group invisible again
db.removeGroupVisibility(txn, contactId, groupId); db.removeGroupVisibility(txn, contactId, groupId);
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(emptyMap(), assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
db.getGroupVisibility(txn, groupId));
db.commitTransaction(txn); db.commitTransaction(txn);
db.close(); db.close();
@@ -2040,7 +2039,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId)); assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the message - now it should be sendable immediately // Share the message - now it should be sendable immediately
db.setMessageShared(txn, messageId); db.setMessageShared(txn, messageId, true);
assertEquals(0, db.getNextSendTime(txn, contactId)); assertEquals(0, db.getNextSendTime(txn, contactId));
// Mark the message as requested - it should still be sendable // Mark the message as requested - it should still be sendable
@@ -2347,6 +2346,87 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close(); db.close();
} }
@Test
public void testCleanupTimer() throws Exception {
long duration = 60_000;
long now = System.currentTimeMillis();
AtomicLong time = new AtomicLong(now);
Database<Connection> db =
open(false, new TestMessageFactory(), new SettableClock(time));
Connection txn = db.startTransaction();
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Add a group and a message
db.addGroup(txn, group);
db.addMessage(txn, message, DELIVERED, false, false, null);
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Set the message's cleanup timer duration
db.setCleanupTimerDuration(txn, messageId, duration);
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Start the message's cleanup timer
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
// The timer can't be started again
assertEquals(TIMER_NOT_STARTED, db.startCleanupTimer(txn, messageId));
// No messages should be due for deletion, but the message should be
// scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// Stop the timer
db.stopCleanupTimer(txn, messageId);
// No messages should be due or scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
// Start the timer again
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
// No messages should be due for deletion, but the message should be
// scheduled for deletion
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// 1 ms before the timer expires, no messages should be due for
// deletion but the message should be scheduled for deletion
time.set(now + duration - 1);
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// When the timer expires, the message should be due and scheduled for
// deletion
time.set(now + duration);
assertEquals(singletonMap(groupId, singletonList(messageId)),
db.getMessagesToDelete(txn));
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// 1 ms after the timer expires, the message should be due and
// scheduled for deletion
time.set(now + duration + 1);
assertEquals(singletonMap(groupId, singletonList(messageId)),
db.getMessagesToDelete(txn));
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
// Once the message has been deleted, it should no longer be due
// or scheduled for deletion
db.deleteMessage(txn, messageId);
assertTrue(db.getMessagesToDelete(txn).isEmpty());
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
}
private Database<Connection> open(boolean resume) throws Exception { private Database<Connection> open(boolean resume) throws Exception {
return open(resume, new TestMessageFactory(), new SystemClock()); return open(resume, new TestMessageFactory(), new SystemClock());
} }

View File

@@ -0,0 +1,122 @@
package org.briarproject.bramble.system;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.junit.Assert.fail;
/**
* A {@link TaskScheduler} for use in tests. The scheduler keeps all scheduled
* tasks in a queue until {@link #runTasks()} is called.
*/
@NotNullByDefault
class TestTaskScheduler implements TaskScheduler {
private final Queue<Task> queue = new PriorityBlockingQueue<>();
private final Clock clock;
TestTaskScheduler(Clock clock) {
this.clock = clock;
}
@Override
public Cancellable schedule(Runnable task, Executor executor, long delay,
TimeUnit unit) {
AtomicBoolean cancelled = new AtomicBoolean(false);
return schedule(task, executor, delay, unit, cancelled);
}
@Override
public Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
long delay, long interval, TimeUnit unit) {
AtomicBoolean cancelled = new AtomicBoolean(false);
return scheduleWithFixedDelay(task, executor, delay, interval, unit,
cancelled);
}
private Cancellable schedule(Runnable task, Executor executor, long delay,
TimeUnit unit, AtomicBoolean cancelled) {
long delayMillis = MILLISECONDS.convert(delay, unit);
long dueMillis = clock.currentTimeMillis() + delayMillis;
Task t = new Task(task, executor, dueMillis, cancelled);
queue.add(t);
return t;
}
private Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
long delay, long interval, TimeUnit unit, AtomicBoolean cancelled) {
// All executions of this periodic task share a cancelled flag
Runnable wrapped = () -> {
task.run();
scheduleWithFixedDelay(task, executor, interval, interval, unit,
cancelled);
};
return schedule(wrapped, executor, delay, unit, cancelled);
}
/**
* Runs any scheduled tasks that are due.
*/
void runTasks() throws InterruptedException {
long now = clock.currentTimeMillis();
while (true) {
Task t = queue.peek();
if (t == null || t.dueMillis > now) return;
t = queue.poll();
// Submit the task to its executor and wait for it to finish
if (!t.run().await(1, MINUTES)) fail();
}
}
private static class Task
implements Cancellable, Comparable<Task> {
private final Runnable task;
private final Executor executor;
private final long dueMillis;
private final AtomicBoolean cancelled;
private Task(Runnable task, Executor executor, long dueMillis,
AtomicBoolean cancelled) {
this.task = task;
this.executor = executor;
this.dueMillis = dueMillis;
this.cancelled = cancelled;
}
@SuppressWarnings("UseCompareMethod") // Animal Sniffer
@Override
public int compareTo(Task task) {
return Long.valueOf(dueMillis).compareTo(task.dueMillis);
}
/**
* Submits the task to its executor and returns a latch that will be
* released when the task finishes.
*/
public CountDownLatch run() {
if (cancelled.get()) return new CountDownLatch(0);
CountDownLatch latch = new CountDownLatch(1);
executor.execute(() -> {
task.run();
latch.countDown();
});
return latch;
}
@Override
public void cancel() {
cancelled.set(true);
}
}
}

View File

@@ -0,0 +1,98 @@
package org.briarproject.bramble.system;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.test.SettableClock;
import org.briarproject.bramble.test.TimeTravel;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class TimeTravelModule {
public static class EagerSingletons {
@Inject
TaskScheduler scheduler;
}
private final ScheduledExecutorService scheduledExecutorService;
private final Clock clock;
private final TaskScheduler taskScheduler;
private final TimeTravel timeTravel;
public TimeTravelModule() {
this(false);
}
public TimeTravelModule(boolean travel) {
// Discard tasks that are submitted during shutdown
RejectedExecutionHandler policy =
new ScheduledThreadPoolExecutor.DiscardPolicy();
scheduledExecutorService =
new ScheduledThreadPoolExecutor(1, policy);
if (travel) {
// Use a SettableClock and TestTaskScheduler to allow time travel
AtomicLong time = new AtomicLong(System.currentTimeMillis());
clock = new SettableClock(time);
TestTaskScheduler testTaskScheduler = new TestTaskScheduler(clock);
taskScheduler = testTaskScheduler;
timeTravel = new TimeTravel() {
@Override
public void setCurrentTimeMillis(long now)
throws InterruptedException {
time.set(now);
testTaskScheduler.runTasks();
}
@Override
public void addCurrentTimeMillis(long add)
throws InterruptedException {
time.addAndGet(add);
testTaskScheduler.runTasks();
}
};
} else {
// Use the default clock and task scheduler
clock = new SystemClock();
taskScheduler = new TaskSchedulerImpl(scheduledExecutorService);
timeTravel = new TimeTravel() {
@Override
public void setCurrentTimeMillis(long now) {
throw new UnsupportedOperationException();
}
@Override
public void addCurrentTimeMillis(long add) {
throw new UnsupportedOperationException();
}
};
}
}
@Provides
Clock provideClock() {
return clock;
}
@Provides
@Singleton
TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager) {
lifecycleManager.registerForShutdown(scheduledExecutorService);
return taskScheduler;
}
@Provides
TimeTravel provideTimeTravel() {
return timeTravel;
}
}

View File

@@ -3,8 +3,8 @@ package org.briarproject.bramble.test;
import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule; import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule; import org.briarproject.bramble.event.DefaultEventExecutorModule;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule; import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@@ -12,11 +12,11 @@ import dagger.Provides;
@Module(includes = { @Module(includes = {
DefaultBatteryManagerModule.class, DefaultBatteryManagerModule.class,
DefaultEventExecutorModule.class, DefaultEventExecutorModule.class,
DefaultTaskSchedulerModule.class,
DefaultWakefulIoExecutorModule.class, DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class, TestDatabaseConfigModule.class,
TestPluginConfigModule.class, TestPluginConfigModule.class,
TestSecureRandomModule.class TestSecureRandomModule.class,
TimeTravelModule.class
}) })
public class BrambleCoreIntegrationTestModule { public class BrambleCoreIntegrationTestModule {
@@ -33,6 +33,11 @@ public class BrambleCoreIntegrationTestModule {
public boolean shouldEnableProfilePictures() { public boolean shouldEnableProfilePictures() {
return true; return true;
} }
@Override
public boolean shouldEnableDisappearingMessages() {
return true;
}
}; };
} }
} }

View File

@@ -38,7 +38,6 @@ import static org.briarproject.bramble.test.TestUtils.getContact;
import static org.briarproject.bramble.test.TestUtils.getGroup; import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage; import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -60,8 +59,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
private final ClientId clientId = getClientId(); private final ClientId clientId = getClientId();
private final long now = System.currentTimeMillis(); private final long now = System.currentTimeMillis();
private final Transaction txn = new Transaction(null, false); private final Transaction txn = new Transaction(null, false);
private final BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
private ClientVersioningManagerImpl createInstance() { private ClientVersioningManagerImpl createInstance() {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -123,8 +120,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).addGroup(txn, contactGroup); oneOf(db).addGroup(txn, contactGroup);
oneOf(db).setGroupVisibility(txn, contact.getId(), oneOf(db).setGroupVisibility(txn, contact.getId(),
contactGroup.getId(), SHARED); contactGroup.getId(), SHARED);
oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(), oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
groupMeta); contact.getId());
oneOf(clock).currentTimeMillis(); oneOf(clock).currentTimeMillis();
will(returnValue(now)); will(returnValue(now));
oneOf(clientHelper).createMessage(contactGroup.getId(), now, oneOf(clientHelper).createMessage(contactGroup.getId(), now,
@@ -460,9 +457,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).deleteMessage(txn, oldRemoteUpdateId); oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId); oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
// Get contact ID // Get contact ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
// No states or visibilities have changed // No states or visibilities have changed
}}); }});
@@ -492,10 +488,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Get client ID // Get contact ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
// No states or visibilities have changed // No states or visibilities have changed
}}); }});
@@ -546,8 +541,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of( BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L), new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true)); new BdfEntry(MSG_KEY_LOCAL, true));
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate); oneOf(clientHelper).toList(newRemoteUpdate);
@@ -577,9 +570,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
oneOf(db).getContact(txn, contact.getId()); oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact)); will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility); oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
@@ -619,8 +611,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of( BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L), new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true)); new BdfEntry(MSG_KEY_LOCAL, true));
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate); oneOf(clientHelper).toList(newRemoteUpdate);
@@ -650,9 +640,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getContactId(txn, contactGroup.getId());
contactGroup.getId()); will(returnValue(contact.getId()));
will(returnValue(groupMeta));
oneOf(db).getContact(txn, contact.getId()); oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact)); will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE); oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);

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

@@ -8,9 +8,9 @@ import org.briarproject.bramble.system.JavaSystemModule;
import dagger.Module; import dagger.Module;
@Module(includes = { @Module(includes = {
CircumventionModule.class,
JavaNetworkModule.class, JavaNetworkModule.class,
JavaSystemModule.class, JavaSystemModule.class,
CircumventionModule.class,
SocksModule.class SocksModule.class
}) })
public class BrambleJavaModule { public class BrambleJavaModule {

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

@@ -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

@@ -19,15 +19,11 @@ android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.2' buildToolsVersion '30.0.2'
packagingOptions {
doNotStrip '**/*.so'
}
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 10220 versionCode 10216
versionName "1.2.20" versionName "1.2.16"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -4,20 +4,16 @@ import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.db.DbException;
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.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.briar.R; import org.briarproject.briar.R;
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 androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE_ASK_AGAIN;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@@ -31,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();
@@ -45,8 +39,22 @@ public abstract class UiTest {
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
@@ -54,17 +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 {
// do not show doze white-listing dialog lifecycleManager.waitForStartup();
Settings settings = new Settings(); } catch (InterruptedException e) {
settings.putBoolean(DOZE_ASK_AGAIN, false); throw new AssertionError(e);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); }
} catch (InterruptedException | DbException e) { runnable.run();
throw new AssertionError(e);
} }
} }
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
@@ -16,6 +17,7 @@ import dagger.Component;
@Component(modules = { @Component(modules = {
AppModule.class, AppModule.class,
AttachmentModule.class, AttachmentModule.class,
ClockModule.class,
MediaModule.class, MediaModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,

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

@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
@@ -17,6 +18,7 @@ import dagger.Component;
@Component(modules = { @Component(modules = {
AppModule.class, AppModule.class,
AttachmentModule.class, AttachmentModule.class,
ClockModule.class,
MediaModule.class, MediaModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,

View File

@@ -16,7 +16,6 @@ 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 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;
@@ -30,9 +29,7 @@ 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.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 {
@@ -79,8 +76,6 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
@Test @Test
public void appLock() { public void appLock() {
assumeTrue("device has no screen lock",
hasScreenLock(getApplicationContext()));
// scroll down // scroll down
onView(withClassName(is(RecyclerView.class.getName()))) onView(withClassName(is(RecyclerView.class.getName())))
.perform(scrollTo(hasDescendant( .perform(scrollTo(hasDescendant(

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"

View File

@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
@@ -42,6 +43,7 @@ import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager; import org.briarproject.briar.api.android.LockManager;
import org.briarproject.briar.api.android.ScreenFilterMonitor; import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.attachment.AttachmentReader; import org.briarproject.briar.api.attachment.AttachmentReader;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -76,6 +78,7 @@ import dagger.Component;
BriarAccountModule.class, BriarAccountModule.class,
AppModule.class, AppModule.class,
AttachmentModule.class, AttachmentModule.class,
ClockModule.class,
MediaModule.class MediaModule.class
}) })
public interface AndroidComponent public interface AndroidComponent
@@ -184,6 +187,8 @@ public interface AndroidComponent
Thread.UncaughtExceptionHandler exceptionHandler(); Thread.UncaughtExceptionHandler exceptionHandler();
AutoDeleteManager autoDeleteManager();
void inject(SignInReminderReceiver briarService); void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService); void inject(BriarService briarService);

View File

@@ -37,6 +37,7 @@ import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.briarproject.briar.android.util.BriarNotificationBuilder; import org.briarproject.briar.android.util.BriarNotificationBuilder;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent; import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import org.briarproject.briar.api.conversation.ConversationResponse;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent; import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent; import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
@@ -226,6 +227,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
} else if (e instanceof ConversationMessageReceivedEvent) { } else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent<?> p = ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e; (ConversationMessageReceivedEvent<?>) e;
if (p.getMessageHeader() instanceof ConversationResponse) {
ConversationResponse r =
(ConversationResponse) p.getMessageHeader();
// don't show notification for own auto-decline responses
if (r.isAutoDecline()) return;
}
showContactNotification(p.getContactId()); showContactNotification(p.getContactId());
} else if (e instanceof GroupMessageAddedEvent) { } else if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;

View File

@@ -30,10 +30,8 @@ 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.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.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;
@@ -84,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,
@@ -287,6 +283,11 @@ public class AppModule {
public boolean shouldEnableProfilePictures() { public boolean shouldEnableProfilePictures() {
return IS_DEBUG_BUILD; return IS_DEBUG_BUILD;
} }
@Override
public boolean shouldEnableDisappearingMessages() {
return IS_DEBUG_BUILD;
}
}; };
} }
} }

View File

@@ -33,6 +33,7 @@ public class DozeFragment extends SetupFragment
private DozeView dozeView; private DozeView dozeView;
private HuaweiView huaweiView; private HuaweiView huaweiView;
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() {
@@ -57,19 +58,11 @@ public class DozeFragment extends SetupFragment
huaweiView = v.findViewById(R.id.huaweiView); huaweiView = v.findViewById(R.id.huaweiView);
huaweiView.setOnCheckedChangedListener(this); huaweiView.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;
} }
@@ -111,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

@@ -38,6 +38,7 @@ 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();
@@ -63,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);
@@ -74,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;
} }
@@ -126,6 +116,20 @@ public class SetPasswordFragment extends SetupFragment {
IBinder token = passwordEntry.getWindowToken(); IBinder token = passwordEntry.getWindowToken();
Object o = getContext().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

@@ -7,6 +7,7 @@ import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.TextView; import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
@@ -18,6 +19,8 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@@ -32,6 +35,7 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
OnEditorActionListener, OnClickListener { OnEditorActionListener, OnClickListener {
private final static String STATE_KEY_CLICKED = "setupFragmentClicked"; private final static String STATE_KEY_CLICKED = "setupFragmentClicked";
private boolean clicked = false;
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@@ -44,6 +48,27 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
.get(SetupViewModel.class); .get(SetupViewModel.class);
} }
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
clicked = savedInstanceState.getBoolean(STATE_KEY_CLICKED);
}
if (clicked) {
setNextClicked();
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_KEY_CLICKED, clicked);
}
@CallSuper
void setNextClicked() {
this.clicked = true;
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.help_action, menu); inflater.inflate(R.menu.help_action, menu);
@@ -89,4 +114,5 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
public void afterTextChanged(Editable editable) { public void afterTextChanged(Editable editable) {
// noop // noop
} }
} }

View File

@@ -17,8 +17,6 @@ import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.account.SetupViewModel.State.AUTHOR_NAME; import static org.briarproject.briar.android.account.SetupViewModel.State.AUTHOR_NAME;
@@ -38,8 +36,6 @@ class SetupViewModel extends AndroidViewModel {
@Nullable @Nullable
private String authorName, password; private String authorName, password;
private final MutableLiveEvent<State> state = new MutableLiveEvent<>(); private final MutableLiveEvent<State> state = new MutableLiveEvent<>();
private final MutableLiveData<Boolean> isCreatingAccount =
new MutableLiveData<>(false);
private final AccountManager accountManager; private final AccountManager accountManager;
private final Executor ioExecutor; private final Executor ioExecutor;
@@ -71,10 +67,6 @@ class SetupViewModel extends AndroidViewModel {
return state; return state;
} }
LiveData<Boolean> getIsCreatingAccount() {
return isCreatingAccount;
}
void setAuthorName(String authorName) { void setAuthorName(String authorName) {
this.authorName = authorName; this.authorName = authorName;
state.setEvent(SET_PASSWORD); state.setEvent(SET_PASSWORD);
@@ -105,7 +97,6 @@ class SetupViewModel extends AndroidViewModel {
private void createAccount() { private void createAccount() {
if (authorName == null) throw new IllegalStateException(); if (authorName == null) throw new IllegalStateException();
if (password == null) throw new IllegalStateException(); if (password == null) throw new IllegalStateException();
isCreatingAccount.setValue(true);
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
if (accountManager.createAccount(authorName, password)) { if (accountManager.createAccount(authorName, password)) {
LOG.info("Created account"); LOG.info("Created account");

View File

@@ -11,8 +11,10 @@ import org.briarproject.briar.android.account.SetupActivity;
import org.briarproject.briar.android.account.UnlockActivity; import org.briarproject.briar.android.account.UnlockActivity;
import org.briarproject.briar.android.blog.BlogActivity; import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.blog.BlogFragment; import org.briarproject.briar.android.blog.BlogFragment;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.blog.BlogPostFragment; import org.briarproject.briar.android.blog.BlogPostFragment;
import org.briarproject.briar.android.blog.FeedFragment; import org.briarproject.briar.android.blog.FeedFragment;
import org.briarproject.briar.android.blog.FeedPostFragment;
import org.briarproject.briar.android.blog.ReblogActivity; import org.briarproject.briar.android.blog.ReblogActivity;
import org.briarproject.briar.android.blog.ReblogFragment; import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity; import org.briarproject.briar.android.blog.RssFeedImportActivity;
@@ -25,6 +27,7 @@ import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity; import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.AliasDialogFragment; import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.conversation.ConversationSettingsDialog;
import org.briarproject.briar.android.conversation.ImageActivity; import org.briarproject.briar.android.conversation.ImageActivity;
import org.briarproject.briar.android.conversation.ImageFragment; import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.CreateForumActivity;
@@ -83,6 +86,7 @@ import dagger.Component;
@ActivityScope @ActivityScope
@Component(modules = { @Component(modules = {
ActivityModule.class, ActivityModule.class,
BlogModule.class,
CreateGroupModule.class, CreateGroupModule.class,
GroupInvitationModule.class, GroupInvitationModule.class,
GroupMemberModule.class, GroupMemberModule.class,
@@ -149,6 +153,8 @@ public interface ActivityComponent {
void inject(BlogPostFragment fragment); void inject(BlogPostFragment fragment);
void inject(FeedPostFragment fragment);
void inject(ReblogFragment fragment); void inject(ReblogFragment fragment);
void inject(ReblogActivity activity); void inject(ReblogActivity activity);
@@ -233,4 +239,6 @@ public interface ActivityComponent {
void inject(ConfirmAvatarDialogFragment fragment); void inject(ConfirmAvatarDialogFragment fragment);
void inject(ConversationSettingsDialog dialog);
} }

View File

@@ -6,6 +6,7 @@ public interface RequestCodes {
int REQUEST_INTRODUCTION = 2; int REQUEST_INTRODUCTION = 2;
int REQUEST_GROUP_INVITE = 3; int REQUEST_GROUP_INVITE = 3;
int REQUEST_SHARE_FORUM = 4; int REQUEST_SHARE_FORUM = 4;
int REQUEST_WRITE_BLOG_POST = 5;
int REQUEST_SHARE_BLOG = 6; int REQUEST_SHARE_BLOG = 6;
int REQUEST_RINGTONE = 7; int REQUEST_RINGTONE = 7;
int REQUEST_PERMISSION_CAMERA_LOCATION = 8; int REQUEST_PERMISSION_CAMERA_LOCATION = 8;

View File

@@ -28,12 +28,9 @@ import javax.inject.Inject;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE; import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
@@ -89,17 +86,7 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
List<AttachmentHeader> headers = messageHeader.getAttachmentHeaders(); List<AttachmentHeader> headers = messageHeader.getAttachmentHeaders();
List<LiveData<AttachmentItem>> items = new ArrayList<>(headers.size()); List<LiveData<AttachmentItem>> items = new ArrayList<>(headers.size());
boolean needsSize = headers.size() == 1; boolean needsSize = headers.size() == 1;
List<String> supported = asList(getSupportedImageContentTypes());
for (AttachmentHeader h : headers) { for (AttachmentHeader h : headers) {
// Fail early if we don't support the content type
if (!supported.contains(h.getContentType())) {
if (LOG.isLoggable(INFO)) {
LOG.info("Unsupported content type " + h.getContentType());
}
AttachmentItem item = new AttachmentItem(h, "", ERROR);
items.add(new MutableLiveData<>(item));
continue;
}
// try cache for existing item live data // try cache for existing item live data
MutableLiveData<AttachmentItem> liveData = MutableLiveData<AttachmentItem> liveData =
itemsWithSize.get(h.getMessageId()); itemsWithSize.get(h.getMessageId());

View File

@@ -0,0 +1,48 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
interface BaseController {
@UiThread
void onStart();
@UiThread
void onStop();
void loadBlogPosts(GroupId g,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlogPost(GroupId g, MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void repeatPost(BlogPostItem item, @Nullable String comment,
ExceptionHandler<DbException> handler);
@NotNullByDefault
interface BlogListener {
@UiThread
void onBlogPostAdded(BlogPostHeader header, boolean local);
@UiThread
void onBlogRemoved();
}
}

View File

@@ -0,0 +1,209 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BaseControllerImpl extends DbControllerImpl
implements BaseController, EventListener {
private static final Logger LOG =
Logger.getLogger(BaseControllerImpl.class.getName());
protected final EventBus eventBus;
protected final AndroidNotificationManager notificationManager;
protected final IdentityManager identityManager;
protected final BlogManager blogManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, BlogPostHeader> headerCache =
new ConcurrentHashMap<>();
BaseControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
AndroidNotificationManager notificationManager,
IdentityManager identityManager, BlogManager blogManager) {
super(dbExecutor, lifecycleManager);
this.eventBus = eventBus;
this.notificationManager = notificationManager;
this.identityManager = identityManager;
this.blogManager = blogManager;
}
@Override
@CallSuper
public void onStart() {
eventBus.addListener(this);
}
@Override
@CallSuper
public void onStop() {
eventBus.removeListener(this);
}
@Override
public void loadBlogPosts(GroupId groupId,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
Collection<BlogPostItem> items = loadItems(groupId);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
Collection<BlogPostItem> loadItems(GroupId groupId) throws DbException {
long start = now();
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(groupId);
logDuration(LOG, "Loading headers", start);
Collection<BlogPostItem> items = new ArrayList<>(headers.size());
start = now();
for (BlogPostHeader h : headers) {
headerCache.put(h.getId(), h);
BlogPostItem item = getItem(h);
items.add(item);
}
logDuration(LOG, "Loading bodies", start);
return items;
}
@Override
public void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
String text = textCache.get(header.getId());
if (text != null) {
LOG.info("Loaded text from cache");
handler.onResult(new BlogPostItem(header, text));
return;
}
runOnDbThread(() -> {
try {
long start = now();
BlogPostItem item = getItem(header);
logDuration(LOG, "Loading text", start);
handler.onResult(item);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void loadBlogPost(GroupId g, MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
BlogPostHeader header = headerCache.get(m);
if (header != null) {
LOG.info("Loaded header from cache");
loadBlogPost(header, handler);
return;
}
runOnDbThread(() -> {
try {
long start = now();
BlogPostHeader header1 = getPostHeader(g, m);
BlogPostItem item = getItem(header1);
logDuration(LOG, "Loading post", start);
handler.onResult(item);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void repeatPost(BlogPostItem item, @Nullable String comment,
ExceptionHandler<DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
BlogPostHeader h = item.getHeader();
blogManager.addLocalComment(a, b.getId(), comment, h);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
private BlogPostHeader getPostHeader(GroupId g, MessageId m)
throws DbException {
BlogPostHeader header = headerCache.get(m);
if (header == null) {
header = blogManager.getPostHeader(g, m);
headerCache.put(m, header);
}
return header;
}
@DatabaseExecutor
private BlogPostItem getItem(BlogPostHeader h) throws DbException {
String text;
if (h instanceof BlogCommentHeader) {
BlogCommentHeader c = (BlogCommentHeader) h;
BlogCommentItem item = new BlogCommentItem(c);
text = getPostText(item.getPostHeader().getId());
item.setText(text);
return item;
} else {
text = getPostText(h.getId());
return new BlogPostItem(h, text);
}
}
@DatabaseExecutor
private String getPostText(MessageId m) throws DbException {
String text = textCache.get(m);
if (text == null) {
text = HtmlUtils.clean(blogManager.getPostText(m), ARTICLE);
textCache.put(m, text);
}
return text;
}
}

View File

@@ -0,0 +1,121 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BasePostFragment extends BaseFragment {
static final String POST_ID = "briar.POST_ID";
private static final Logger LOG =
getLogger(BasePostFragment.class.getName());
private final Handler handler = new Handler(Looper.getMainLooper());
protected MessageId postId;
private ProgressBar progressBar;
private BlogPostViewHolder ui;
private BlogPostItem post;
private Runnable refresher;
@CallSuper
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// retrieve MessageId of blog post from arguments
byte[] p = requireArguments().getByteArray(POST_ID);
if (p == null) throw new IllegalStateException("No post ID in args");
postId = new MessageId(p);
View view = inflater.inflate(R.layout.fragment_blog_post, container,
false);
progressBar = view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
ui = new BlogPostViewHolder(view, true, new OnBlogPostClickListener() {
@Override
public void onBlogPostClick(BlogPostItem post) {
// We're already there
}
@Override
public void onAuthorClick(BlogPostItem post) {
if (getContext() == null) return;
Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
getContext().startActivity(i);
}
}, getFragmentManager());
return view;
}
@CallSuper
@Override
public void onStart() {
super.onStart();
startPeriodicUpdate();
}
@CallSuper
@Override
public void onStop() {
super.onStop();
stopPeriodicUpdate();
}
@UiThread
protected void onBlogPostLoaded(BlogPostItem post) {
progressBar.setVisibility(INVISIBLE);
this.post = post;
ui.bindItem(post);
}
private void startPeriodicUpdate() {
refresher = () -> {
LOG.info("Updating Content...");
ui.updateDate(post.getTimestamp());
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
};
LOG.info("Adding Handler Callback");
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
private void stopPeriodicUpdate() {
if (refresher != null) {
LOG.info("Removing Handler Callback");
handler.removeCallbacks(refresher);
}
}
}

View File

@@ -1,216 +0,0 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@NotNullByDefault
abstract class BaseViewModel extends DbViewModel implements EventListener {
private static final Logger LOG = getLogger(BaseViewModel.class.getName());
private final EventBus eventBus;
protected final IdentityManager identityManager;
protected final AndroidNotificationManager notificationManager;
protected final BlogManager blogManager;
protected final MutableLiveData<LiveResult<ListUpdate>> blogPosts =
new MutableLiveData<>();
BaseViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.eventBus = eventBus;
this.identityManager = identityManager;
this.notificationManager = notificationManager;
this.blogManager = blogManager;
eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
@DatabaseExecutor
protected List<BlogPostItem> loadBlogPosts(Transaction txn, GroupId groupId)
throws DbException {
long start = now();
List<BlogPostHeader> headers =
blogManager.getPostHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
List<BlogPostItem> items = new ArrayList<>(headers.size());
start = now();
for (BlogPostHeader h : headers) {
BlogPostItem item = getItem(txn, h);
items.add(item);
}
logDuration(LOG, "Loading bodies", start);
return items;
}
@DatabaseExecutor
protected BlogPostItem getItem(Transaction txn, BlogPostHeader h)
throws DbException {
String text;
if (h instanceof BlogCommentHeader) {
BlogCommentHeader c = (BlogCommentHeader) h;
BlogCommentItem item = new BlogCommentItem(c);
text = getPostText(txn, item.getPostHeader().getId());
item.setText(text);
return item;
} else {
text = getPostText(txn, h.getId());
return new BlogPostItem(h, text);
}
}
@DatabaseExecutor
private String getPostText(Transaction txn, MessageId m)
throws DbException {
return HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE);
}
LiveData<LiveResult<BlogPostItem>> loadBlogPost(GroupId g, MessageId m) {
MutableLiveData<LiveResult<BlogPostItem>> result =
new MutableLiveData<>();
runOnDbThread(true, txn -> {
long start = now();
BlogPostHeader header = blogManager.getPostHeader(txn, g, m);
BlogPostItem item = getItem(txn, header);
logDuration(LOG, "Loading post", start);
result.postValue(new LiveResult<>(item));
}, e -> {
logException(LOG, WARNING, e);
result.postValue(new LiveResult<>(e));
});
return result;
}
protected void onBlogPostAdded(BlogPostHeader header, boolean local) {
runOnDbThread(true, txn -> {
BlogPostItem item = getItem(txn, header);
txn.attach(() -> onBlogPostItemAdded(item, local));
}, this::handleException);
}
@UiThread
private void onBlogPostItemAdded(BlogPostItem item, boolean local) {
List<BlogPostItem> items = addListItem(getBlogPostItems(), item);
if (items != null) {
Collections.sort(items);
blogPosts.setValue(new LiveResult<>(new ListUpdate(local, items)));
}
}
void repeatPost(BlogPostItem item, @Nullable String comment) {
runOnDbThread(() -> {
try {
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
BlogPostHeader h = item.getHeader();
blogManager.addLocalComment(a, b.getId(), comment, h);
} catch (DbException e) {
handleException(e);
}
});
}
LiveData<LiveResult<ListUpdate>> getBlogPosts() {
return blogPosts;
}
@UiThread
@Nullable
protected List<BlogPostItem> getBlogPostItems() {
LiveResult<ListUpdate> value = blogPosts.getValue();
if (value == null) return null;
ListUpdate result = value.getResultOrNull();
return result == null ? null : result.getItems();
}
/**
* Call this after {@link ListUpdate#getPostAddedWasLocal()} was processed.
* This prevents it from getting processed again.
*/
@UiThread
void resetLocalUpdate() {
LiveResult<ListUpdate> value = blogPosts.getValue();
if (value == null) return;
ListUpdate result = value.getResultOrNull();
result.postAddedWasLocal = null;
}
static class ListUpdate {
@Nullable
private Boolean postAddedWasLocal;
private final List<BlogPostItem> items;
ListUpdate(@Nullable Boolean postAddedWasLocal,
List<BlogPostItem> items) {
this.postAddedWasLocal = postAddedWasLocal;
this.items = items;
}
/**
* @return null when not a single post was added with this update.
* true when a single post was added locally and false if remotely.
*/
@Nullable
public Boolean getPostAddedWasLocal() {
return postAddedWasLocal;
}
public List<BlogPostItem> getItems() {
return items;
}
}
}

View File

@@ -6,11 +6,9 @@ import android.os.Bundle;
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.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
@@ -18,10 +16,6 @@ import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -29,16 +23,7 @@ public class BlogActivity extends BriarActivity
implements BaseFragmentListener { implements BaseFragmentListener {
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; BlogController blogController;
private BlogViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(BlogViewModel.class);
}
@Override @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
@@ -46,46 +31,32 @@ public class BlogActivity extends BriarActivity
// GroupId from Intent // GroupId from Intent
Intent i = getIntent(); Intent i = getIntent();
GroupId groupId = byte[] b = i.getByteArrayExtra(GROUP_ID);
new GroupId(requireNonNull(i.getByteArrayExtra(GROUP_ID))); if (b == null) throw new IllegalStateException("No group ID in intent");
// Get post info from intent GroupId groupId = new GroupId(b);
@Nullable byte[] postId = i.getByteArrayExtra(POST_ID); blogController.setGroupId(groupId);
viewModel.setGroupId(groupId, postId == null);
setContentView(R.layout.activity_fragment_container_toolbar); setContentView(R.layout.activity_fragment_container_toolbar);
Toolbar toolbar = setUpCustomToolbar(false); Toolbar toolbar = setUpCustomToolbar(false);
// Open Sharing Status on Toolbar click // Open Sharing Status on Toolbar click
toolbar.setOnClickListener(v -> { if (toolbar != null) {
Intent i1 = new Intent(BlogActivity.this, toolbar.setOnClickListener(v -> {
BlogSharingStatusActivity.class); Intent i1 = new Intent(BlogActivity.this,
i1.putExtra(GROUP_ID, groupId.getBytes()); BlogSharingStatusActivity.class);
startActivity(i1); i1.putExtra(GROUP_ID, groupId.getBytes());
}); startActivity(i1);
});
viewModel.getBlog().observe(this, blog -> }
setTitle(blog.getBlog().getAuthor().getName())
);
viewModel.getSharingInfo().observe(this, info ->
setToolbarSubTitle(info.total, info.online)
);
if (state == null) { if (state == null) {
if (postId == null) { showInitialFragment(BlogFragment.newInstance(groupId));
showInitialFragment(BlogFragment.newInstance(groupId));
} else {
MessageId messageId = new MessageId(postId);
BaseFragment f =
BlogPostFragment.newInstance(groupId, messageId);
showInitialFragment(f);
}
} }
} }
private void setToolbarSubTitle(int total, int online) { @Override
requireNonNull(getSupportActionBar()) public void injectActivity(ActivityComponent component) {
.setSubtitle(getString(R.string.shared_with, total, online)); component.inject(this);
} }
} }

View File

@@ -8,9 +8,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import javax.annotation.concurrent.NotThreadSafe; // This class is not thread-safe
@NotThreadSafe
class BlogCommentItem extends BlogPostItem { class BlogCommentItem extends BlogPostItem {
private static final BlogCommentComparator COMPARATOR = private static final BlogCommentComparator COMPARATOR =

View File

@@ -0,0 +1,46 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import java.util.Collection;
import androidx.annotation.UiThread;
@NotNullByDefault
public interface BlogController extends BaseController {
void setGroupId(GroupId g);
@UiThread
void setBlogSharingListener(BlogSharingListener listener);
@UiThread
void unsetBlogSharingListener(BlogSharingListener listener);
void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlog(ResultExceptionHandler<BlogItem, DbException> handler);
void deleteBlog(ResultExceptionHandler<Void, DbException> handler);
void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
interface BlogSharingListener extends BlogListener {
@UiThread
void onBlogInvitationAccepted(ContactId c);
@UiThread
void onBlogLeft(ContactId c);
}
}

View File

@@ -1,24 +1,24 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.app.Application; import android.app.Activity;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
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.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.sharing.SharingController; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogInvitationResponse; import org.briarproject.briar.api.blog.BlogInvitationResponse;
@@ -35,54 +35,85 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.UiThread; import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class BlogViewModel extends BaseViewModel { class BlogControllerImpl extends BaseControllerImpl
implements ActivityLifecycleController, BlogController, EventListener {
private static final Logger LOG = getLogger(BlogViewModel.class.getName()); private static final Logger LOG =
Logger.getLogger(BlogControllerImpl.class.getName());
private final BlogSharingManager blogSharingManager; private final BlogSharingManager blogSharingManager;
private final SharingController sharingController;
private volatile GroupId groupId; // UI thread
@Nullable
private BlogSharingListener listener;
private final MutableLiveData<BlogItem> blog = new MutableLiveData<>(); private volatile GroupId groupId = null;
private final MutableLiveData<Boolean> blogRemoved =
new MutableLiveData<>();
@Inject @Inject
BlogViewModel(Application application, BlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
@DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, EventBus eventBus,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager, AndroidNotificationManager notificationManager,
BlogManager blogManager, IdentityManager identityManager, BlogManager blogManager,
BlogSharingManager blogSharingManager, BlogSharingManager blogSharingManager) {
SharingController sharingController) { super(dbExecutor, lifecycleManager, eventBus, notificationManager,
super(application, dbExecutor, lifecycleManager, db, androidExecutor, identityManager, blogManager);
eventBus, identityManager, notificationManager, blogManager);
this.blogSharingManager = blogSharingManager; this.blogSharingManager = blogSharingManager;
this.sharingController = sharingController; }
@Override
public void onActivityCreate(Activity activity) {
}
@Override
public void onActivityStart() {
super.onStart();
notificationManager.blockNotification(groupId);
notificationManager.clearBlogPostNotification(groupId);
}
@Override
public void onActivityStop() {
super.onStop();
notificationManager.unblockNotification(groupId);
}
@Override
public void onActivityDestroy() {
}
@Override
public void setGroupId(GroupId g) {
groupId = g;
}
@Override
public void setBlogSharingListener(BlogSharingListener listener) {
this.listener = listener;
}
@Override
public void unsetBlogSharingListener(BlogSharingListener listener) {
if (this.listener == listener) this.listener = null;
} }
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (groupId == null || listener == null)
throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) { if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e; BlogPostAddedEvent b = (BlogPostAddedEvent) e;
if (b.getGroupId().equals(groupId)) { if (b.getGroupId().equals(groupId)) {
LOG.info("Blog post added"); LOG.info("Blog post added");
onBlogPostAdded(b.getHeader(), b.isLocal()); listener.onBlogPostAdded(b.getHeader(), b.isLocal());
} }
} else if (e instanceof BlogInvitationResponseReceivedEvent) { } else if (e instanceof BlogInvitationResponseReceivedEvent) {
BlogInvitationResponseReceivedEvent b = BlogInvitationResponseReceivedEvent b =
@@ -90,36 +121,41 @@ class BlogViewModel extends BaseViewModel {
BlogInvitationResponse r = b.getMessageHeader(); BlogInvitationResponse r = b.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) { if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Blog invitation accepted"); LOG.info("Blog invitation accepted");
sharingController.add(b.getContactId()); listener.onBlogInvitationAccepted(b.getContactId());
} }
} else if (e instanceof ContactLeftShareableEvent) { } else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e; ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
if (s.getGroupId().equals(groupId)) { if (s.getGroupId().equals(groupId)) {
LOG.info("Blog left by contact"); LOG.info("Blog left by contact");
sharingController.remove(s.getContactId()); listener.onBlogLeft(s.getContactId());
} }
} else if (e instanceof GroupRemovedEvent) { } else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e; GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getId().equals(groupId)) { if (g.getGroup().getId().equals(groupId)) {
LOG.info("Blog removed"); LOG.info("Blog removed");
blogRemoved.setValue(true); listener.onBlogRemoved();
} }
} }
} }
/** @Override
* Set this before calling any other methods. public void loadBlogPosts(
*/ ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
@UiThread if (groupId == null) throw new IllegalStateException();
public void setGroupId(GroupId groupId, boolean loadAllPosts) { loadBlogPosts(groupId, handler);
if (this.groupId == groupId) return; // configuration change
this.groupId = groupId;
loadBlog(groupId);
if (loadAllPosts) loadBlogPosts(groupId);
loadSharingContacts(groupId);
} }
private void loadBlog(GroupId groupId) { @Override
public void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
loadBlogPost(groupId, m, handler);
}
@Override
public void loadBlog(
ResultExceptionHandler<BlogItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
long start = now(); long start = now();
@@ -127,65 +163,50 @@ class BlogViewModel extends BaseViewModel {
Blog b = blogManager.getBlog(groupId); Blog b = blogManager.getBlog(groupId);
boolean ours = a.getId().equals(b.getAuthor().getId()); boolean ours = a.getId().equals(b.getAuthor().getId());
boolean removable = blogManager.canBeRemoved(b); boolean removable = blogManager.canBeRemoved(b);
blog.postValue(new BlogItem(b, ours, removable)); BlogItem blog = new BlogItem(b, ours, removable);
logDuration(LOG, "Loading blog", start); logDuration(LOG, "Loading blog", start);
handler.onResult(blog);
} catch (DbException e) { } catch (DbException e) {
handleException(e); logException(LOG, WARNING, e);
handler.onException(e);
} }
}); });
} }
void blockAndClearNotifications() { @Override
notificationManager.blockNotification(groupId); public void deleteBlog(ResultExceptionHandler<Void, DbException> handler) {
notificationManager.clearBlogPostNotification(groupId); if (groupId == null) throw new IllegalStateException();
}
void unblockNotifications() {
notificationManager.unblockNotification(groupId);
}
private void loadBlogPosts(GroupId groupId) {
loadFromDb(txn -> new ListUpdate(null, loadBlogPosts(txn, groupId)),
blogPosts::setValue);
}
private void loadSharingContacts(GroupId groupId) {
runOnDbThread(true, txn -> {
Collection<Contact> contacts =
blogSharingManager.getSharedWith(txn, groupId);
txn.attach(() -> onSharingContactsLoaded(contacts));
}, this::handleException);
}
@UiThread
private void onSharingContactsLoaded(Collection<Contact> contacts) {
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
sharingController.addAll(contactIds);
}
void deleteBlog() {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
long start = now(); long start = now();
Blog b = blogManager.getBlog(groupId); Blog b = blogManager.getBlog(groupId);
blogManager.removeBlog(b); blogManager.removeBlog(b);
logDuration(LOG, "Removing blog", start); logDuration(LOG, "Removing blog", start);
handler.onResult(null);
} catch (DbException e) { } catch (DbException e) {
handleException(e); logException(LOG, WARNING, e);
handler.onException(e);
} }
}); });
} }
LiveData<BlogItem> getBlog() { @Override
return blog; public void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(() -> {
try {
Collection<Contact> contacts =
blogSharingManager.getSharedWith(groupId);
Collection<ContactId> contactIds =
new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
handler.onResult(contactIds);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
} }
LiveData<Boolean> getBlogRemoved() {
return blogRemoved;
}
LiveData<SharingInfo> getSharingInfo() {
return sharingController.getSharingInfo();
}
} }

View File

@@ -1,7 +1,9 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@@ -10,25 +12,33 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.Author;
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.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.BlogController.BlogSharingListener;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity; import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareBlogActivity; import org.briarproject.briar.android.sharing.ShareBlogActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.LayoutManager; import androidx.recyclerview.widget.RecyclerView.LayoutManager;
@@ -38,22 +48,31 @@ import static android.widget.Toast.LENGTH_SHORT;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
import static org.briarproject.briar.android.controller.SharingController.SharingListener;
@UiThread @UiThread
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class BlogFragment extends BaseFragment public class BlogFragment extends BaseFragment
implements OnBlogPostClickListener { implements BlogSharingListener, SharingListener,
OnBlogPostClickListener {
private final static String TAG = BlogFragment.class.getName(); private final static String TAG = BlogFragment.class.getName();
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; BlogController blogController;
@Inject
SharingController sharingController;
@Nullable
private Parcelable layoutManagerState;
private GroupId groupId; private GroupId groupId;
private BlogViewModel viewModel; private BlogPostAdapter adapter;
private final BlogPostAdapter adapter = new BlogPostAdapter(false, this); private LayoutManager layoutManager;
private BriarRecyclerView list; private BriarRecyclerView list;
private MenuItem writeButton, deleteButton;
private boolean isMyBlog = false, canDeleteBlog = false;
static BlogFragment newInstance(GroupId groupId) { static BlogFragment newInstance(GroupId groupId) {
BlogFragment f = new BlogFragment(); BlogFragment f = new BlogFragment();
@@ -68,8 +87,8 @@ public class BlogFragment extends BaseFragment
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) blogController.setBlogSharingListener(this);
.get(BlogViewModel.class); sharingController.setSharingListener(this);
} }
@Nullable @Nullable
@@ -84,82 +103,106 @@ public class BlogFragment extends BaseFragment
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(requireActivity(), this,
getFragmentManager());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);
LayoutManager layoutManager = new LinearLayoutManager(getActivity()); layoutManager = new LinearLayoutManager(getActivity());
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
list.showProgressBar(); list.showProgressBar();
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state)); list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> if (savedInstanceState != null) {
result.onError(this::handleException) layoutManagerState =
.onSuccess(this::onBlogPostsLoaded) savedInstanceState.getParcelable("layoutManager");
); }
viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> {
if (removed) finish();
});
return v; return v;
} }
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
viewModel.blockAndClearNotifications(); sharingController.onStart();
loadBlog();
loadSharedContacts();
loadBlogPosts(false);
list.startPeriodicUpdate(); list.startPeriodicUpdate();
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
viewModel.unblockNotifications(); sharingController.onStop();
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
} }
@Override
public void onDestroy() {
super.onDestroy();
blogController.unsetBlogSharingListener(this);
sharingController.unsetSharingListener(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (layoutManager != null) {
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.blogs_blog_actions, menu); inflater.inflate(R.menu.blogs_blog_actions, menu);
MenuItem writeButton = menu.findItem(R.id.action_write_blog_post); writeButton = menu.findItem(R.id.action_write_blog_post);
MenuItem deleteButton = menu.findItem(R.id.action_blog_delete); if (isMyBlog) writeButton.setVisible(true);
viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> { deleteButton = menu.findItem(R.id.action_blog_delete);
if (blog.isOurs()) writeButton.setVisible(true); if (canDeleteBlog) deleteButton.setEnabled(true);
if (blog.canBeRemoved()) deleteButton.setEnabled(true);
});
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId(); switch (item.getItemId()) {
if (itemId == R.id.action_write_blog_post) { case R.id.action_write_blog_post:
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class); Intent i = new Intent(getActivity(),
i.putExtra(GROUP_ID, groupId.getBytes()); WriteBlogPostActivity.class);
startActivity(i); i.putExtra(GROUP_ID, groupId.getBytes());
return true; startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
} else if (itemId == R.id.action_blog_share) { return true;
Intent i = new Intent(getActivity(), ShareBlogActivity.class); case R.id.action_blog_share:
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes()); i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResult(i, REQUEST_SHARE_BLOG); i2.putExtra(GROUP_ID, groupId.getBytes());
return true; startActivityForResult(i2, REQUEST_SHARE_BLOG);
} else if (itemId == R.id.action_blog_sharing_status) { return true;
Intent i = case R.id.action_blog_sharing_status:
new Intent(getActivity(), BlogSharingStatusActivity.class); Intent i3 = new Intent(getActivity(),
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); BlogSharingStatusActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes()); i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i); i3.putExtra(GROUP_ID, groupId.getBytes());
return true; startActivity(i3);
} else if (itemId == R.id.action_blog_delete) { return true;
showDeleteDialog(); case R.id.action_blog_delete:
return true; showDeleteDialog();
return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override
public void onActivityResult(int request, int result, public void onActivityResult(int request, int result,
@Nullable Intent data) { @Nullable Intent data) {
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) {
displaySnackbar(R.string.blogs_blog_post_created, true);
loadBlogPosts(true);
} else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
displaySnackbar(R.string.blogs_sharing_snackbar, false); displaySnackbar(R.string.blogs_sharing_snackbar, false);
} }
} }
@@ -169,26 +212,35 @@ public class BlogFragment extends BaseFragment
return TAG; return TAG;
} }
private void onBlogPostsLoaded(ListUpdate update) { @Override
adapter.submitList(update.getItems(), () -> { public void onBlogPostAdded(BlogPostHeader header, boolean local) {
Boolean wasLocal = update.getPostAddedWasLocal(); blogController.loadBlogPost(header,
if (wasLocal != null && wasLocal) { new UiResultExceptionHandler<BlogPostItem, DbException>(
list.scrollToPosition(0); this) {
displaySnackbar(R.string.blogs_blog_post_created, @Override
false); public void onResultUi(BlogPostItem post) {
} else if (wasLocal != null) { adapter.add(post);
displaySnackbar(R.string.blogs_blog_post_received, if (local) {
true); list.scrollToPosition(0);
} displaySnackbar(R.string.blogs_blog_post_created,
viewModel.resetLocalUpdate(); false);
list.showData(); } else {
}); displaySnackbar(R.string.blogs_blog_post_received,
true);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
} }
@Override @Override
public void onBlogPostClick(BlogPostItem post) { public void onBlogPostClick(BlogPostItem post) {
BlogPostFragment f = BlogPostFragment f = BlogPostFragment.newInstance(post.getId());
BlogPostFragment.newInstance(groupId, post.getId());
showNextFragment(f); showNextFragment(f);
} }
@@ -204,10 +256,111 @@ public class BlogFragment extends BaseFragment
getContext().startActivity(i); getContext().startActivity(i);
} }
private void loadBlogPosts(boolean reload) {
blogController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>,
DbException>(this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
if (posts.isEmpty()) {
list.showData();
} else {
adapter.addAll(posts);
if (reload || layoutManagerState == null) {
list.scrollToPosition(0);
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void loadBlog() {
blogController.loadBlog(
new UiResultExceptionHandler<BlogItem, DbException>(this) {
@Override
public void onResultUi(BlogItem blog) {
setToolbarTitle(blog.getBlog().getAuthor());
if (blog.isOurs())
showWriteButton();
if (blog.canBeRemoved())
enableDeleteButton();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void setToolbarTitle(Author a) {
getActivity().setTitle(a.getName());
}
private void loadSharedContacts() {
blogController.loadSharingContacts(
new UiResultExceptionHandler<Collection<ContactId>,
DbException>(this) {
@Override
public void onResultUi(Collection<ContactId> contacts) {
sharingController.addAll(contacts);
int online = sharingController.getOnlineCount();
setToolbarSubTitle(contacts.size(), online);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override @Override
public void onLinkClick(String url) { public void onBlogInvitationAccepted(ContactId c) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url); sharingController.add(c);
f.show(getParentFragmentManager(), f.getUniqueTag()); setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onBlogLeft(ContactId c) {
sharingController.remove(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onSharingInfoUpdated(int total, int online) {
setToolbarSubTitle(total, online);
}
private void setToolbarSubTitle(int total, int online) {
ActionBar actionBar =
((BriarActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(
getString(R.string.shared_with, total, online));
}
}
private void showWriteButton() {
isMyBlog = true;
if (writeButton != null)
writeButton.setVisible(true);
}
private void enableDeleteButton() {
canDeleteBlog = true;
if (deleteButton != null)
deleteButton.setEnabled(true);
} }
private void displaySnackbar(int stringId, boolean scroll) { private void displaySnackbar(int stringId, boolean scroll) {
@@ -220,21 +373,38 @@ public class BlogFragment extends BaseFragment
} }
private void showDeleteDialog() { private void showDeleteDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(), DialogInterface.OnClickListener okListener =
(dialog, which) -> deleteBlog();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
R.style.BriarDialogTheme); R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_remove_blog)); builder.setTitle(getString(R.string.blogs_remove_blog));
builder.setMessage( builder.setMessage(
getString(R.string.blogs_remove_blog_dialog_message)); getString(R.string.blogs_remove_blog_dialog_message));
builder.setPositiveButton(R.string.cancel, null); builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_remove_blog_ok, builder.setNegativeButton(R.string.blogs_remove_blog_ok, okListener);
(dialog, which) -> deleteBlog());
builder.show(); builder.show();
} }
private void deleteBlog() { private void deleteBlog() {
viewModel.deleteBlog(); blogController.deleteBlog(
Toast.makeText(getActivity(), R.string.blogs_blog_removed, LENGTH_SHORT) new UiResultExceptionHandler<Void, DbException>(this) {
.show(); @Override
public void onResultUi(Void result) {
Toast.makeText(getActivity(),
R.string.blogs_blog_removed, LENGTH_SHORT)
.show();
finish();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onBlogRemoved() {
finish(); finish();
} }

View File

@@ -1,23 +1,35 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import org.briarproject.briar.android.viewmodel.ViewModelKey; import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.SharingControllerImpl;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.multibindings.IntoMap; import dagger.Provides;
@Module @Module
public interface BlogModule { public class BlogModule {
@Binds @ActivityScope
@IntoMap @Provides
@ViewModelKey(FeedViewModel.class) BlogController provideBlogController(BaseActivity activity,
ViewModel bindFeedViewModel(FeedViewModel feedViewModel); BlogControllerImpl blogController) {
activity.addLifecycleController(blogController);
return blogController;
}
@Binds @ActivityScope
@IntoMap @Provides
@ViewModelKey(BlogViewModel.class) FeedController provideFeedController(FeedControllerImpl feedController) {
ViewModel bindBlogViewModel(BlogViewModel blogViewModel); return feedController;
}
@ActivityScope
@Provides
SharingController provideSharingController(
SharingControllerImpl sharingController) {
return sharingController;
}
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -7,44 +8,52 @@ import android.view.ViewGroup;
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.util.BriarAdapter;
import androidx.recyclerview.widget.DiffUtil; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ListAdapter; import androidx.fragment.app.FragmentManager;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class BlogPostAdapter extends ListAdapter<BlogPostItem, BlogPostViewHolder> { class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
private final boolean authorClickable;
private final OnBlogPostClickListener listener; private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostAdapter(boolean authorClickable, OnBlogPostClickListener listener) { BlogPostAdapter(Context ctx, OnBlogPostClickListener listener,
super(new DiffUtil.ItemCallback<BlogPostItem>() { @Nullable FragmentManager fragmentManager) {
@Override super(ctx, BlogPostItem.class);
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
}
@Override
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
});
this.authorClickable = authorClickable;
this.listener = listener; this.listener = listener;
this.fragmentManager = fragmentManager;
} }
@Override @Override
public BlogPostViewHolder onCreateViewHolder(ViewGroup parent, public BlogPostViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) { int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate( View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_blog_post, parent, false); R.layout.list_item_blog_post, parent, false);
return new BlogPostViewHolder(v, false, listener, authorClickable); return new BlogPostViewHolder(v, false, listener, fragmentManager);
} }
@Override @Override
public void onBindViewHolder(BlogPostViewHolder ui, int position) { public void onBindViewHolder(BlogPostViewHolder ui, int position) {
ui.bindItem(getItem(position)); ui.bindItem(getItemAt(position));
}
@Override
public int compare(BlogPostItem a, BlogPostItem b) {
return a.compareTo(b);
}
@Override
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
@Override
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
} }
} }

View File

@@ -1,159 +1,76 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.db.DbException;
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.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.blog.BaseController.BlogListener;
import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@UiThread
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class BlogPostFragment extends BaseFragment public class BlogPostFragment extends BasePostFragment implements BlogListener {
implements OnBlogPostClickListener {
private static final String TAG = BlogPostFragment.class.getName(); private static final String TAG = BlogPostFragment.class.getName();
private static final Logger LOG = getLogger(TAG);
static final String POST_ID = "briar.POST_ID";
protected BlogViewModel viewModel;
private final Handler handler = new Handler(Looper.getMainLooper());
private ProgressBar progressBar;
private BlogPostViewHolder ui;
private BlogPostItem post;
private Runnable refresher;
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; BlogController blogController;
static BlogPostFragment newInstance(GroupId blogId, MessageId postId) { static BlogPostFragment newInstance(MessageId postId) {
BlogPostFragment f = new BlogPostFragment(); BlogPostFragment f = new BlogPostFragment();
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, blogId.getBytes());
bundle.putByteArray(POST_ID, postId.getBytes()); bundle.putByteArray(POST_ID, postId.getBytes());
f.setArguments(bundle); f.setArguments(bundle);
return f; return f;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireArguments();
GroupId groupId =
new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
MessageId postId =
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
View view = inflater.inflate(R.layout.fragment_blog_post, container,
false);
progressBar = view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
ui = new BlogPostViewHolder(view, true, this, false);
LifecycleOwner owner = getViewLifecycleOwner();
viewModel.loadBlogPost(groupId, postId).observe(owner, result ->
result.onError(this::handleException)
.onSuccess(this::onBlogPostLoaded)
);
return view;
}
@Override
public void onStart() {
super.onStart();
startPeriodicUpdate();
}
@Override
public void onStop() {
super.onStop();
stopPeriodicUpdate();
}
@UiThread
private void onBlogPostLoaded(BlogPostItem post) {
progressBar.setVisibility(INVISIBLE);
this.post = post;
ui.bindItem(post);
}
@Override
public void onBlogPostClick(BlogPostItem post) {
// We're already there
}
@Override
public void onAuthorClick(BlogPostItem post) {
Intent i = new Intent(requireContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
requireContext().startActivity(i);
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
}
private void startPeriodicUpdate() {
refresher = () -> {
LOG.info("Updating Content...");
ui.updateDate(post.getTimestamp());
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
};
LOG.info("Adding Handler Callback");
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
private void stopPeriodicUpdate() {
if (refresher != null) {
LOG.info("Removing Handler Callback");
handler.removeCallbacks(refresher);
}
}
@Override @Override
public String getUniqueTag() { public String getUniqueTag() {
return TAG; return TAG;
} }
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();
blogController.loadBlogPost(postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
// doesn't matter here
}
@Override
public void onBlogRemoved() {
finish();
}
} }

View File

@@ -17,7 +17,7 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
private final BlogPostHeader header; private final BlogPostHeader header;
@Nullable @Nullable
protected String text; protected String text;
private final boolean read; private boolean read;
BlogPostItem(BlogPostHeader header, @Nullable String text) { BlogPostItem(BlogPostHeader header, @Nullable String text) {
this.header = header; this.header = header;
@@ -74,6 +74,9 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
protected static int compare(BlogPostHeader h1, BlogPostHeader h2) { protected static int compare(BlogPostHeader h1, BlogPostHeader h2) {
// The newest post comes first // The newest post comes first
return Long.compare(h2.getTimeReceived(), h1.getTimeReceived()); long aTime = h1.getTimeReceived(), bTime = h2.getTimeReceived();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
return 0;
} }
} }

View File

@@ -9,31 +9,31 @@ import android.view.ViewGroup;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.view.AuthorView; import org.briarproject.briar.android.view.AuthorView;
import org.briarproject.briar.api.blog.BlogCommentHeader; import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogPostHeader; import org.briarproject.briar.api.blog.BlogPostHeader;
import javax.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH; import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.getSpanned; import static org.briarproject.briar.android.util.UiUtils.getSpanned;
import static org.briarproject.briar.android.util.UiUtils.getTeaser; import static org.briarproject.briar.android.util.UiUtils.getTeaser;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable; import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.briar.android.view.AuthorView.COMMENTER; import static org.briarproject.briar.api.blog.MessageType.POST;
import static org.briarproject.briar.android.view.AuthorView.REBLOGGER;
import static org.briarproject.briar.android.view.AuthorView.RSS_FEED_REBLOGGED;
@UiThread @UiThread
@NotNullByDefault
class BlogPostViewHolder extends RecyclerView.ViewHolder { class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final Context ctx; private final Context ctx;
@@ -43,16 +43,20 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final ImageButton reblogButton; private final ImageButton reblogButton;
private final TextView text; private final TextView text;
private final ViewGroup commentContainer; private final ViewGroup commentContainer;
private final boolean fullText, authorClickable; private final boolean fullText;
@NonNull
private final OnBlogPostClickListener listener; private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostViewHolder(View v, boolean fullText, BlogPostViewHolder(View v, boolean fullText,
OnBlogPostClickListener listener, boolean authorClickable) { @NonNull OnBlogPostClickListener listener,
@Nullable FragmentManager fragmentManager) {
super(v); super(v);
this.fullText = fullText; this.fullText = fullText;
this.listener = listener; this.listener = listener;
this.authorClickable = authorClickable; this.fragmentManager = fragmentManager;
ctx = v.getContext(); ctx = v.getContext();
layout = v.findViewById(R.id.postLayout); layout = v.findViewById(R.id.postLayout);
@@ -63,6 +67,10 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
commentContainer = v.findViewById(R.id.commentContainer); commentContainer = v.findViewById(R.id.commentContainer);
} }
void setVisibility(int visibility) {
layout.setVisibility(visibility);
}
void hideReblogButton() { void hideReblogButton() {
reblogButton.setVisibility(GONE); reblogButton.setVisibility(GONE);
} }
@@ -79,15 +87,15 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
return "blogPost" + id.hashCode(); return "blogPost" + id.hashCode();
} }
void bindItem(BlogPostItem item) { void bindItem(@Nullable BlogPostItem item) {
if (item == null) return;
setTransitionName(item.getId()); setTransitionName(item.getId());
if (!fullText) { if (!fullText) {
layout.setClickable(true); layout.setClickable(true);
layout.setOnClickListener(v -> listener.onBlogPostClick(item)); layout.setOnClickListener(v -> listener.onBlogPostClick(item));
} }
boolean isReblog = item instanceof BlogCommentItem;
// author and date // author and date
BlogPostHeader post = item.getPostHeader(); BlogPostHeader post = item.getPostHeader();
author.setAuthor(post.getAuthor(), post.getAuthorInfo()); author.setAuthor(post.getAuthor(), post.getAuthorInfo());
@@ -95,7 +103,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
author.setPersona( author.setPersona(
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL); item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
// TODO make author clickable more often #624 // TODO make author clickable more often #624
if (authorClickable && !isReblog) { if (!fullText && item.getHeader().getType() == POST) {
author.setAuthorClickable(v -> listener.onAuthorClick(item)); author.setAuthorClickable(v -> listener.onAuthorClick(item));
} else { } else {
author.setAuthorNotClickable(); author.setAuthorNotClickable();
@@ -106,7 +114,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
if (fullText) { if (fullText) {
text.setText(postText); text.setText(postText);
text.setTextIsSelectable(true); text.setTextIsSelectable(true);
makeLinksClickable(text, listener::onLinkClick); makeLinksClickable(text, fragmentManager);
} else { } else {
text.setTextIsSelectable(false); text.setTextIsSelectable(false);
if (postText.length() > TEASER_LENGTH) if (postText.length() > TEASER_LENGTH)
@@ -124,33 +132,32 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
// comments // comments
commentContainer.removeAllViews(); commentContainer.removeAllViews();
if (isReblog) { if (item instanceof BlogCommentItem) {
onBindComment((BlogCommentItem) item, authorClickable); onBindComment((BlogCommentItem) item);
} else { } else {
reblogger.setVisibility(GONE); reblogger.setVisibility(GONE);
} }
} }
private void onBindComment(BlogCommentItem item, boolean authorClickable) { private void onBindComment(BlogCommentItem item) {
// reblogger // reblogger
reblogger.setAuthor(item.getAuthor(), item.getAuthorInfo()); reblogger.setAuthor(item.getAuthor(), item.getAuthorInfo());
reblogger.setDate(item.getTimestamp()); reblogger.setDate(item.getTimestamp());
if (authorClickable) { if (!fullText) {
reblogger.setAuthorClickable(v -> listener.onAuthorClick(item)); reblogger.setAuthorClickable(v -> listener.onAuthorClick(item));
} else {
reblogger.setAuthorNotClickable();
} }
reblogger.setVisibility(VISIBLE); reblogger.setVisibility(VISIBLE);
reblogger.setPersona(REBLOGGER); reblogger.setPersona(AuthorView.REBLOGGER);
author.setPersona(item.getHeader().getRootPost().isRssFeed() ? author.setPersona(item.getHeader().getRootPost().isRssFeed() ?
RSS_FEED_REBLOGGED : COMMENTER); AuthorView.RSS_FEED_REBLOGGED :
AuthorView.COMMENTER);
// comments // comments
// TODO use nested RecyclerView instead like we do for Image Attachments
for (BlogCommentHeader c : item.getComments()) { for (BlogCommentHeader c : item.getComments()) {
View v = LayoutInflater.from(ctx).inflate( View v = LayoutInflater.from(ctx)
R.layout.list_item_blog_comment, commentContainer, false); .inflate(R.layout.list_item_blog_comment,
commentContainer, false);
AuthorView author = v.findViewById(R.id.authorView); AuthorView author = v.findViewById(R.id.authorView);
TextView text = v.findViewById(R.id.textView); TextView text = v.findViewById(R.id.textView);

View File

@@ -0,0 +1,32 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.blog.Blog;
import java.util.Collection;
import androidx.annotation.UiThread;
@NotNullByDefault
public interface FeedController extends BaseController {
void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadPersonalBlog(ResultExceptionHandler<Blog, DbException> handler);
@UiThread
void setFeedListener(FeedListener listener);
@UiThread
void unsetFeedListener(FeedListener listener);
@NotNullByDefault
interface FeedListener extends BlogListener {
@UiThread
void onBlogAdded();
}
}

View File

@@ -0,0 +1,143 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class FeedControllerImpl extends BaseControllerImpl implements FeedController {
private static final Logger LOG =
Logger.getLogger(FeedControllerImpl.class.getName());
// UI thread
@Nullable
private FeedListener listener;
@Inject
FeedControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
AndroidNotificationManager notificationManager,
IdentityManager identityManager, BlogManager blogManager) {
super(dbExecutor, lifecycleManager, eventBus, notificationManager,
identityManager, blogManager);
}
@Override
public void onStart() {
super.onStart();
if (listener == null) throw new IllegalStateException();
notificationManager.blockAllBlogPostNotifications();
notificationManager.clearAllBlogPostNotifications();
}
@Override
public void onStop() {
super.onStop();
notificationManager.unblockAllBlogPostNotifications();
}
@Override
public void setFeedListener(FeedListener listener) {
this.listener = listener;
}
@Override
public void unsetFeedListener(FeedListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void eventOccurred(Event e) {
if (listener == null) throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
LOG.info("Blog post added");
listener.onBlogPostAdded(b.getHeader(), b.isLocal());
} else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog added");
listener.onBlogAdded();
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog removed");
listener.onBlogRemoved();
}
}
}
@Override
public void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Collection<BlogPostItem> posts = new ArrayList<>();
for (Blog b : blogManager.getBlogs()) {
try {
posts.addAll(loadItems(b.getId()));
} catch (NoSuchGroupException | NoSuchMessageException e) {
logException(LOG, WARNING, e);
}
}
logDuration(LOG, "Loading all posts", start);
handler.onResult(posts);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void loadPersonalBlog(
ResultExceptionHandler<Blog, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Author a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
logDuration(LOG, "Loading personal blog", start);
handler.onResult(b);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.blog;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@@ -9,43 +10,53 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
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.bramble.api.sync.GroupId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate; import org.briarproject.briar.android.blog.FeedController.FeedListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.blog.Blog; import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider; import androidx.annotation.UiThread;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
@UiThread
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class FeedFragment extends BaseFragment public class FeedFragment extends BaseFragment implements
implements OnBlogPostClickListener { OnBlogPostClickListener, FeedListener {
public final static String TAG = FeedFragment.class.getName(); public final static String TAG = FeedFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; FeedController feedController;
private FeedViewModel viewModel; private BlogPostAdapter adapter;
private final BlogPostAdapter adapter = new BlogPostAdapter(true, this);
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
private BriarRecyclerView list; private BriarRecyclerView list;
@Nullable
private Blog personalBlog;
@Nullable
private Parcelable layoutManagerState;
public static FeedFragment newInstance() { public static FeedFragment newInstance() {
FeedFragment f = new FeedFragment(); FeedFragment f = new FeedFragment();
@@ -59,8 +70,7 @@ public class FeedFragment extends BaseFragment
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory) feedController.setFeedListener(this);
.get(FeedViewModel.class);
} }
@Nullable @Nullable
@@ -72,6 +82,9 @@ public class FeedFragment extends BaseFragment
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
layoutManager = new LinearLayoutManager(getActivity()); layoutManager = new LinearLayoutManager(getActivity());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
@@ -80,39 +93,103 @@ public class FeedFragment extends BaseFragment
list.setEmptyText(R.string.blogs_feed_empty_state); list.setEmptyText(R.string.blogs_feed_empty_state);
list.setEmptyAction(R.string.blogs_feed_empty_state_action); list.setEmptyAction(R.string.blogs_feed_empty_state_action);
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result -> if (savedInstanceState != null) {
result.onError(this::handleException) layoutManagerState =
.onSuccess(this::onBlogPostsLoaded) savedInstanceState.getParcelable("layoutManager");
); }
return v; return v;
} }
@Override
public void onActivityResult(int requestCode, int resultCode,
@Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// The BlogPostAddedEvent arrives when the controller is not listening
if (requestCode == REQUEST_WRITE_BLOG_POST && resultCode == RESULT_OK) {
showSnackBar(R.string.blogs_blog_post_created);
}
}
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
viewModel.blockAndClearAllBlogPostNotifications(); feedController.onStart();
list.startPeriodicUpdate(); list.startPeriodicUpdate();
loadPersonalBlog();
loadBlogPosts(false);
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
viewModel.unblockAllBlogPostNotifications(); feedController.onStop();
adapter.clear();
list.showProgressBar();
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
// TODO save list position in database/preferences?
} }
private void onBlogPostsLoaded(ListUpdate update) { @Override
adapter.submitList(update.getItems(), () -> { public void onDestroy() {
Boolean wasLocal = update.getPostAddedWasLocal(); super.onDestroy();
if (wasLocal != null && wasLocal) { feedController.unsetFeedListener(this);
showSnackBar(R.string.blogs_blog_post_created); }
} else if (wasLocal != null) {
showSnackBar(R.string.blogs_blog_post_received); @Override
} public void onSaveInstanceState(Bundle outState) {
viewModel.resetLocalUpdate(); super.onSaveInstanceState(outState);
list.showData(); if (layoutManager != null) {
}); layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
private void loadPersonalBlog() {
feedController.loadPersonalBlog(
new UiResultExceptionHandler<Blog, DbException>(this) {
@Override
public void onResultUi(Blog b) {
personalBlog = b;
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void loadBlogPosts(boolean clear) {
int revision = adapter.getRevision();
feedController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (clear) adapter.setItems(posts);
else adapter.addAll(posts);
if (posts.isEmpty()) list.showData();
if (layoutManagerState == null) {
list.scrollToPosition(0); // Scroll to the top
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
} else {
LOG.info("Concurrent update, reloading");
loadBlogPosts(clear);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
} }
@Override @Override
@@ -123,46 +200,67 @@ public class FeedFragment extends BaseFragment
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId(); if (personalBlog == null) return false;
if (itemId == R.id.action_write_blog_post) { switch (item.getItemId()) {
Blog personalBlog = viewModel.getPersonalBlog().getValue(); case R.id.action_write_blog_post:
if (personalBlog == null) return false; Intent i1 =
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class); new Intent(getActivity(), WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes()); i1.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i); startActivityForResult(i1, REQUEST_WRITE_BLOG_POST);
return true; return true;
} else if (itemId == R.id.action_rss_feeds_import) { case R.id.action_rss_feeds_import:
Intent i = new Intent(getActivity(), RssFeedImportActivity.class); Intent i2 =
startActivity(i); new Intent(getActivity(), RssFeedImportActivity.class);
return true; startActivity(i2);
} else if (itemId == R.id.action_rss_feeds_manage) { return true;
Blog personalBlog = viewModel.getPersonalBlog().getValue(); case R.id.action_rss_feeds_manage:
if (personalBlog == null) return false; Intent i3 =
Intent i = new Intent(getActivity(), RssFeedManageActivity.class); new Intent(getActivity(), RssFeedManageActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes()); i3.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i); startActivity(i3);
return true; return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item); }
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
feedController.loadBlogPost(header,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
adapter.incrementRevision();
adapter.add(post);
if (local) {
showSnackBar(R.string.blogs_blog_post_created);
} else {
showSnackBar(R.string.blogs_blog_post_received);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
} }
@Override @Override
public void onBlogPostClick(BlogPostItem post) { public void onBlogPostClick(BlogPostItem post) {
Intent i = getBlogActivityIntent(post.getGroupId()); FeedPostFragment f =
i.putExtra(POST_ID, post.getId().getBytes()); FeedPostFragment.newInstance(post.getGroupId(), post.getId());
requireContext().startActivity(i); showNextFragment(f);
} }
@Override @Override
public void onAuthorClick(BlogPostItem post) { public void onAuthorClick(BlogPostItem post) {
Intent i = getBlogActivityIntent(post.getGroupId()); Intent i = new Intent(getContext(), BlogActivity.class);
requireContext().startActivity(i); i.putExtra(GROUP_ID, post.getGroupId().getBytes());
} i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
getContext().startActivity(i);
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
} }
@Override @Override
@@ -170,13 +268,6 @@ public class FeedFragment extends BaseFragment
return TAG; return TAG;
} }
private Intent getBlogActivityIntent(GroupId groupId) {
Intent i = new Intent(requireContext(), BlogActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
return i;
}
private void showSnackBar(int stringRes) { private void showSnackBar(int stringRes) {
int firstVisible = int firstVisible =
layoutManager.findFirstCompletelyVisibleItemPosition(); layoutManager.findFirstCompletelyVisibleItemPosition();
@@ -192,4 +283,14 @@ public class FeedFragment extends BaseFragment
sb.make(list, stringRes, LENGTH_LONG).show(); sb.make(list, stringRes, LENGTH_LONG).show();
} }
@Override
public void onBlogAdded() {
loadBlogPosts(false);
}
@Override
public void onBlogRemoved() {
loadBlogPosts(true);
}
} }

View File

@@ -0,0 +1,87 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FeedPostFragment extends BasePostFragment {
private static final String TAG = FeedPostFragment.class.getName();
private GroupId blogId;
@Inject
FeedController feedController;
static FeedPostFragment newInstance(GroupId blogId, MessageId postId) {
FeedPostFragment f = new FeedPostFragment();
Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, blogId.getBytes());
bundle.putByteArray(POST_ID, postId.getBytes());
f.setArguments(bundle);
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args");
blogId = new GroupId(b);
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onStart() {
super.onStart();
feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
}

View File

@@ -1,133 +0,0 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
@NotNullByDefault
class FeedViewModel extends BaseViewModel {
private static final Logger LOG = getLogger(FeedViewModel.class.getName());
private final MutableLiveData<Blog> personalBlog = new MutableLiveData<>();
@Inject
FeedViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, notificationManager, blogManager);
loadPersonalBlog();
loadAllBlogPosts();
}
@Override
public void eventOccurred(Event e) {
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
LOG.info("Blog post added");
onBlogPostAdded(b.getHeader(), b.isLocal());
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog removed");
onBlogRemoved(g.getGroup().getId());
}
}
}
void blockAndClearAllBlogPostNotifications() {
notificationManager.blockAllBlogPostNotifications();
notificationManager.clearAllBlogPostNotifications();
}
void unblockAllBlogPostNotifications() {
notificationManager.unblockAllBlogPostNotifications();
}
private void loadPersonalBlog() {
runOnDbThread(() -> {
try {
long start = now();
Author a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
logDuration(LOG, "Loading personal blog", start);
personalBlog.postValue(b);
} catch (DbException e) {
handleException(e);
}
});
}
LiveData<Blog> getPersonalBlog() {
return personalBlog;
}
private void loadAllBlogPosts() {
loadFromDb(this::loadAllBlogPosts, blogPosts::setValue);
}
@DatabaseExecutor
private ListUpdate loadAllBlogPosts(Transaction txn)
throws DbException {
long start = now();
List<BlogPostItem> posts = new ArrayList<>();
for (GroupId g : blogManager.getBlogIds(txn)) {
posts.addAll(loadBlogPosts(txn, g));
}
Collections.sort(posts);
logDuration(LOG, "Loading all posts", start);
return new ListUpdate(null, posts);
}
@UiThread
private void onBlogRemoved(GroupId g) {
List<BlogPostItem> items = removeListItems(getBlogPostItems(), item ->
item.getGroupId().equals(g)
);
if (items != null) {
blogPosts.setValue(new LiveResult<>(new ListUpdate(null, items)));
}
}
}

View File

@@ -5,6 +5,4 @@ interface OnBlogPostClickListener {
void onBlogPostClick(BlogPostItem post); void onBlogPostClick(BlogPostItem post);
void onAuthorClick(BlogPostItem post); void onAuthorClick(BlogPostItem post);
void onLinkClick(String url);
} }

View File

@@ -11,7 +11,7 @@ import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
public class ReblogActivity extends BriarActivity implements public class ReblogActivity extends BriarActivity implements
BaseFragmentListener { BaseFragmentListener {
@@ -39,11 +39,13 @@ public class ReblogActivity extends BriarActivity implements
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { switch (item.getItemId()) {
onBackPressed(); case android.R.id.home:
return true; onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override

View File

@@ -7,17 +7,20 @@ import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.ScrollView; import android.widget.ScrollView;
import org.briarproject.bramble.api.db.DbException;
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.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.widget.LinkDialogFragment; import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List; import java.util.List;
@@ -25,7 +28,8 @@ import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.view.View.FOCUS_DOWN; import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE; import static android.view.View.GONE;
@@ -33,7 +37,8 @@ import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID; import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -42,13 +47,12 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public static final String TAG = ReblogFragment.class.getName(); public static final String TAG = ReblogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private BlogViewModel viewModel;
private ViewHolder ui; private ViewHolder ui;
private BlogPostItem item; private BlogPostItem item;
@Inject
FeedController feedController;
static ReblogFragment newInstance(GroupId groupId, MessageId messageId) { static ReblogFragment newInstance(GroupId groupId, MessageId messageId) {
ReblogFragment f = new ReblogFragment(); ReblogFragment f = new ReblogFragment();
@@ -68,8 +72,6 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
} }
@Override @Override
@@ -93,20 +95,30 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH); ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
showProgressBar(); showProgressBar();
viewModel.loadBlogPost(blogId, postId).observe(getViewLifecycleOwner(), feedController.loadBlogPost(blogId, postId,
result -> result.onError(this::handleException) new UiResultExceptionHandler<BlogPostItem, DbException>(
.onSuccess(this::bindViewHolder) this) {
); @Override
public void onResultUi(BlogPostItem result) {
item = result;
bindViewHolder();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
return v; return v;
} }
private void bindViewHolder(BlogPostItem item) { private void bindViewHolder() {
this.item = item; if (item == null) return;
hideProgressBar(); hideProgressBar();
ui.post.bindItem(this.item); ui.post.bindItem(item);
ui.post.hideReblogButton(); ui.post.hideReblogButton();
ui.input.setReady(true); ui.input.setReady(true);
@@ -114,11 +126,18 @@ public class ReblogFragment extends BaseFragment implements SendListener {
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
ui.input.hideSoftKeyboard(); ui.input.hideSoftKeyboard();
viewModel.repeatPost(item, text); feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
finish(); finish();
return new MutableLiveData<>(SENT);
} }
private void showProgressBar() { private void showProgressBar() {
@@ -131,7 +150,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setVisibility(VISIBLE); ui.input.setVisibility(VISIBLE);
} }
private class ViewHolder implements OnBlogPostClickListener { private class ViewHolder {
private final ScrollView scrollView; private final ScrollView scrollView;
private final ProgressBar progressBar; private final ProgressBar progressBar;
@@ -142,25 +161,18 @@ public class ReblogFragment extends BaseFragment implements SendListener {
scrollView = v.findViewById(R.id.scrollView); scrollView = v.findViewById(R.id.scrollView);
progressBar = v.findViewById(R.id.progressBar); progressBar = v.findViewById(R.id.progressBar);
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout), post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
true, this, false); true, new OnBlogPostClickListener() {
@Override
public void onBlogPostClick(BlogPostItem post) {
// do nothing
}
@Override
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
}, getFragmentManager());
input = v.findViewById(R.id.inputText); input = v.findViewById(R.id.inputText);
} }
@Override
public void onBlogPostClick(BlogPostItem post) {
// do nothing
}
@Override
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
}
} }
} }

View File

@@ -31,12 +31,16 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
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.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.view.TextSendController.SendState;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH; import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -112,8 +116,8 @@ public class WriteBlogPostActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers) { List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text)) throw new AssertionError(); if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar // hide publish button, show progress bar
@@ -122,6 +126,7 @@ public class WriteBlogPostActivity extends BriarActivity
progressBar.setVisibility(VISIBLE); progressBar.setVisibility(VISIBLE);
storePost(text); storePost(text);
return new MutableLiveData<>(SENT);
} }
private void storePost(String text) { private void storePost(String text) {

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.contact; package org.briarproject.briar.android.contact;
import android.content.Context; import android.content.Context;
import android.view.View;
import org.briarproject.briar.android.util.BriarAdapter; import org.briarproject.briar.android.util.BriarAdapter;
@@ -44,4 +45,8 @@ public abstract class BaseContactListAdapter<I extends ContactItem, VH extends C
return true; return true;
} }
public interface OnContactClickListener<I> {
void onItemClick(View view, I item);
}
} }

View File

@@ -7,6 +7,7 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import javax.annotation.Nullable; import javax.annotation.Nullable;

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NullSafety; import org.briarproject.bramble.api.nullsafety.NullSafety;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import androidx.recyclerview.widget.DiffUtil.ItemCallback; import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.ListAdapter;
@@ -15,6 +16,9 @@ import androidx.recyclerview.widget.ListAdapter;
public class ContactListAdapter extends public class ContactListAdapter extends
ListAdapter<ContactListItem, ContactListItemViewHolder> { ListAdapter<ContactListItem, ContactListItemViewHolder> {
// TODO: using the click listener interface from BaseContactListAdapter on
// purpose here because it is entangled with ContactListItemViewHolder. At
// some point we probably want to change that.
protected final OnContactClickListener<ContactListItem> listener; protected final OnContactClickListener<ContactListItem> listener;
public ContactListAdapter( public ContactListAdapter(

View File

@@ -15,6 +15,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.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity; import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity; import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ConversationActivity;
@@ -100,6 +101,8 @@ public class ContactListFragment extends BaseFragment
.observe(getViewLifecycleOwner(), result -> { .observe(getViewLifecycleOwner(), result -> {
result.onError(this::handleException).onSuccess(items -> { result.onError(this::handleException).onSuccess(items -> {
adapter.submitList(items); adapter.submitList(items);
// TODO remove when BriarRecyclerView was adapted
list.showData();
}); });
}); });
viewModel.getHasPendingContacts() viewModel.getHasPendingContacts()

View File

@@ -5,6 +5,7 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import java.util.Locale; import java.util.Locale;

View File

@@ -3,41 +3,70 @@ package org.briarproject.briar.android.contact;
import android.app.Application; import android.app.Application;
import org.briarproject.bramble.api.connection.ConnectionRegistry; import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent; import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent; import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.conversation.ConversationManager; import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager; import org.briarproject.briar.api.identity.AuthorManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault @NotNullByDefault
class ContactListViewModel extends ContactsViewModel { class ContactListViewModel extends DbViewModel implements EventListener {
private static final Logger LOG = private static final Logger LOG =
getLogger(ContactListViewModel.class.getName()); getLogger(ContactListViewModel.class.getName());
private final ContactManager contactManager;
private final AuthorManager authorManager;
private final ConversationManager conversationManager;
private final ConnectionRegistry connectionRegistry;
private final EventBus eventBus;
private final AndroidNotificationManager notificationManager; private final AndroidNotificationManager notificationManager;
private final MutableLiveData<LiveResult<List<ContactListItem>>>
contactListItems = new MutableLiveData<>();
private final MutableLiveData<Boolean> hasPendingContacts = private final MutableLiveData<Boolean> hasPendingContacts =
new MutableLiveData<>(); new MutableLiveData<>();
@@ -50,25 +79,99 @@ class ContactListViewModel extends ContactsViewModel {
ConversationManager conversationManager, ConversationManager conversationManager,
ConnectionRegistry connectionRegistry, EventBus eventBus, ConnectionRegistry connectionRegistry, EventBus eventBus,
AndroidNotificationManager notificationManager) { AndroidNotificationManager notificationManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor, super(application, dbExecutor, lifecycleManager, db, androidExecutor);
contactManager, authorManager, conversationManager, this.contactManager = contactManager;
connectionRegistry, eventBus); this.authorManager = authorManager;
this.conversationManager = conversationManager;
this.connectionRegistry = connectionRegistry;
this.eventBus = eventBus;
this.notificationManager = notificationManager; this.notificationManager = notificationManager;
this.eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
void loadContacts() {
loadList(this::loadContacts, contactListItems::setValue);
}
private List<ContactListItem> loadContacts(Transaction txn)
throws DbException {
long start = now();
List<ContactListItem> contacts = new ArrayList<>();
for (Contact c : contactManager.getContacts(txn)) {
ContactId id = c.getId();
AuthorInfo authorInfo = authorManager.getAuthorInfo(txn, c);
MessageTracker.GroupCount count =
conversationManager.getGroupCount(txn, id);
boolean connected = connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, authorInfo, connected, count));
}
Collections.sort(contacts);
logDuration(LOG, "Full load", start);
return contacts;
} }
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
super.eventOccurred(e); if (e instanceof ContactAddedEvent) {
if (e instanceof PendingContactAddedEvent || LOG.info("Contact added, reloading");
loadContacts();
} else if (e instanceof ContactConnectedEvent) {
updateItem(((ContactConnectedEvent) e).getContactId(),
item -> new ContactListItem(item, true), false);
} else if (e instanceof ContactDisconnectedEvent) {
updateItem(((ContactDisconnectedEvent) e).getContactId(),
item -> new ContactListItem(item, false), false);
} else if (e instanceof ContactRemovedEvent) {
LOG.info("Contact removed, removing item");
removeItem(((ContactRemovedEvent) e).getContactId());
} else if (e instanceof ConversationMessageReceivedEvent) {
LOG.info("Conversation message received, updating item");
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
ConversationMessageHeader h = p.getMessageHeader();
updateItem(p.getContactId(), item -> new ContactListItem(item, h),
true);
} else if (e instanceof PendingContactAddedEvent ||
e instanceof PendingContactRemovedEvent) { e instanceof PendingContactRemovedEvent) {
checkForPendingContacts(); checkForPendingContacts();
} else if (e instanceof AvatarUpdatedEvent) {
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
updateItem(a.getContactId(), item -> new ContactListItem(item,
a.getAttachmentHeader()), false);
} }
} }
LiveData<LiveResult<List<ContactListItem>>> getContactListItems() {
return contactListItems;
}
LiveData<Boolean> getHasPendingContacts() { LiveData<Boolean> getHasPendingContacts() {
return hasPendingContacts; return hasPendingContacts;
} }
private void updateItem(ContactId c,
Function<ContactListItem, ContactListItem> replacer, boolean sort) {
List<ContactListItem> list = updateListItems(contactListItems,
itemToTest -> itemToTest.getContact().getId().equals(c),
replacer);
if (list == null) return;
if (sort) Collections.sort(list);
contactListItems.setValue(new LiveResult<>(list));
}
private void removeItem(ContactId c) {
List<ContactListItem> list = removeListItems(contactListItems,
itemToTest -> itemToTest.getContact().getId().equals(c));
if (list == null) return;
contactListItems.setValue(new LiveResult<>(list));
}
void checkForPendingContacts() { void checkForPendingContacts() {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {

View File

@@ -1,168 +0,0 @@
package org.briarproject.briar.android.contact;
import android.app.Application;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault
public class ContactsViewModel extends DbViewModel implements EventListener {
private static final Logger LOG =
getLogger(ContactsViewModel.class.getName());
protected final ContactManager contactManager;
private final AuthorManager authorManager;
private final ConversationManager conversationManager;
private final ConnectionRegistry connectionRegistry;
private final EventBus eventBus;
private final MutableLiveData<LiveResult<List<ContactListItem>>>
contactListItems = new MutableLiveData<>();
@Inject
public ContactsViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, TransactionManager db,
AndroidExecutor androidExecutor, ContactManager contactManager,
AuthorManager authorManager,
ConversationManager conversationManager,
ConnectionRegistry connectionRegistry, EventBus eventBus) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.contactManager = contactManager;
this.authorManager = authorManager;
this.conversationManager = conversationManager;
this.connectionRegistry = connectionRegistry;
this.eventBus = eventBus;
this.eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
protected void loadContacts() {
loadFromDb(this::loadContacts, contactListItems::setValue);
}
private List<ContactListItem> loadContacts(Transaction txn)
throws DbException {
long start = now();
List<ContactListItem> contacts = new ArrayList<>();
for (Contact c : contactManager.getContacts(txn)) {
ContactId id = c.getId();
if (!displayContact(id)) {
continue;
}
AuthorInfo authorInfo = authorManager.getAuthorInfo(txn, c);
MessageTracker.GroupCount count =
conversationManager.getGroupCount(txn, id);
boolean connected = connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, authorInfo, connected, count));
}
Collections.sort(contacts);
logDuration(LOG, "Full load", start);
return contacts;
}
/**
* Override this method to display only a subset of contacts.
*/
protected boolean displayContact(ContactId contactId) {
return true;
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactAddedEvent) {
LOG.info("Contact added, reloading");
loadContacts();
} else if (e instanceof ContactConnectedEvent) {
updateItem(((ContactConnectedEvent) e).getContactId(),
item -> new ContactListItem(item, true), false);
} else if (e instanceof ContactDisconnectedEvent) {
updateItem(((ContactDisconnectedEvent) e).getContactId(),
item -> new ContactListItem(item, false), false);
} else if (e instanceof ContactRemovedEvent) {
LOG.info("Contact removed, removing item");
removeItem(((ContactRemovedEvent) e).getContactId());
} else if (e instanceof ConversationMessageReceivedEvent) {
LOG.info("Conversation message received, updating item");
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
ConversationMessageHeader h = p.getMessageHeader();
updateItem(p.getContactId(), item -> new ContactListItem(item, h),
true);
} else if (e instanceof AvatarUpdatedEvent) {
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
updateItem(a.getContactId(), item -> new ContactListItem(item,
a.getAttachmentHeader()), false);
}
}
public LiveData<LiveResult<List<ContactListItem>>> getContactListItems() {
return contactListItems;
}
@UiThread
private void updateItem(ContactId c,
Function<ContactListItem, ContactListItem> replacer, boolean sort) {
List<ContactListItem> list = updateListItems(getList(contactListItems),
itemToTest -> itemToTest.getContact().getId().equals(c),
replacer);
if (list == null) return;
if (sort) Collections.sort(list);
contactListItems.setValue(new LiveResult<>(list));
}
@UiThread
private void removeItem(ContactId c) {
removeAndUpdateListItems(contactListItems,
itemToTest -> itemToTest.getContact().getId().equals(c));
}
}

View File

@@ -0,0 +1,50 @@
package org.briarproject.briar.android.contact;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
@NotNullByDefault
public class LegacyContactListAdapter extends
BaseContactListAdapter<ContactListItem, ContactListItemViewHolder> {
public LegacyContactListAdapter(Context context,
OnContactClickListener<ContactListItem> listener) {
super(context, ContactListItem.class, listener);
}
@Override
public ContactListItemViewHolder onCreateViewHolder(ViewGroup viewGroup,
int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_contact, viewGroup, false);
return new ContactListItemViewHolder(v);
}
@Override
public boolean areContentsTheSame(ContactListItem c1, ContactListItem c2) {
// check for all properties that influence visual
// representation of contact
if (c1.isEmpty() != c2.isEmpty()) {
return false;
}
if (c1.getUnreadCount() != c2.getUnreadCount()) {
return false;
}
if (c1.getTimestamp() != c2.getTimestamp()) {
return false;
}
return c1.isConnected() == c2.isConnected();
}
@Override
public int compare(ContactListItem c1, ContactListItem c2) {
return Long.compare(c2.getTimestamp(), c1.getTimestamp());
}
}

View File

@@ -1,9 +0,0 @@
package org.briarproject.briar.android.contact;
import android.view.View;
public interface OnContactClickListener<I> {
void onItemClick(View view, I item);
}

View File

@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.contact.BaseContactListAdapter; import org.briarproject.briar.android.contact.BaseContactListAdapter;
import org.briarproject.briar.android.contact.ContactItemViewHolder; import org.briarproject.briar.android.contact.ContactItemViewHolder;
import org.briarproject.briar.android.contact.OnContactClickListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;

View File

@@ -12,8 +12,8 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import org.briarproject.briar.android.contact.ContactItemViewHolder; import org.briarproject.briar.android.contact.ContactItemViewHolder;
import org.briarproject.briar.android.contact.OnContactClickListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;

View File

@@ -6,8 +6,8 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import org.briarproject.briar.android.contact.ContactItemViewHolder; import org.briarproject.briar.android.contact.ContactItemViewHolder;
import org.briarproject.briar.android.contact.OnContactClickListener;
import javax.annotation.Nullable; import javax.annotation.Nullable;

View File

@@ -7,7 +7,6 @@ import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.OnContactClickListener;
@NotNullByDefault @NotNullByDefault
class ContactSelectorAdapter extends class ContactSelectorAdapter extends

View File

@@ -8,7 +8,7 @@ import android.view.MenuItem;
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.contact.OnContactClickListener; import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault

View File

@@ -3,7 +3,7 @@ package org.briarproject.briar.android.contactselection;
import android.view.View; import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.contact.OnContactClickListener; import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import javax.annotation.Nullable; import javax.annotation.Nullable;

View File

@@ -0,0 +1,77 @@
package org.briarproject.briar.android.controller;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault
public interface SharingController {
/**
* Sets the listener that is called when contacts go on or offline.
*/
@UiThread
void setSharingListener(SharingListener listener);
/**
* Unsets the listener.
*/
@UiThread
void unsetSharingListener(SharingListener listener);
/**
* Call this when your lifecycle starts,
* so the listener will be called when information changes.
*/
@UiThread
void onStart();
/**
* Call this when your lifecycle stops,
* so that the controller knows it can stops listening to events.
*/
@UiThread
void onStop();
/**
* Adds one contact to be tracked.
*/
@UiThread
void add(ContactId c);
/**
* Adds a collection of contacts to be tracked.
*/
@UiThread
void addAll(Collection<ContactId> contacts);
/**
* Call this when the contact identified by c is no longer sharing
* the given group identified by GroupId g.
*/
@UiThread
void remove(ContactId c);
/**
* Returns the number of online contacts.
*/
@UiThread
int getOnlineCount();
/**
* Returns the total number of contacts that have been added.
*/
@UiThread
int getTotalCount();
interface SharingListener {
@UiThread
void onSharingInfoUpdated(int total, int online);
}
}

View File

@@ -0,0 +1,109 @@
package org.briarproject.briar.android.controller;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault
public class SharingControllerImpl implements SharingController, EventListener {
private final EventBus eventBus;
private final ConnectionRegistry connectionRegistry;
// UI thread
private final Set<ContactId> contacts = new HashSet<>();
// UI thread
@Nullable
private SharingListener listener;
@Inject
SharingControllerImpl(EventBus eventBus,
ConnectionRegistry connectionRegistry) {
this.eventBus = eventBus;
this.connectionRegistry = connectionRegistry;
}
@Override
public void setSharingListener(SharingListener listener) {
this.listener = listener;
}
@Override
public void unsetSharingListener(SharingListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void onStart() {
eventBus.addListener(this);
}
@Override
public void onStop() {
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId());
} else if (e instanceof ContactDisconnectedEvent) {
setConnected(((ContactDisconnectedEvent) e).getContactId());
}
}
@UiThread
private void setConnected(ContactId c) {
if (listener == null) throw new IllegalStateException();
if (contacts.contains(c)) {
int online = getOnlineCount();
listener.onSharingInfoUpdated(contacts.size(), online);
}
}
@Override
public void addAll(Collection<ContactId> c) {
contacts.addAll(c);
}
@Override
public void add(ContactId c) {
contacts.add(c);
}
@Override
public void remove(ContactId c) {
contacts.remove(c);
}
@Override
public int getOnlineCount() {
int online = 0;
for (ContactId c : contacts) {
if (connectionRegistry.isConnected(c)) online++;
}
return online;
}
@Override
public int getTotalCount() {
return contacts.size();
}
}

View File

@@ -31,6 +31,8 @@ import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
// TODO: we can probably switch to androidx DialogFragment here but need to
// test this properly
public class AliasDialogFragment extends AppCompatDialogFragment { public class AliasDialogFragment extends AppCompatDialogFragment {
final static String TAG = AliasDialogFragment.class.getName(); final static String TAG = AliasDialogFragment.class.getName();

View File

@@ -50,6 +50,7 @@ import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache; import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
@@ -59,8 +60,10 @@ import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener; import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.client.SessionId;
@@ -138,12 +141,14 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.android.view.AuthorView.setAvatar; import static org.briarproject.briar.android.view.AuthorView.setAvatar;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextCache, implements BaseFragmentListener, EventListener, ConversationListener,
AttachmentCache, AttachmentListener, ActionMode.Callback { TextCache, AttachmentCache, AttachmentListener, ActionMode.Callback {
public static final String CONTACT_ID = "briar.CONTACT_ID"; public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -268,15 +273,11 @@ public class ConversationActivity extends BriarActivity
ImagePreview imagePreview = findViewById(R.id.imagePreview); ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView, sendController = new TextAttachmentController(textInputView,
imagePreview, this, viewModel); imagePreview, this, viewModel);
viewModel.hasImageSupport().observe(this, new Observer<Boolean>() { observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
@Override if (format != TEXT_ONLY) {
public void onChanged(@Nullable Boolean hasSupport) { // TODO: remove cast when removing feature flag
if (hasSupport != null && hasSupport) { ((TextAttachmentController) sendController)
// TODO: remove cast when removing feature flag .setImagesSupported();
((TextAttachmentController) sendController)
.setImagesSupported();
viewModel.hasImageSupport().removeObserver(this);
}
} }
}); });
} else { } else {
@@ -286,6 +287,9 @@ public class ConversationActivity extends BriarActivity
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH); textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false); textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom); textInputView.setOnKeyboardShownListener(this::scrollToBottom);
viewModel.getAutoDeleteTimer().observe(this, timer ->
sendController.setAutoDeleteTimer(timer));
} }
private void scrollToBottom() { private void scrollToBottom() {
@@ -369,6 +373,14 @@ public class ConversationActivity extends BriarActivity
// enable alias action if available // enable alias action if available
observeOnce(viewModel.getContactItem(), this, contact -> observeOnce(viewModel.getContactItem(), this, contact ->
menu.findItem(R.id.action_set_alias).setEnabled(true)); menu.findItem(R.id.action_set_alias).setEnabled(true));
// Show auto-delete menu item if feature is enabled
if (featureFlags.shouldEnableDisappearingMessages()) {
MenuItem item = menu.findItem(R.id.action_conversation_settings);
item.setVisible(true);
// Enable menu item only if contact supports auto-delete
viewModel.getPrivateMessageFormat().observe(this, format ->
item.setEnabled(format == TEXT_IMAGES_AUTO_DELETE));
}
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -390,6 +402,10 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show( AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG); getSupportFragmentManager(), AliasDialogFragment.TAG);
return true; return true;
case R.id.action_conversation_settings:
if (contactId == null) return false;
onAutoDeleteTimerNoticeClicked();
return true;
case R.id.action_delete_all_messages: case R.id.action_delete_all_messages:
askToDeleteAllMessages(); askToDeleteAllMessages();
return true; return true;
@@ -559,7 +575,7 @@ public class ConversationActivity extends BriarActivity
this::showImageOnboarding); this::showImageOnboarding);
} }
List<ConversationItem> items = createItems(headers); List<ConversationItem> items = createItems(headers);
adapter.addAll(items); adapter.replaceAll(items);
list.showData(); list.showData();
if (layoutManagerState == null) { if (layoutManagerState == null) {
scrollToBottom(); scrollToBottom();
@@ -640,8 +656,8 @@ public class ConversationActivity extends BriarActivity
supportFinishAfterTransition(); supportFinishAfterTransition();
} }
} else if (e instanceof ConversationMessageReceivedEvent) { } else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent p = ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent) e; (ConversationMessageReceivedEvent<?>) e;
if (p.getContactId().equals(contactId)) { if (p.getContactId().equals(contactId)) {
LOG.info("Message received, adding"); LOG.info("Message received, adding");
onNewConversationMessage(p.getMessageHeader()); onNewConversationMessage(p.getMessageHeader());
@@ -658,6 +674,13 @@ public class ConversationActivity extends BriarActivity
LOG.info("Messages acked"); LOG.info("Messages acked");
markMessages(m.getMessageIds(), true, true); markMessages(m.getMessageIds(), true, true);
} }
} else if (e instanceof ConversationMessagesDeletedEvent) {
ConversationMessagesDeletedEvent m =
(ConversationMessagesDeletedEvent) e;
if (m.getContactId().equals(contactId)) {
LOG.info("Messages auto-deleted");
onConversationMessagesDeleted(m.getMessageIds());
}
} else if (e instanceof ContactConnectedEvent) { } else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e; ContactConnectedEvent c = (ContactConnectedEvent) e;
if (c.getContactId().equals(contactId)) { if (c.getContactId().equals(contactId)) {
@@ -705,6 +728,13 @@ public class ConversationActivity extends BriarActivity
} }
} }
@UiThread
private void onConversationMessagesDeleted(
Collection<MessageId> messageIds) {
adapter.incrementRevision();
adapter.removeItems(messageIds);
}
@UiThread @UiThread
private void markMessages(Collection<MessageId> messageIds, boolean sent, private void markMessages(Collection<MessageId> messageIds, boolean sent,
boolean seen) { boolean seen) {
@@ -735,20 +765,13 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public void onSendClick(@Nullable String text, public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) { List<AttachmentHeader> attachmentHeaders,
long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty()) if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError(); throw new AssertionError();
long timestamp = System.currentTimeMillis(); return viewModel
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); .sendMessage(text, attachmentHeaders, expectedAutoDeleteTimer);
viewModel.sendMessage(text, attachmentHeaders, timestamp);
textInputView.clearText();
}
private long getMinTimestampForNewMessage() {
// Don't use an earlier timestamp than the newest message
ConversationItem item = adapter.getLastItem();
return item == null ? 0 : item.getTime() + 1;
} }
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) { private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
@@ -823,10 +846,6 @@ public class ConversationActivity extends BriarActivity
fails.add(getString( fails.add(getString(
R.string.dialog_message_not_deleted_ongoing_invitations)); R.string.dialog_message_not_deleted_ongoing_invitations));
} }
if (result.hasNotFullyDownloaded()) {
fails.add(getString(
R.string.dialog_message_not_deleted_partly_downloaded));
}
// add problems the user can resolve // add problems the user can resolve
if (result.hasNotAllIntroductionSelected() && if (result.hasNotAllIntroductionSelected() &&
result.hasNotAllInvitationSelected()) { result.hasNotAllInvitationSelected()) {
@@ -958,13 +977,11 @@ public class ConversationActivity extends BriarActivity
adapter.notifyItemChanged(position, item); adapter.notifyItemChanged(position, item);
} }
runOnDbThread(() -> { runOnDbThread(() -> {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try { try {
switch (item.getRequestType()) { switch (item.getRequestType()) {
case INTRODUCTION: case INTRODUCTION:
respondToIntroductionRequest(item.getSessionId(), respondToIntroductionRequest(item.getSessionId(),
accept, timestamp); accept);
break; break;
case FORUM: case FORUM:
respondToForumRequest(item.getSessionId(), accept); respondToForumRequest(item.getSessionId(), accept);
@@ -1038,11 +1055,18 @@ public class ConversationActivity extends BriarActivity
ActivityCompat.startActivity(this, i, options.toBundle()); ActivityCompat.startActivity(this, i, options.toBundle());
} }
@Override
public void onAutoDeleteTimerNoticeClicked() {
ConversationSettingsDialog dialog =
ConversationSettingsDialog.newInstance(contactId);
dialog.show(getSupportFragmentManager(),
ConversationSettingsDialog.TAG);
}
@DatabaseExecutor @DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId, private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException { boolean accept) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, time, introductionManager.respondToIntroduction(contactId, sessionId, accept);
accept);
} }
@DatabaseExecutor @DatabaseExecutor

View File

@@ -13,20 +13,26 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter; import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.ItemReturningAdapter; import org.briarproject.briar.android.util.ItemReturningAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.recyclerview.selection.SelectionTracker; import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool; import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@NotNullByDefault @NotNullByDefault
class ConversationAdapter class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder> extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
implements ItemReturningAdapter<ConversationItem> { implements ItemReturningAdapter<ConversationItem> {
private ConversationListener listener; private final ConversationListener listener;
private final RecycledViewPool imageViewPool; private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration; private final ImageItemDecoration imageItemDecoration;
@Nullable @Nullable
@@ -65,22 +71,20 @@ class ConversationAdapter
@LayoutRes int type) { @LayoutRes int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate( View v = LayoutInflater.from(viewGroup.getContext()).inflate(
type, viewGroup, false); type, viewGroup, false);
switch (type) { if (type == R.layout.list_item_conversation_msg_in) {
case R.layout.list_item_conversation_msg_in: return new ConversationMessageViewHolder(v, listener, true,
return new ConversationMessageViewHolder(v, listener, true, imageViewPool, imageItemDecoration);
imageViewPool, imageItemDecoration); } else if (type == R.layout.list_item_conversation_msg_out) {
case R.layout.list_item_conversation_msg_out: return new ConversationMessageViewHolder(v, listener, false,
return new ConversationMessageViewHolder(v, listener, false, imageViewPool, imageItemDecoration);
imageViewPool, imageItemDecoration); } else if (type == R.layout.list_item_conversation_notice_in) {
case R.layout.list_item_conversation_notice_in: return new ConversationNoticeViewHolder(v, listener, true);
return new ConversationNoticeViewHolder(v, listener, true); } else if (type == R.layout.list_item_conversation_notice_out) {
case R.layout.list_item_conversation_notice_out: return new ConversationNoticeViewHolder(v, listener, false);
return new ConversationNoticeViewHolder(v, listener, false); } else if (type == R.layout.list_item_conversation_request) {
case R.layout.list_item_conversation_request: return new ConversationRequestViewHolder(v, listener, true);
return new ConversationRequestViewHolder(v, listener, true);
default:
throw new IllegalArgumentException("Unknown ConversationItem");
} }
throw new IllegalArgumentException("Unknown ConversationItem");
} }
@Override @Override
@@ -107,22 +111,65 @@ class ConversationAdapter
return c1.equals(c2); return c1.equals(c2);
} }
@Override
public void add(ConversationItem item) {
items.beginBatchedUpdates();
items.add(item);
updateTimersInBatch();
items.endBatchedUpdates();
}
@Override
public void replaceAll(Collection<ConversationItem> itemsToReplace) {
items.beginBatchedUpdates();
// there can be items already in the adapter
// SortedList takes care of duplicates and detecting changed items
items.replaceAll(itemsToReplace);
updateTimersInBatch();
items.endBatchedUpdates();
}
@UiThread
void removeItems(Collection<MessageId> messageIds) {
// Collect all items to be deleted first
// and then delete them in one batched update.
// Deleting them right away would cause issues
// due to changing list positions.
List<ConversationItem> toRemove = new ArrayList<>(messageIds.size());
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (messageIds.contains(item.getId())) toRemove.add(item);
}
items.beginBatchedUpdates();
for (ConversationItem item : toRemove) items.remove(item);
items.endBatchedUpdates();
}
private void updateTimersInBatch() {
long lastTimerIncoming = NO_AUTO_DELETE_TIMER;
long lastTimerOutgoing = NO_AUTO_DELETE_TIMER;
for (int i = 0; i < items.size(); i++) {
ConversationItem c = items.get(i);
boolean itemChanged;
boolean timerChanged;
if (c.isIncoming()) {
timerChanged = lastTimerIncoming != c.getAutoDeleteTimer();
lastTimerIncoming = c.getAutoDeleteTimer();
} else {
timerChanged = lastTimerOutgoing != c.getAutoDeleteTimer();
lastTimerOutgoing = c.getAutoDeleteTimer();
}
itemChanged = c.setTimerNoticeVisible(timerChanged);
if (itemChanged) items.updateItemAt(i, c);
}
}
void setSelectionTracker(SelectionTracker<String> tracker) { void setSelectionTracker(SelectionTracker<String> tracker) {
this.tracker = tracker; this.tracker = tracker;
} }
@Nullable
ConversationItem getLastItem() {
if (items.size() > 0) {
return items.get(items.size() - 1);
} else {
return null;
}
}
SparseArray<ConversationItem> getOutgoingMessages() { SparseArray<ConversationItem> getOutgoingMessages() {
SparseArray<ConversationItem> messages = new SparseArray<>(); SparseArray<ConversationItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i); ConversationItem item = items.get(i);
if (!item.isIncoming()) { if (!item.isIncoming()) {

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