Compare commits

...

192 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
akwizgran
629cff20a3 Merge branch '1952-oom-avatar-preview-glide' into 'master'
Load avatar previews with Glide to prevent OOM errors

Closes #1952

See merge request briar/briar!1388
2021-03-01 18:02:19 +00:00
Torsten Grote
6cfb70db95 Load image from URI with Glide to prevent OOM errors 2021-03-01 14:15:53 -03:00
Torsten Grote
737ecfb620 Some unrelated code changes to avatar settings 2021-03-01 14:15:08 -03:00
akwizgran
5a424b178e Merge branch '1667-toolbar-options' into 'master'
Make group/create forum/write blog post buttons to always show

Closes #1667

See merge request briar/briar!1377
2021-03-01 16:34:14 +00:00
Torsten Grote
59f4e7c34a Super call to onRequestPermissionsResult() is now required 2021-02-23 10:55:20 -03:00
Torsten Grote
2480824d69 Fix toolbar buttons not showing up after sign-in on lower API levels 2021-02-23 10:55:20 -03:00
akwizgran
a6c2000d81 Merge branch '1825-pending-contact-error' into 'master'
Be more specific about errors when adding pending contact

Closes #1825

See merge request briar/briar!1354
2021-02-22 11:12:49 +00:00
akwizgran
a38a3139d9 Merge branch 'fix-message-in-profile-picture-confirmation' into 'master'
Fix message in profile picture confirmation

See merge request briar/briar!1356
2021-02-22 11:06:58 +00:00
akwizgran
4c8adaa02b Merge branch '1399-unlock-activity-crash' into 'master'
Let LockManager only lock current, not future process

Closes #1399

See merge request briar/briar!1374
2021-02-22 10:49:17 +00:00
akwizgran
8a534b4503 Bump version numbers for 1.2.16 release. 2021-02-19 18:01:56 +00:00
akwizgran
e5b2275c82 Merge branch '1947-forum-crash' into 'master'
Don't add new thread items when the existing ones haven't loaded

Closes #1947

See merge request briar/briar!1375
2021-02-19 17:27:38 +00:00
Torsten Grote
5159593825 Don't add new item when the existing ones haven't loaded 2021-02-19 14:17:21 -03:00
Torsten Grote
a546fecc01 Let LockManager only lock current, not future process
This fixes a bug on Android 8
where the AlarmManager would re-start a killed BriarService.
Then the LockManager lingers around locked and causes an ANR on Android 8.x when the user comes back to it.
2021-02-19 10:42:43 -03:00
Nico Alt
3e7e37f5f6 Include pending contact id in error response 2021-02-19 12:00:00 +00:00
Nico Alt
d095ba0b15 Include name/alias of already existing (pending) contact in error 2021-02-19 14:44:56 +01:00
Nico Alt
7fab97d26c Be more specific about errors when adding pending contact
Following the docs at
https://code.briarproject.org/briar/briar/-/blob/beta-1.2.14/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java#L110

Fixes #1825
2021-02-19 14:44:56 +01:00
akwizgran
6fbc82ee27 Merge branch '1075-1146-1317-ongoing-notification' into 'master'
Use IMPORTANCE_LOW for ongoing notification, don't show a badge

Closes #1317, #1146, and #1075

See merge request briar/briar!1369
2021-02-18 17:00:47 +00:00
akwizgran
885b03cfd7 Bump version numbers for 1.2.15 release. 2021-02-18 15:27:57 +00:00
akwizgran
f81bfcafeb Update translations. 2021-02-18 15:26:10 +00:00
akwizgran
f36f1cf3d4 Merge branch '1764-fix-change-app-language-does-not-work' into 'master'
Resolve "Change app language does not work"

Closes #1764

See merge request briar/briar!1367
2021-02-17 16:59:59 +00:00
Torsten Grote
7d6a63d866 Merge branch '1934-upgrade-obfs4proxy' into 'master'
Upgrade obfs4proxy to 0.0.12-dev

Closes #1934

See merge request briar/briar!1372
2021-02-17 16:58:22 +00:00
akwizgran
15ebdf8dd5 Upgrade obfs4proxy to 0.0.12-dev. 2021-02-17 16:41:49 +00:00
akwizgran
db2c235283 Merge branch 'private-group-disabled' into 'master'
Fix disabled groups after screen rotation

See merge request briar/briar!1371
2021-02-17 14:09:57 +00:00
Sebastian Kürten
e5bd43469e Add Javados to Localizer#setLocale() 2021-02-15 14:54:20 +01:00
Torsten Grote
9366c184d8 Fix disabled groups after screen rotation
isDissolved was reverted to LiveData that only shows a dialog when the activity was first opened
2021-02-15 09:55:59 -03:00
Sebastian Kürten
73d2c964d4 Make language switching for robust 2021-02-15 12:31:51 +01:00
akwizgran
fb2b4209cf Use IMPORTANCE_LOW for ongoing notification, don't show a badge. 2021-02-10 11:46:41 +00:00
Torsten Grote
a04b512497 Merge branch 'tor-0.3.5.13' into 'master'
Upgrade Tor to 0.3.5.13

Closes #1922

See merge request briar/briar!1363
2021-02-09 12:15:45 +00:00
akwizgran
3d9515e308 Also upgrade obfs4proxy and bramble-java's Tor. 2021-02-09 12:05:54 +00:00
akwizgran
1b19b331b1 Merge branch '1904-fragment-started-too-late' into 'master'
Don't launch fragments with back button when not started

Closes #1904

See merge request briar/briar!1365
2021-02-09 11:05:08 +00:00
akwizgran
d151a2d7f7 Merge branch '1910-state-exception-when-adding-contact' into 'master'
Restore remote handshake link when AddContactViewModel gets destroyed

Closes #1910

See merge request briar/briar!1364
2021-02-09 10:49:38 +00:00
Torsten Grote
9712a4b849 Don't launch fragments with back button when not started
Sounds strange, but apparently can happen.
2021-02-08 16:38:15 -03:00
Torsten Grote
cf1ac5e3e5 Restore remote handshake link when AddContactViewModel gets destroyed 2021-02-08 16:03:10 -03:00
Torsten Grote
cb859e998d Upgrade Tor to 0.3.5.13 2021-02-08 15:44:35 -03:00
akwizgran
0b9345f867 Merge branch '1621-link-disappearing' into 'master'
Remove monospace typeface from our briar:// link

Closes #1621

See merge request briar/briar!1362
2021-02-08 18:36:16 +00:00
Torsten Grote
12988120d1 Remove monospace typeface from our briar:// link
as this makes the text to become invisible when selecting all text on API 15-17
2021-02-08 14:45:57 -03:00
akwizgran
8d6c866e62 Merge branch '1926-cap-scrypt-cost' into 'master'
Cap the scrypt cost parameter to avoid OOM

Closes #1926

See merge request briar/briar!1360
2021-02-08 17:30:57 +00:00
akwizgran
8f82cf3c73 Merge branch '1917-logcat-process' into 'master'
Fix crash reporter to capture logs from main process

Closes #1917

See merge request briar/briar!1359
2021-02-08 16:58:12 +00:00
Torsten Grote
21112ce092 Encrypt logs before handing them to crash report process 2021-02-08 13:43:37 -03:00
akwizgran
21ee3ea00d Merge branch 'add-custom-dictionary' into 'master'
Add a custom dictionary

See merge request briar/briar!1361
2021-02-08 14:01:43 +00:00
Sebastian Kürten
bb964101b3 Add a custom dictionary
This reduces the amount of words highlighted by the spell checker and
helps focussing on words that are really misspelled.
2021-02-08 14:35:14 +01:00
akwizgran
d796eff0f6 Cap the scrypt cost parameter to avoid OOM. 2021-02-08 11:32:03 +00:00
Torsten Grote
700ea2b387 Add support for logs to StreamReader and StreamWriter
Shamelessly stolen from d9b4c013
2021-02-05 17:07:48 -03:00
Sebastian Kürten
e4a66615a7 Fix remark in dialog for confirming profile picture 2021-02-04 18:43:32 +01:00
Torsten Grote
6e3a7d8d0c Merge branch 'gitlab-bridge-test' into 'master'
Add GitLab pipeline stage for running optional tests

See merge request briar/briar!1353
2021-01-29 16:07:49 +00:00
akwizgran
166b5d4add Run optional tests automatically for tags, otherwise manually. 2021-01-29 15:45:39 +00:00
akwizgran
0fd59a26f6 Raise BridgeTest timeout to avoid spurious failures. 2021-01-29 15:39:59 +00:00
akwizgran
4162bf990a Merge branch '1881-thread-list-controller' into 'master'
Migrate ThreadListController to ViewModel

Closes #1881, #1873, and #1870

See merge request briar/briar!1336
2021-01-29 15:10:16 +00:00
akwizgran
09cfadbf7e Add manual pipeline stage for running optional tests. 2021-01-29 14:38:03 +00:00
akwizgran
3fb27dbb12 Bump version numbers for 1.2.14 release. 2021-01-29 14:10:24 +00:00
Torsten Grote
ae4a04bada Finishing touches of ThreadListViewModel migration
docs and minor improvements
2021-01-29 08:33:28 -03:00
akwizgran
831c65b647 Merge branch 'vector-compat' into 'master'
Use vector support libraries instead of rasterizing all drawables

See merge request briar/briar!1346
2021-01-28 17:59:08 +00:00
akwizgran
afcd38b84c Update translations. 2021-01-28 16:15:44 +00:00
Torsten Grote
d670179e30 Access MessageTree only on UiThread and improve code in the process 2021-01-27 15:37:09 -03:00
Torsten Grote
998c435b13 Allow to add forum/group posts in transaction 2021-01-27 15:37:09 -03:00
Torsten Grote
4a0327a62b thread list: fix redundant load and dissolved dialog showing again after screen rotation 2021-01-27 15:37:08 -03:00
akwizgran
70532732c8 Use commit action to add contacts on UI thread. 2021-01-27 15:37:08 -03:00
akwizgran
d69406dfe3 Add transactional getSharedWith() method to SharingManager. 2021-01-27 15:37:08 -03:00
akwizgran
98619df867 Use commit action to add contacts to SharingController. 2021-01-27 15:37:07 -03:00
akwizgran
f2eca0fdb6 Add transactional getMembers() method to PrivateGroupManager. 2021-01-27 15:37:07 -03:00
akwizgran
c62a57e8b2 Add transactional helper method to DbViewModel. 2021-01-27 15:37:07 -03:00
Torsten Grote
239c4a27ad Address first round of review feedback for thread list view model migration 2021-01-27 15:37:06 -03:00
Torsten Grote
e5d78a858d Clear thread notification automatically after blocking new ones 2021-01-26 15:42:18 -03:00
Torsten Grote
5c1bcdeb9d Merge branch 'update-bridges' into 'master'
Update bridges

See merge request briar/briar!1352
2021-01-26 14:11:00 +00:00
akwizgran
6c1f5450cb Add run configuration for BridgeTest. 2021-01-26 13:57:33 +00:00
akwizgran
0d070cf422 Change dummy address for meek bridge.
See https://gitweb.torproject.org/builders/tor-browser-build.git/commit/projects/tor-browser/Bundle-Data/PTConfigs/bridge_prefs.js?id=8bd845464ae14bf56e0187dfa6f6e773a6593f55
2021-01-26 13:53:51 +00:00
akwizgran
d34d66c691 Update list of obfs4 bridges. 2021-01-26 13:51:41 +00:00
Torsten Grote
6005d156eb Rename ic_lock icon to notification_lock 2021-01-26 08:24:16 -03:00
Torsten Grote
94dd75f24b Use VectorDrawableCompat compatible ways of setting drawables programmatically
so they won't crash on API < 21
2021-01-25 13:34:27 -03:00
Torsten Grote
c93e5441b0 Store rasterized notification icons, because NotificationCompat doesn't handle it
and would crash on API < 21
2021-01-25 13:34:27 -03:00
Torsten Grote
8ec8cc927b Use vector support libraries instead of rasterizing all drawables
https://developer.android.com/guide/topics/graphics/vector-drawable-resources#vector-drawables-backward-solution
2021-01-25 13:34:26 -03:00
410 changed files with 12511 additions and 2502 deletions

View File

@@ -2,6 +2,7 @@ image: briar/ci-image-android:latest
stages:
- test
- optional_tests
- check_reproducibility
test:
@@ -31,3 +32,33 @@ test_reproducible:
- "curl -X POST -F token=${RELEASE_CHECK_TOKEN} -F ref=master -F variables[RELEASE_TAG]=${CI_COMMIT_REF_NAME} https://code.briarproject.org/api/v4/projects/61/trigger/pipeline"
only:
- tags
.optional_tests:
stage: optional_tests
before_script:
- set -e
- export GRADLE_USER_HOME=$PWD/.gradle
cache:
paths:
- .gradle/wrapper
- .gradle/caches
script:
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
after_script:
# these file change every time but should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
manual_tests:
extends: .optional_tests
when: manual
except:
- tags
pre_release_tests:
extends: .optional_tests
only:
- tags

15
.idea/dictionaries/briar.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<component name="ProjectDictionaryState">
<dictionary name="briar">
<words>
<w>briar</w>
<w>briarproject</w>
<w>emoji</w>
<w>encrypter</w>
<w>identicon</w>
<w>introducee</w>
<w>introducees</w>
<w>introducer</w>
<w>onboarding</w>
</words>
</dictionary>
</component>

24
.idea/runConfigurations/BridgeTest.xml generated Normal file
View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BridgeTest" type="AndroidJUnit" factoryName="Android JUnit" nameIsGenerated="true">
<module name="briar.bramble-java" />
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="org.briarproject.bramble.plugin.tor.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="org.briarproject.bramble.plugin.tor" />
<option name="MAIN_CLASS_NAME" value="org.briarproject.bramble.plugin.tor.BridgeTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
<envs>
<env name="OPTIONAL_TESTS" value="org.briarproject.bramble.plugin.tor.BridgeTest" />
</envs>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -11,8 +11,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 10213
versionName "1.2.13"
versionCode 10216
versionName "1.2.16"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -38,8 +38,8 @@ configurations {
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.12@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
tor 'org.briarproject:tor-android:0.3.5.13@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'

View File

@@ -31,6 +31,7 @@ public class AndroidUtils {
private static final String FAKE_BLUETOOTH_ADDRESS = "02:00:00:00:00:00";
private static final String STORED_REPORTS = "dev-reports";
private static final String STORED_LOGCAT = "dev-logcat";
public static Collection<String> getSupportedArchitectures() {
List<String> abis = new ArrayList<>();
@@ -107,6 +108,10 @@ public class AndroidUtils {
return ctx.getDir(STORED_REPORTS, MODE_PRIVATE);
}
public static File getLogcatFile(Context ctx) {
return new File(ctx.getFilesDir(), STORED_LOGCAT);
}
/**
* Returns an array of supported content types for image attachments.
* GIFs can't be compressed on API < 24 so they're not supported.

View File

@@ -75,8 +75,8 @@ dependencyVerification {
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
'org.briarproject:tor-android:0.3.5.12:tor-android-0.3.5.12.zip:db71fb3290acff79d572af0752570eaf6aad7c4d88c9b9aa0b4d5afe2b9ead9c',
'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a:obfs4proxy-android-0.0.12-dev-40245c4a.zip:8ab05a8f8391be2cb5ab2b665c281a06d9e3a756bd0f95a40a36ca927866ea82',
'org.briarproject:tor-android:0.3.5.13:tor-android-0.3.5.13.zip:e0978db136731dae07774b722970cdae1e462fb5adc82845dd80a7e2d87f356c',
'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.8.1:checker-qual-2.8.1.jar:9103499008bcecd4e948da29b17864abb64304e15706444ae209d17ebe0575df',

View File

@@ -9,4 +9,5 @@ public interface FeatureFlags {
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;
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.PublicKey;
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 java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.Map;
@NotNullByDefault
@@ -50,9 +52,11 @@ public interface ClientHelper {
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
throws DbException, FormatException;
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
BdfDictionary query) throws DbException, FormatException;
BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException,
FormatException;
throws DbException, FormatException;
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
throws DbException, FormatException;
@@ -119,4 +123,17 @@ public interface ClientHelper {
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
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

@@ -19,4 +19,10 @@ public interface StreamDecrypterFactory {
*/
StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
SecretKey headerKey);
/**
* Creates a {@link StreamDecrypter} for decrypting a log stream.
*/
StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey);
}

View File

@@ -17,6 +17,12 @@ public interface StreamEncrypterFactory {
* Creates a {@link StreamEncrypter} for encrypting a contact exchange
* stream.
*/
StreamEncrypter createContactExchangeStreamDecrypter(OutputStream out,
StreamEncrypter createContactExchangeStreamEncrypter(OutputStream out,
SecretKey headerKey);
/**
* Creates a {@link StreamEncrypter} for encrypting a log stream.
*/
StreamEncrypter createLogStreamEncrypter(OutputStream out,
SecretKey headerKey);
}

View File

@@ -41,6 +41,18 @@ import javax.annotation.Nullable;
@NotNullByDefault
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.
*
@@ -288,6 +300,16 @@ public interface DatabaseComponent extends TransactionManager {
Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
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.
* <p/>
@@ -314,6 +336,15 @@ public interface DatabaseComponent extends TransactionManager {
Collection<MessageId> getMessagesToShare(Transaction 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(Transaction txn)
throws DbException;
/**
* Returns the metadata for all delivered messages in the given group.
* <p/>
@@ -395,6 +426,15 @@ public interface DatabaseComponent extends TransactionManager {
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
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
* 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)
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.
*/
@@ -557,6 +604,12 @@ public interface DatabaseComponent extends TransactionManager {
*/
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.
*/
@@ -599,6 +652,22 @@ public interface DatabaseComponent extends TransactionManager {
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
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.
*/

View File

@@ -13,4 +13,6 @@ public interface DevConfig {
String getDevOnionAddress();
File getReportDir();
File getLogcatFile();
}

View File

@@ -16,8 +16,13 @@ public interface StreamReaderFactory {
/**
* Creates an {@link InputStream InputStream} for reading from a contact
* exchangestream.
* exchange stream.
*/
InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey);
/**
* Creates an {@link InputStream} for reading from a log stream.
*/
InputStream createLogStreamReader(InputStream in, SecretKey headerKey);
}

View File

@@ -7,17 +7,19 @@ import java.io.OutputStream;
@NotNullByDefault
public interface StreamWriterFactory {
/**
* Creates an {@link OutputStream OutputStream} for writing to a
* transport stream
* Creates a {@link StreamWriter} for writing to a transport stream.
*/
StreamWriter createStreamWriter(OutputStream out, StreamContext ctx);
/**
* Creates an {@link OutputStream OutputStream} for writing to a contact
* exchange stream.
* Creates a {@link StreamWriter} for writing to a contact exchange stream.
*/
StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey);
/**
* Creates a {@link StreamWriter} for writing to a log stream.
*/
StreamWriter createLogStreamWriter(OutputStream out, SecretKey headerKey);
}

View File

@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class ValidationUtils {
@@ -64,4 +66,9 @@ public class ValidationUtils {
if (dictionary != null && dictionary.size() != size)
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;
import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.contact.ContactModule;
import org.briarproject.bramble.crypto.CryptoExecutorModule;
import org.briarproject.bramble.db.DatabaseExecutorModule;
@@ -14,6 +15,8 @@ import org.briarproject.bramble.versioning.VersioningModule;
public interface BrambleCoreEagerSingletons {
void inject(CleanupModule.EagerSingletons init);
void inject(ContactModule.EagerSingletons init);
void inject(CryptoExecutorModule.EagerSingletons init);
@@ -39,6 +42,7 @@ public interface BrambleCoreEagerSingletons {
class Helper {
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
c.inject(new CleanupModule.EagerSingletons());
c.inject(new ContactModule.EagerSingletons());
c.inject(new CryptoExecutorModule.EagerSingletons());
c.inject(new DatabaseExecutorModule.EagerSingletons());

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble;
import org.briarproject.bramble.cleanup.CleanupModule;
import org.briarproject.bramble.client.ClientModule;
import org.briarproject.bramble.connection.ConnectionModule;
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.sync.SyncModule;
import org.briarproject.bramble.sync.validation.ValidationModule;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.bramble.transport.TransportModule;
import org.briarproject.bramble.versioning.VersioningModule;
import dagger.Module;
@Module(includes = {
CleanupModule.class,
ClientModule.class,
ClockModule.class,
ConnectionModule.class,
ContactModule.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.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
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.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
@@ -32,6 +34,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@@ -39,6 +42,7 @@ import java.util.Map.Entry;
import javax.annotation.concurrent.Immutable;
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.AuthorConstants.MAX_AUTHOR_NAME_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);
}
@Override
public Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
BdfDictionary query) throws DbException, FormatException {
return db.getMessageIds(txn, g, metadataEncoder.encode(query));
}
@Override
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException, FormatException {
@@ -389,4 +399,27 @@ class ClientHelperImpl implements ClientHelper {
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

@@ -9,14 +9,16 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.lang.Math.min;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
class ScryptKdf implements PasswordBasedKdf {
private static final Logger LOG =
Logger.getLogger(ScryptKdf.class.getName());
getLogger(ScryptKdf.class.getName());
private static final int MIN_COST = 256; // Min parameter N
private static final int MAX_COST = 1024 * 1024; // Max parameter N
@@ -33,10 +35,20 @@ class ScryptKdf implements PasswordBasedKdf {
@Override
public int chooseCostParameter() {
// Scrypt uses at least 128 * N * r bytes of memory. Don't use more
// than half of the JVM's max heap size or we may run out of memory.
// https://blog.filippo.io/the-scrypt-parameters/
long maxMemory = Runtime.getRuntime().maxMemory();
long maxCost = min(MAX_COST, maxMemory / BLOCK_SIZE / 256);
if (LOG.isLoggable(INFO) && maxCost < MAX_COST) {
LOG.info("Max cost capped at " + maxCost
+ " due to max heap size " + maxMemory);
}
// Increase the cost from min to max while measuring performance
int cost = MIN_COST;
while (cost * 2 <= MAX_COST && measureDuration(cost) * 2 <= TARGET_MS)
while (cost * 2 <= maxCost && measureDuration(cost) * 2 <= TARGET_MS) {
cost *= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("KDF cost parameter " + cost);
return cost;

View File

@@ -36,4 +36,10 @@ class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
SecretKey headerKey) {
return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey);
}
@Override
public StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey) {
return createContactExchangeStreamDecrypter(in, headerKey);
}
}

View File

@@ -51,7 +51,7 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
}
@Override
public StreamEncrypter createContactExchangeStreamDecrypter(
public StreamEncrypter createContactExchangeStreamEncrypter(
OutputStream out, SecretKey headerKey) {
AuthenticatedCipher cipher = cipherProvider.get();
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
@@ -60,4 +60,10 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce,
headerKey, frameKey);
}
@Override
public StreamEncrypter createLogStreamEncrypter(OutputStream out,
SecretKey headerKey) {
return createContactExchangeStreamEncrypter(out, headerKey);
}
}

View File

@@ -497,6 +497,25 @@ interface Database<T> {
*/
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
* 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.
*
* @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.
@@ -671,6 +692,13 @@ interface Database<T> {
*/
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.
*/
@@ -701,9 +729,10 @@ interface Database<T> {
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.
@@ -730,6 +759,22 @@ interface Database<T> {
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
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
* of the given message with respect to the given contact, using the latency

View File

@@ -1,5 +1,6 @@
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.ContactId;
import org.briarproject.bramble.api.contact.PendingContact;
@@ -576,6 +577,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
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
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
throws DbException {
@@ -597,6 +607,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessagesToShare(txn);
}
@Override
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
Transaction transaction) throws DbException {
T txn = unbox(transaction);
return db.getMessagesToDelete(txn);
}
@Override
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
GroupId g) throws DbException {
@@ -692,6 +709,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessageDependents(txn, m);
}
@Override
public long getNextCleanupDeadline(Transaction transaction)
throws DbException {
T txn = unbox(transaction);
return db.getNextCleanupDeadline(txn);
}
@Override
public long getNextSendTime(Transaction transaction, ContactId c)
throws DbException {
@@ -795,8 +819,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
Collection<MessageId> acked = new ArrayList<>();
for (MessageId m : a.getMessageIds()) {
if (db.containsVisibleMessage(txn, c, m)) {
db.raiseSeenFlag(txn, c, m);
acked.add(m);
if (db.raiseSeenFlag(txn, c, 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) {
@@ -952,6 +985,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
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
public void setContactVerified(Transaction transaction, ContactId c)
throws DbException {
@@ -1001,6 +1044,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
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
public void setMessageShared(Transaction transaction, MessageId m)
throws DbException {
@@ -1010,7 +1063,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchMessageException();
if (db.getMessageState(txn, m) != DELIVERED)
throw new IllegalArgumentException("Shared undelivered message");
db.setMessageShared(txn, m);
db.setMessageShared(txn, m, true);
transaction.attach(new MessageSharedEvent(m));
}
@@ -1082,6 +1135,30 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
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
public void updateTransportKeys(Transaction transaction,
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.WARNING;
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.sync.Group.Visibility.INVISIBLE;
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> {
// 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
private static final int OFFSET_PREV = -1;
@@ -180,6 +182,11 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " state INT NOT NULL,"
+ " shared 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,"
+ " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId),"
@@ -336,6 +343,10 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
+ " 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 =
getLogger(JdbcDatabase.class.getName());
@@ -463,7 +474,8 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration43_44(dbTypes),
new Migration44_45(),
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_STATUSES_BY_CONTACT_ID_GROUP_ID);
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
s.executeUpdate(INDEX_MESSAGES_BY_CLEANUP_DEADLINE);
s.close();
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
@@ -1290,7 +1303,9 @@ abstract class JdbcDatabase implements Database<Connection> {
public void deleteMessage(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null;
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.setBytes(1, m.getBytes());
int affected = ps.executeUpdate();
@@ -1769,7 +1784,6 @@ abstract class JdbcDatabase implements Database<Connection> {
// Return early if there are no matches
if (intersection.isEmpty()) return Collections.emptySet();
}
if (intersection == null) throw new AssertionError();
return intersection;
} catch (SQLException e) {
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
public long getNextSendTime(Connection txn, ContactId c)
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
public PendingContact getPendingContact(Connection txn, PendingContactId p)
throws DbException {
@@ -2776,7 +2848,7 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void raiseSeenFlag(Connection txn, ContactId c, MessageId m)
public boolean raiseSeenFlag(Connection txn, ContactId c, MessageId m)
throws DbException {
PreparedStatement ps = null;
try {
@@ -2788,6 +2860,7 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
return affected == 1;
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
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
public void setContactVerified(Connection txn, ContactId c)
throws DbException {
@@ -3128,22 +3220,24 @@ abstract class JdbcDatabase implements Database<Connection> {
}
@Override
public void setMessageShared(Connection txn, MessageId m)
public void setMessageShared(Connection txn, MessageId m, boolean shared)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "UPDATE messages SET shared = TRUE"
String sql = "UPDATE messages SET shared = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setBoolean(1, shared);
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Update denormalised column in statuses
sql = "UPDATE statuses SET messageShared = TRUE"
sql = "UPDATE statuses SET messageShared = ?"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setBoolean(1, shared);
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
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
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
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

@@ -24,15 +24,21 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
@Override
public InputStream createStreamReader(InputStream in, StreamContext ctx) {
return new StreamReaderImpl(
streamDecrypterFactory.createStreamDecrypter(in, ctx));
return new StreamReaderImpl(streamDecrypterFactory
.createStreamDecrypter(in, ctx));
}
@Override
public InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(
streamDecrypterFactory.createContactExchangeStreamDecrypter(in,
headerKey));
return new StreamReaderImpl(streamDecrypterFactory
.createContactExchangeStreamDecrypter(in, headerKey));
}
@Override
public InputStream createLogStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(streamDecrypterFactory
.createLogStreamDecrypter(in, headerKey));
}
}

View File

@@ -26,15 +26,21 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
@Override
public StreamWriter createStreamWriter(OutputStream out,
StreamContext ctx) {
return new StreamWriterImpl(
streamEncrypterFactory.createStreamEncrypter(out, ctx));
return new StreamWriterImpl(streamEncrypterFactory
.createStreamEncrypter(out, ctx));
}
@Override
public StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(
streamEncrypterFactory.createContactExchangeStreamDecrypter(out,
headerKey));
return new StreamWriterImpl(streamEncrypterFactory
.createContactExchangeStreamEncrypter(out, headerKey));
}
}
@Override
public StreamWriter createLogStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(streamEncrypterFactory
.createLogStreamEncrypter(out, headerKey));
}
}

View File

@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
// Metadata keys
String MSG_KEY_UPDATE_VERSION = "version";
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.SHARED;
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_UPDATE_VERSION;
@@ -161,13 +160,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
db.addGroup(txn, g);
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
// Attach the contact ID to the group
BdfDictionary meta = new BdfDictionary();
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
try {
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new AssertionError(e);
}
clientHelper.setContactId(txn, g.getId(), c.getId());
// Create and store the first local update
List<ClientVersion> versions = new ArrayList<>(clients);
Collections.sort(versions);
@@ -229,7 +222,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
Map<ClientMajorVersion, Visibility> after =
getVisibilities(newLocalStates, newRemoteStates);
// 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)) {
Contact contact = db.getContact(txn, c);
callVisibilityHooks(txn, contact, before, after);
@@ -521,17 +514,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
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(
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
Set<ClientMajorVersion> remoteSet = new HashSet<>();

View File

@@ -1,5 +1,14 @@
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
Bridge obfs4 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 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: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 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 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 52.15.78.72:9443 02069A3C5362476936B62BA6F5ACC41ABD573A9B cert=ijYG/OKc7kqu2YzKNFfeXN7/BG2BOgfEP2KyYEiGDQthnHbsOiTWHeIG0WJVW+BckzDgKw iat-mode=0
Bridge obfs4 13.58.29.242:9443 0C58939A77DA6B6B29D4B5236A75865659607AE0 cert=OylWIEHb/ezpq1zWxW0sgKRn+9ARH2eOcQOZ8/Gew+4l+oKOhQ2jUX/Y+FSl61JorXZUWA iat-mode=0
Bridge obfs4 45.33.37.112:9443 60A609BB4ABE8D46E634AE81ED29ADAB7776B399 cert=t5v19WmNv5Sc2YPNr8RQids365W7MY8zJwQVkOxBjUMFomMWARDzsbYpcWLLcw0J9Gm+BQ iat-mode=0
Bridge meek_lite 0.0.2.0:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com

View File

@@ -1,18 +1,18 @@
package org.briarproject.bramble;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.TimeTravelModule;
public interface BrambleCoreIntegrationTestEagerSingletons
extends BrambleCoreEagerSingletons {
void inject(DefaultTaskSchedulerModule.EagerSingletons init);
void inject(TimeTravelModule.EagerSingletons init);
class Helper {
public static void injectEagerSingletons(
BrambleCoreIntegrationTestEagerSingletons 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;
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
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.emptyMap;
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.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
@@ -510,11 +513,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// 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));
exactly(8).of(database).containsGroup(txn, groupId);
exactly(10).of(database).containsGroup(txn, groupId);
will(returnValue(false));
exactly(8).of(database).abortTransaction(txn);
exactly(10).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -523,7 +526,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager);
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getGroup(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -531,7 +534,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getGroupMetadata(transaction, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -539,7 +542,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
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));
fail();
} catch (NoSuchGroupException expected) {
@@ -547,7 +566,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageMetadata(transaction, groupId,
new Metadata()));
fail();
@@ -556,7 +575,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, groupId));
fail();
} catch (NoSuchGroupException expected) {
@@ -594,11 +613,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// 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));
exactly(12).of(database).containsMessage(txn, messageId);
exactly(15).of(database).containsMessage(txn, messageId);
will(returnValue(false));
exactly(12).of(database).abortTransaction(txn);
exactly(15).of(database).abortTransaction(txn);
// Allow other checks to pass
allowing(database).containsContact(txn, contactId);
will(returnValue(true));
@@ -623,7 +642,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessage(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -631,7 +650,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageMetadata(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -639,7 +658,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageState(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -647,7 +666,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -662,6 +681,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// Expected
}
try {
db.transaction(false, transaction ->
db.setCleanupTimerDuration(transaction, message.getId(),
HOURS.toMillis(1)));
fail();
} catch (NoSuchMessageException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.setMessagePermanent(transaction, message.getId()));
@@ -687,7 +715,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageDependencies(transaction, messageId));
fail();
} catch (NoSuchMessageException expected) {
@@ -695,12 +723,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageDependents(transaction, messageId));
fail();
} catch (NoSuchMessageException 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
@@ -981,6 +1025,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
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(TIMER_NOT_STARTED)); // No cleanup duration was set
oneOf(database).commitTransaction(txn);
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
public void testReceiveMessage() throws Exception {
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.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
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.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
@@ -351,7 +352,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
// 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);
assertEquals(singletonList(messageId), ids);
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
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(emptyMap(),
db.getGroupVisibility(txn, groupId));
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
// Make the group visible to the contact
db.addGroupVisibility(txn, contactId, groupId, false);
@@ -655,8 +655,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Make the group invisible again
db.removeGroupVisibility(txn, contactId, groupId);
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
assertEquals(emptyMap(),
db.getGroupVisibility(txn, groupId));
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
db.commitTransaction(txn);
db.close();
@@ -2040,7 +2039,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the message - now it should be sendable immediately
db.setMessageShared(txn, messageId);
db.setMessageShared(txn, messageId, true);
assertEquals(0, db.getNextSendTime(txn, contactId));
// Mark the message as requested - it should still be sendable
@@ -2347,6 +2346,87 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
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 {
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.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule;
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import dagger.Module;
import dagger.Provides;
@@ -12,11 +12,11 @@ import dagger.Provides;
@Module(includes = {
DefaultBatteryManagerModule.class,
DefaultEventExecutorModule.class,
DefaultTaskSchedulerModule.class,
DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class,
TestPluginConfigModule.class,
TestSecureRandomModule.class
TestSecureRandomModule.class,
TimeTravelModule.class
})
public class BrambleCoreIntegrationTestModule {
@@ -33,6 +33,11 @@ public class BrambleCoreIntegrationTestModule {
public boolean shouldEnableProfilePictures() {
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.getMessage;
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_UPDATE_VERSION;
import static org.junit.Assert.assertEquals;
@@ -60,8 +59,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
private final ClientId clientId = getClientId();
private final long now = System.currentTimeMillis();
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() {
context.checking(new Expectations() {{
@@ -123,8 +120,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).addGroup(txn, contactGroup);
oneOf(db).setGroupVisibility(txn, contact.getId(),
contactGroup.getId(), SHARED);
oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(),
groupMeta);
oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
contact.getId());
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
@@ -460,9 +457,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
// Get contact ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(groupMeta));
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
will(returnValue(contact.getId()));
// No states or visibilities have changed
}});
@@ -492,10 +488,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the latest local update
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody));
// Get client ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(groupMeta));
// Get contact ID
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
will(returnValue(contact.getId()));
// No states or visibilities have changed
}});
@@ -546,8 +541,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true));
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate);
@@ -577,9 +570,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false);
// The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(groupMeta));
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
will(returnValue(contact.getId()));
oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
@@ -619,8 +611,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
new BdfEntry(MSG_KEY_LOCAL, true));
BdfDictionary groupMeta = BdfDictionary.of(
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
context.checking(new Expectations() {{
oneOf(clientHelper).toList(newRemoteUpdate);
@@ -650,9 +640,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true, false);
// The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId());
will(returnValue(groupMeta));
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
will(returnValue(contact.getId()));
oneOf(db).getContact(txn, contact.getId());
will(returnValue(contact));
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ public class BridgeTest extends BrambleTestCase {
return component.getCircumventionProvider().getBridges(false);
}
private final static long TIMEOUT = SECONDS.toMillis(30);
private final static long TIMEOUT = SECONDS.toMillis(60);
private final static Logger LOG = getLogger(BridgeTest.class.getName());

View File

@@ -23,8 +23,8 @@ dependencyVerification {
'org.apache.ant:ant-launcher:1.9.4:ant-launcher-1.9.4.jar:7bccea20b41801ca17bcbc909a78c835d0f443f12d639c77bd6ae3d05861608d',
'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8',
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.briarproject:obfs4proxy:0.0.7:obfs4proxy-0.0.7.zip:5b2f693262ce43a7e130f7cc7d5d1617925330640a2eb6d71085e95df8ee0642',
'org.briarproject:tor:0.3.5.12:tor-0.3.5.12.zip:2f542c4befd216f2226bf7c76e3b8b2d99af6f146a8cb28bf727f42014587006',
'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766',
'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-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0 0,1 16,4V6H14V4H5V20H14V18H16V20A2,2 0 0,1 14,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2H14Z" /></svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@@ -22,9 +22,11 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 10213
versionName "1.2.13"
versionCode 10216
versionName "1.2.16"
applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true
buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
def now = (long) (System.currentTimeMillis() / 1000)
@@ -93,6 +95,7 @@ dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
implementation project(':bramble-android')
implementation 'androidx.fragment:fragment:1.3.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.exifinterface:exifinterface:1.3.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
@@ -134,6 +137,7 @@ dependencies {
testImplementation "org.jmock:jmock:$jmockVersion"
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
testImplementation "org.jmock:jmock-legacy:$jmockVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
androidTestImplementation project(path: ':bramble-api', configuration: 'testOutput')
androidTestImplementation 'androidx.test.ext:junit:1.1.2'

View File

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

View File

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

View File

@@ -29,11 +29,13 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -41,6 +43,7 @@ import org.briarproject.briar.api.android.DozeWatchdog;
import org.briarproject.briar.api.android.LockManager;
import org.briarproject.briar.api.android.ScreenFilterMonitor;
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.BlogPostFactory;
import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -75,6 +78,7 @@ import dagger.Component;
BriarAccountModule.class,
AppModule.class,
AttachmentModule.class,
ClockModule.class,
MediaModule.class
})
public interface AndroidComponent
@@ -179,6 +183,12 @@ public interface AndroidComponent
AndroidWakeLockManager wakeLockManager();
CachingLogHandler logHandler();
Thread.UncaughtExceptionHandler exceptionHandler();
AutoDeleteManager autoDeleteManager();
void inject(SignInReminderReceiver 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.api.android.AndroidNotificationManager;
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.forum.event.ForumPostReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
@@ -226,6 +227,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent<?> p =
(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());
} else if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
@@ -255,7 +262,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
R.string.ongoing_notification_title;
int text = locked ? R.string.lock_tap_to_unlock :
R.string.ongoing_notification_text;
int icon = locked ? R.drawable.startup_lock :
int icon = locked ? R.drawable.notification_lock :
R.drawable.notification_ongoing;
// Ongoing foreground notification that shows BriarService is running
NotificationCompat.Builder b =
@@ -624,7 +631,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext, REMINDER_CHANNEL_ID);
b.setSmallIcon(R.drawable.ic_signout);
b.setSmallIcon(R.drawable.notification_signout);
b.setColor(getColor(appContext, R.color.briar_primary));
b.setContentTitle(
appContext.getText(R.string.reminder_notification_title));

View File

@@ -33,12 +33,13 @@ import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.logging.LoggingModule;
import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.reporting.DevReportModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.test.TestAvatarCreatorImpl;
import org.briarproject.briar.android.viewmodel.ViewModelModule;
@@ -74,6 +75,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
SetupModule.class,
DozeHelperModule.class,
ContactExchangeModule.class,
LoggingModule.class,
LoginModule.class,
NavDrawerModule.class,
ViewModelModule.class,
@@ -192,6 +194,11 @@ public class AppModule {
public File getReportDir() {
return AndroidUtils.getReportDir(app.getApplicationContext());
}
@Override
public File getLogcatFile() {
return AndroidUtils.getLogcatFile(app.getApplicationContext());
}
};
return devConfig;
}
@@ -276,6 +283,11 @@ public class AppModule {
public boolean shouldEnableProfilePictures() {
return IS_DEBUG_BUILD;
}
@Override
public boolean shouldEnableDisappearingMessages() {
return IS_DEBUG_BUILD;
}
};
}
}

View File

@@ -6,9 +6,6 @@ import android.content.SharedPreferences;
import org.briarproject.bramble.BrambleApplication;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import java.util.Collection;
import java.util.logging.LogRecord;
/**
* This exists so that the Application object will not necessarily be cast
* directly to the Briar application object.
@@ -17,8 +14,6 @@ public interface BriarApplication extends BrambleApplication {
Class<? extends Activity> ENTRY_ACTIVITY = NavDrawerActivity.class;
Collection<LogRecord> getRecentLogRecords();
AndroidComponent getApplicationComponent();
SharedPreferences getDefaultSharedPreferences();

View File

@@ -20,12 +20,10 @@ import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.R;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
import org.briarproject.briar.android.util.UiUtils;
import java.util.Collection;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import androidx.annotation.NonNull;
@@ -42,22 +40,18 @@ public class BriarApplicationImpl extends Application
private static final Logger LOG =
getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler();
private final BriarExceptionHandler exceptionHandler =
new BriarExceptionHandler(this);
private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs;
@Override
protected void attachBaseContext(Context base) {
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
if (prefs == null)
prefs = PreferenceManager.getDefaultSharedPreferences(base);
// Loading the language needs to be done here.
Localizer.initialize(prefs);
super.attachBaseContext(
Localizer.getInstance().setLocale(base));
Localizer.getInstance().setLocale(this);
setTheme(base, prefs);
}
@@ -67,6 +61,11 @@ public class BriarApplicationImpl extends Application
if (IS_DEBUG_BUILD) enableStrictMode();
applicationComponent = createApplicationComponent();
UncaughtExceptionHandler exceptionHandler =
applicationComponent.exceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
Logger rootLogger = getLogger("");
Handler[] handlers = rootLogger.getHandlers();
// Disable the Android logger for release builds
@@ -78,12 +77,12 @@ public class BriarApplicationImpl extends Application
// Restore the default handlers after the level raising handler
for (Handler handler : handlers) rootLogger.addHandler(handler);
}
CachingLogHandler logHandler = applicationComponent.logHandler();
rootLogger.addHandler(logHandler);
rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO);
LOG.info("Created");
applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider());
}
@@ -136,11 +135,6 @@ public class BriarApplicationImpl extends Application
return applicationComponent;
}
@Override
public Collection<LogRecord> getRecentLogRecords() {
return logHandler.getRecentLogRecords();
}
@Override
public AndroidComponent getApplicationComponent() {
return applicationComponent;

View File

@@ -37,7 +37,7 @@ import javax.inject.Inject;
import androidx.core.app.NotificationCompat;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.ACTION_SHUTDOWN;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
@@ -46,6 +46,7 @@ import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Process.myPid;
import static androidx.core.app.NotificationCompat.VISIBILITY_SECRET;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -56,8 +57,10 @@ import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_NOTIFICATION_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_OLD_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_NOTIFICATION_ID;
import static org.briarproject.briar.api.android.LockManager.ACTION_LOCK;
import static org.briarproject.briar.api.android.LockManager.EXTRA_PID;
public class BriarService extends Service {
@@ -120,11 +123,17 @@ public class BriarService extends Service {
if (SDK_INT >= 26) {
NotificationManager nm = (NotificationManager)
requireNonNull(getSystemService(NOTIFICATION_SERVICE));
// Delete the old notification channel, which had
// IMPORTANCE_NONE and showed a badge
nm.deleteNotificationChannel(ONGOING_CHANNEL_OLD_ID);
// Use IMPORTANCE_LOW so the system doesn't show its own
// notification on API 26-27
NotificationChannel ongoingChannel = new NotificationChannel(
ONGOING_CHANNEL_ID,
getString(R.string.ongoing_notification_title),
IMPORTANCE_NONE);
IMPORTANCE_LOW);
ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET);
ongoingChannel.setShowBadge(false);
nm.createNotificationChannel(ongoingChannel);
NotificationChannel failureChannel = new NotificationChannel(
FAILURE_CHANNEL_ID,
@@ -170,6 +179,7 @@ public class BriarService extends Service {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(Localizer.getInstance().setLocale(base));
Localizer.getInstance().setLocale(this);
}
private void showStartupFailureNotification(StartResult result) {
@@ -202,7 +212,12 @@ public class BriarService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (ACTION_LOCK.equals(intent.getAction())) {
lockManager.setLocked(true);
int pid = intent.getIntExtra(EXTRA_PID, -1);
if (pid == myPid()) lockManager.setLocked(true);
else if (LOG.isLoggable(WARNING)) {
LOG.warning("Tried to lock process " + pid + " but this is " +
myPid());
}
}
return START_NOT_STICKY; // Don't restart automatically if killed
}

View File

@@ -68,7 +68,21 @@ public class Localizer {
return new Locale(tag);
}
// Returns the localized version of context
/*
* Apply localization to the specified context.
*
* It updates the configuration of the context's resources object but can
* also return a new context derived from the context parameter. Hence
* make sure to work with the return value of this method instead of
* the context you passed as a parameter.
*
* This method also has side-effects as it calls Locale#setDefault().
*
* When using this in attachBaseContext() of Application, Service or
* Activity subclasses, it is important to not only apply this method to the
* base Context parameter received in that method, but also apply it on the
* class itself which also extends Context.
*/
public Context setLocale(Context context) {
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
@@ -82,7 +96,7 @@ public class Localizer {
Locale.setDefault(locale);
if (SDK_INT >= 17) {
conf.setLocale(locale);
context.createConfigurationContext(conf);
context = context.createConfigurationContext(conf);
} else
conf.locale = locale;
//noinspection deprecation

View File

@@ -32,8 +32,10 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.app.AlarmManager.ELAPSED_REALTIME;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.app.PendingIntent.getService;
import static android.content.Context.ALARM_SERVICE;
import static android.os.Process.myPid;
import static android.os.SystemClock.elapsedRealtime;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.WARNING;
@@ -75,23 +77,25 @@ public class LockManagerImpl implements LockManager, Service, EventListener {
LockManagerImpl(Application app, SettingsManager settingsManager,
AndroidNotificationManager notificationManager,
@DatabaseExecutor Executor dbExecutor) {
this.appContext = app.getApplicationContext();
appContext = app.getApplicationContext();
this.settingsManager = settingsManager;
this.notificationManager = notificationManager;
this.dbExecutor = dbExecutor;
this.alarmManager =
alarmManager =
(AlarmManager) appContext.getSystemService(ALARM_SERVICE);
Intent i =
new Intent(ACTION_LOCK, null, appContext, BriarService.class);
this.lockIntent = getService(appContext, 0, i, 0);
this.timeoutNever = Integer.valueOf(
i.putExtra(EXTRA_PID, myPid());
// When not using FLAG_UPDATE_CURRENT, the intent might have no extras
lockIntent = getService(appContext, 0, i, FLAG_UPDATE_CURRENT);
timeoutNever = Integer.parseInt(
appContext.getString(R.string.pref_lock_timeout_value_never));
this.timeoutDefault = Integer.valueOf(
timeoutDefault = Integer.parseInt(
appContext.getString(R.string.pref_lock_timeout_value_default));
this.timeoutMinutes = timeoutNever;
timeoutMinutes = timeoutNever;
// setting this in the constructor makes #getValue() @NonNull
this.lockable.setValue(false);
lockable.setValue(false);
}
@Override
@@ -148,7 +152,7 @@ public class LockManagerImpl implements LockManager, Service, EventListener {
boolean oldValue = lockable.getValue();
boolean newValue = hasScreenLock(appContext) && lockableSetting;
if (oldValue != newValue) {
this.lockable.setValue(newValue);
lockable.setValue(newValue);
}
}

View File

@@ -77,7 +77,7 @@ public class UnlockActivity extends BaseActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
@Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_KEYGUARD_UNLOCK) {
if (resultCode == RESULT_OK) unlock();

View File

@@ -27,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.conversation.AliasDialogFragment;
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.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity;
@@ -238,4 +239,6 @@ public interface ActivityComponent {
void inject(ConfirmAvatarDialogFragment fragment);
void inject(ConversationSettingsDialog dialog);
}

View File

@@ -109,6 +109,7 @@ public abstract class BaseActivity extends AppCompatActivity
protected void attachBaseContext(Context base) {
super.attachBaseContext(
Localizer.getInstance().setLocale(base));
Localizer.getInstance().setLocale(this);
}
public ActivityComponent getActivityComponent() {

View File

@@ -20,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List;
@@ -27,6 +28,9 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
@@ -34,6 +38,7 @@ import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -121,8 +126,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {
@@ -132,6 +137,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
}
});
finish();
return new MutableLiveData<>(SENT);
}
private void showProgressBar() {

View File

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

View File

@@ -34,7 +34,6 @@ import io.github.kobakei.materialfabspeeddial.FabSpeedDial;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListener;
import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@MethodsNotNullByDefault
@@ -102,7 +101,8 @@ public class ContactListFragment extends BaseFragment
.observe(getViewLifecycleOwner(), result -> {
result.onError(this::handleException).onSuccess(items -> {
adapter.submitList(items);
if (requireNonNull(items).size() == 0) list.showData();
// TODO remove when BriarRecyclerView was adapted
list.showData();
});
});
viewModel.getHasPendingContacts()

View File

@@ -124,7 +124,7 @@ public class AddContactViewModel extends DbViewModel {
return addContactResult;
}
public void updatePendingContact(String name, PendingContact p) {
void updatePendingContact(String name, PendingContact p) {
runOnDbThread(() -> {
try {
contactManager.removePendingContact(p.getId());

View File

@@ -29,11 +29,11 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@@ -48,6 +48,7 @@ import static org.briarproject.briar.android.util.UiUtils.getDialogIcon;
public class NicknameFragment extends BaseFragment {
private static final String TAG = NicknameFragment.class.getName();
private static final String SAVED_LINK = "savedLink";
@Inject
ViewModelProvider.Factory viewModelFactory;
@@ -67,6 +68,20 @@ public class NicknameFragment extends BaseFragment {
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(AddContactViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// When the activity (and the ViewModel) get destroyed,
// the link will not be available anymore and needs to be restored.
// TODO migrate to SavedStateViewModelFactory (once we can use it)
String savedLink = savedInstanceState.getString(SAVED_LINK);
if (savedLink != null) viewModel.setRemoteHandshakeLink(savedLink);
}
}
@Nullable
@@ -74,14 +89,9 @@ public class NicknameFragment extends BaseFragment {
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (getActivity() == null || getContext() == null) return null;
View v = inflater.inflate(R.layout.fragment_nickname,
container, false);
viewModel = ViewModelProviders.of(getActivity(), viewModelFactory)
.get(AddContactViewModel.class);
contactNameLayout = v.findViewById(R.id.contactNameLayout);
contactNameInput = v.findViewById(R.id.contactNameInput);
@@ -93,6 +103,12 @@ public class NicknameFragment extends BaseFragment {
return v;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(SAVED_LINK, viewModel.getRemoteHandshakeLink());
}
@Nullable
private String getNicknameOrNull() {
Editable text = contactNameInput.getText();

View File

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

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.transition.Slide;
@@ -49,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.TextCache;
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.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
@@ -58,8 +60,10 @@ import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView;
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.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
@@ -105,6 +109,7 @@ import androidx.recyclerview.selection.SelectionTracker.SelectionObserver;
import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import de.hdodenhof.circleimageview.CircleImageView;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
@@ -136,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.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.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextCache,
AttachmentCache, AttachmentListener, ActionMode.Callback {
implements BaseFragmentListener, EventListener, ConversationListener,
TextCache, AttachmentCache, AttachmentListener, ActionMode.Callback {
public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -266,15 +273,11 @@ public class ConversationActivity extends BriarActivity
ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView,
imagePreview, this, viewModel);
viewModel.hasImageSupport().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(@Nullable Boolean hasSupport) {
if (hasSupport != null && hasSupport) {
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController)
.setImagesSupported();
viewModel.hasImageSupport().removeObserver(this);
}
observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
if (format != TEXT_ONLY) {
// TODO: remove cast when removing feature flag
((TextAttachmentController) sendController)
.setImagesSupported();
}
});
} else {
@@ -284,6 +287,9 @@ public class ConversationActivity extends BriarActivity
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
viewModel.getAutoDeleteTimer().observe(this, timer ->
sendController.setAutoDeleteTimer(timer));
}
private void scrollToBottom() {
@@ -367,6 +373,14 @@ public class ConversationActivity extends BriarActivity
// enable alias action if available
observeOnce(viewModel.getContactItem(), this, contact ->
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);
}
@@ -388,6 +402,10 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG);
return true;
case R.id.action_conversation_settings:
if (contactId == null) return false;
onAutoDeleteTimerNoticeClicked();
return true;
case R.id.action_delete_all_messages:
askToDeleteAllMessages();
return true;
@@ -479,12 +497,10 @@ public class ConversationActivity extends BriarActivity
@UiThread
private void displayContactOnlineStatus() {
if (connectionRegistry.isConnected(contactId)) {
toolbarStatus.setImageDrawable(ContextCompat.getDrawable(
ConversationActivity.this, R.drawable.contact_online));
toolbarStatus.setImageResource(R.drawable.contact_online);
toolbarStatus.setContentDescription(getString(R.string.online));
} else {
toolbarStatus.setImageDrawable(ContextCompat.getDrawable(
ConversationActivity.this, R.drawable.contact_offline));
toolbarStatus.setImageResource(R.drawable.contact_offline);
toolbarStatus.setContentDescription(getString(R.string.offline));
}
}
@@ -559,7 +575,7 @@ public class ConversationActivity extends BriarActivity
this::showImageOnboarding);
}
List<ConversationItem> items = createItems(headers);
adapter.addAll(items);
adapter.replaceAll(items);
list.showData();
if (layoutManagerState == null) {
scrollToBottom();
@@ -640,8 +656,8 @@ public class ConversationActivity extends BriarActivity
supportFinishAfterTransition();
}
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent p =
(ConversationMessageReceivedEvent) e;
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
if (p.getContactId().equals(contactId)) {
LOG.info("Message received, adding");
onNewConversationMessage(p.getMessageHeader());
@@ -658,6 +674,13 @@ public class ConversationActivity extends BriarActivity
LOG.info("Messages acked");
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) {
ContactConnectedEvent c = (ContactConnectedEvent) e;
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
private void markMessages(Collection<MessageId> messageIds, boolean sent,
boolean seen) {
@@ -735,20 +765,13 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders,
long expectedAutoDeleteTimer) {
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
throw new AssertionError();
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
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;
return viewModel
.sendMessage(text, attachmentHeaders, expectedAutoDeleteTimer);
}
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
@@ -823,10 +846,6 @@ public class ConversationActivity extends BriarActivity
fails.add(getString(
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
if (result.hasNotAllIntroductionSelected() &&
result.hasNotAllInvitationSelected()) {
@@ -935,13 +954,16 @@ public class ConversationActivity extends BriarActivity
return;
}
int color =
ContextCompat.getColor(this, R.color.briar_primary);
Drawable drawable = VectorDrawableCompat
.create(getResources(), R.drawable.ic_more_vert_accent, null);
new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent)
.setBackgroundColour(
ContextCompat.getColor(this, R.color.briar_primary))
.setIconDrawable(drawable)
.setBackgroundColour(color)
.show();
}
@@ -955,13 +977,11 @@ public class ConversationActivity extends BriarActivity
adapter.notifyItemChanged(position, item);
}
runOnDbThread(() -> {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try {
switch (item.getRequestType()) {
case INTRODUCTION:
respondToIntroductionRequest(item.getSessionId(),
accept, timestamp);
accept);
break;
case FORUM:
respondToForumRequest(item.getSessionId(), accept);
@@ -1035,11 +1055,18 @@ public class ConversationActivity extends BriarActivity
ActivityCompat.startActivity(this, i, options.toBundle());
}
@Override
public void onAutoDeleteTimerNoticeClicked() {
ConversationSettingsDialog dialog =
ConversationSettingsDialog.newInstance(contactId);
dialog.show(getSupportFragmentManager(),
ConversationSettingsDialog.TAG);
}
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, time,
accept);
boolean accept) throws DbException {
introductionManager.respondToIntroduction(contactId, sessionId, accept);
}
@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.ItemReturningAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@NotNullByDefault
class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
implements ItemReturningAdapter<ConversationItem> {
private ConversationListener listener;
private final ConversationListener listener;
private final RecycledViewPool imageViewPool;
private final ImageItemDecoration imageItemDecoration;
@Nullable
@@ -65,22 +71,20 @@ class ConversationAdapter
@LayoutRes int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
type, viewGroup, false);
switch (type) {
case R.layout.list_item_conversation_msg_in:
return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_msg_out:
return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration);
case R.layout.list_item_conversation_notice_in:
return new ConversationNoticeViewHolder(v, listener, true);
case R.layout.list_item_conversation_notice_out:
return new ConversationNoticeViewHolder(v, listener, false);
case R.layout.list_item_conversation_request:
return new ConversationRequestViewHolder(v, listener, true);
default:
throw new IllegalArgumentException("Unknown ConversationItem");
if (type == R.layout.list_item_conversation_msg_in) {
return new ConversationMessageViewHolder(v, listener, true,
imageViewPool, imageItemDecoration);
} else if (type == R.layout.list_item_conversation_msg_out) {
return new ConversationMessageViewHolder(v, listener, false,
imageViewPool, imageItemDecoration);
} else if (type == R.layout.list_item_conversation_notice_in) {
return new ConversationNoticeViewHolder(v, listener, true);
} else if (type == R.layout.list_item_conversation_notice_out) {
return new ConversationNoticeViewHolder(v, listener, false);
} else if (type == R.layout.list_item_conversation_request) {
return new ConversationRequestViewHolder(v, listener, true);
}
throw new IllegalArgumentException("Unknown ConversationItem");
}
@Override
@@ -107,22 +111,65 @@ class ConversationAdapter
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) {
this.tracker = tracker;
}
@Nullable
ConversationItem getLastItem() {
if (items.size() > 0) {
return items.get(items.size() - 1);
} else {
return null;
}
}
SparseArray<ConversationItem> getOutgoingMessages() {
SparseArray<ConversationItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (!item.isIncoming()) {

View File

@@ -9,6 +9,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
import static org.briarproject.bramble.util.StringUtils.toHexString;
@@ -22,20 +23,25 @@ abstract class ConversationItem {
protected String text;
private final MessageId id;
private final GroupId groupId;
private final long time;
private final long time, autoDeleteTimer;
private final boolean isIncoming;
private boolean read, sent, seen;
private final LiveData<String> contactName;
private boolean read, sent, seen, showTimerNotice;
ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h) {
ConversationItem(@LayoutRes int layoutRes, ConversationMessageHeader h,
LiveData<String> contactName) {
this.layoutRes = layoutRes;
this.text = null;
this.id = h.getId();
this.groupId = h.getGroupId();
this.time = h.getTimestamp();
this.autoDeleteTimer = h.getAutoDeleteTimer();
this.read = h.isRead();
this.sent = h.isSent();
this.seen = h.isSeen();
this.isIncoming = !h.isLocal();
this.contactName = contactName;
this.showTimerNotice = false;
}
@LayoutRes
@@ -68,6 +74,10 @@ abstract class ConversationItem {
return time;
}
public long getAutoDeleteTimer() {
return autoDeleteTimer;
}
/**
* Only useful for incoming messages.
*/
@@ -111,4 +121,25 @@ abstract class ConversationItem {
return isIncoming;
}
public LiveData<String> getContactName() {
return contactName;
}
/**
* Set this to true when {@link #getAutoDeleteTimer()} has changed
* since the last message from the same peer.
*
* @return true if the value was set, false if it was already set.
*/
boolean setTimerNoticeVisible(boolean visible) {
if (this.showTimerNotice != visible) {
this.showTimerNotice = visible;
return true;
}
return false;
}
boolean isTimerNoticeVisible() {
return showTimerNotice;
}
}

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -12,8 +14,12 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.util.StringUtils.trim;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
import static org.briarproject.briar.android.util.UiUtils.formatDuration;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@UiThread
@NotNullByDefault
@@ -24,8 +30,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
protected final ConstraintLayout layout;
@Nullable
private final OutItemViewHolder outViewHolder;
private final TextView text;
private final TextView topNotice, text;
protected final TextView time;
protected final ImageView bomb;
@Nullable
private String itemKey = null;
@@ -33,11 +40,13 @@ abstract class ConversationItemViewHolder extends ViewHolder {
boolean isIncoming) {
super(v);
this.listener = listener;
this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
root = v;
topNotice = v.findViewById(R.id.topNotice);
layout = v.findViewById(R.id.layout);
text = v.findViewById(R.id.text);
time = v.findViewById(R.id.time);
bomb = v.findViewById(R.id.bomb);
}
@CallSuper
@@ -45,6 +54,8 @@ abstract class ConversationItemViewHolder extends ViewHolder {
itemKey = item.getKey();
root.setActivated(selected);
setTopNotice(item);
if (item.getText() != null) {
text.setText(trim(item.getText()));
}
@@ -52,6 +63,9 @@ abstract class ConversationItemViewHolder extends ViewHolder {
long timestamp = item.getTime();
time.setText(formatDate(time.getContext(), timestamp));
boolean showBomb = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
bomb.setVisibility(showBomb ? VISIBLE : GONE);
if (outViewHolder != null) outViewHolder.bind(item);
}
@@ -64,4 +78,35 @@ abstract class ConversationItemViewHolder extends ViewHolder {
return itemKey;
}
private void setTopNotice(ConversationItem item) {
if (item.isTimerNoticeVisible()) {
Context ctx = itemView.getContext();
topNotice.setVisibility(VISIBLE);
boolean enabled = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
String duration = enabled ?
formatDuration(ctx, item.getAutoDeleteTimer()) : "";
String tapToLearnMore = ctx.getString(R.string.tap_to_learn_more);
String text;
if (item.isIncoming()) {
String name = item.getContactName().getValue();
text = enabled ?
ctx.getString(R.string.auto_delete_msg_contact_enabled,
name, duration, tapToLearnMore) :
ctx.getString(R.string.auto_delete_msg_contact_disabled,
name, tapToLearnMore);
} else {
text = enabled ?
ctx.getString(R.string.auto_delete_msg_you_enabled,
duration, tapToLearnMore) :
ctx.getString(R.string.auto_delete_msg_you_disabled,
tapToLearnMore);
}
topNotice.setText(text);
topNotice.setOnClickListener(
v -> listener.onAutoDeleteTimerNoticeClicked());
} else {
topNotice.setVisibility(GONE);
}
}
}

View File

@@ -18,4 +18,6 @@ interface ConversationListener {
void onAttachmentClicked(View view, ConversationMessageItem messageItem,
AttachmentItem attachmentItem);
void onAutoDeleteTimerNoticeClicked();
}

View File

@@ -10,6 +10,7 @@ import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -18,8 +19,8 @@ class ConversationMessageItem extends ConversationItem {
private final List<AttachmentItem> attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
List<AttachmentItem> attachments) {
super(layoutRes, h);
LiveData<String> contactName, List<AttachmentItem> attachments) {
super(layoutRes, h, contactName);
this.attachments = attachments;
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.conversation;
import android.content.res.ColorStateList;
import android.view.View;
import android.view.ViewGroup;
@@ -14,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.widget.ImageViewCompat.setImageTintList;
@UiThread
@NotNullByDefault
@@ -84,6 +86,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
if (item.getText() == null) {
statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
time.setTextColor(timeColorBubble);
setImageTintList(bomb, ColorStateList.valueOf(timeColorBubble));
constraintSet = imageConstraints;
} else {
resetStatusLayoutForText();
@@ -111,6 +114,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
// also reset padding (the background drawable defines some)
statusLayout.setPadding(0, 0, 0, 0);
time.setTextColor(timeColor);
setImageTintList(bomb, ColorStateList.valueOf(timeColor));
}
}

View File

@@ -8,6 +8,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -17,15 +18,15 @@ class ConversationNoticeItem extends ConversationItem {
private final String msgText;
ConversationNoticeItem(@LayoutRes int layoutRes, String text,
ConversationRequest r) {
super(layoutRes, r);
LiveData<String> contactName, ConversationRequest<?> r) {
super(layoutRes, r, contactName);
this.text = text;
this.msgText = r.getText();
}
ConversationNoticeItem(@LayoutRes int layoutRes, String text,
ConversationResponse r) {
super(layoutRes, r);
LiveData<String> contactName, ConversationResponse r) {
super(layoutRes, r, contactName);
this.text = text;
this.msgText = null;
}

View File

@@ -11,6 +11,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.lifecycle.LiveData;
@NotThreadSafe
@NotNullByDefault
@@ -26,14 +27,15 @@ class ConversationRequestItem extends ConversationNoticeItem {
private boolean answered;
ConversationRequestItem(@LayoutRes int layoutRes, String text,
RequestType type, ConversationRequest r) {
super(layoutRes, text, r);
LiveData<String> contactName, RequestType type,
ConversationRequest<?> r) {
super(layoutRes, text, contactName, r);
this.requestType = type;
this.sessionId = r.getSessionId();
this.answered = r.wasAnswered();
if (r instanceof InvitationRequest) {
this.requestedGroupId = ((Shareable) r.getNameable()).getId();
this.canBeOpened = ((InvitationRequest) r).canBeOpened();
this.canBeOpened = ((InvitationRequest<?>) r).canBeOpened();
} else {
this.requestedGroupId = null;
this.canBeOpened = false;

View File

@@ -0,0 +1,123 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.widget.OnboardingFullDialogFragment;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConversationSettingsDialog extends DialogFragment {
final static String TAG = ConversationSettingsDialog.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject
ViewModelProvider.Factory viewModelFactory;
private ConversationViewModel viewModel;
static ConversationSettingsDialog newInstance(ContactId contactId) {
Bundle args = new Bundle();
args.putInt(CONTACT_ID, contactId.getInt());
ConversationSettingsDialog dialog = new ConversationSettingsDialog();
dialog.setArguments(args);
return dialog;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
injectFragment(((BaseFragment.BaseFragmentListener) context)
.getActivityComponent());
}
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(ConversationViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME,
R.style.BriarFullScreenDialogTheme);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_conversation_settings,
container, false);
Bundle args = requireArguments();
int id = args.getInt(CONTACT_ID, -1);
if (id == -1) throw new IllegalStateException();
ContactId contactId = new ContactId(id);
FragmentActivity activity = requireActivity();
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(ConversationViewModel.class);
viewModel.setContactId(contactId);
Toolbar toolbar = view.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v -> dismiss());
SwitchCompat switchDisappearingMessages = view.findViewById(
R.id.switchDisappearingMessages);
switchDisappearingMessages.setOnCheckedChangeListener(
(button, value) -> viewModel.setAutoDeleteTimerEnabled(value));
Button buttonLearnMore =
view.findViewById(R.id.buttonLearnMore);
buttonLearnMore.setOnClickListener(e -> showLearnMoreDialog());
viewModel.getAutoDeleteTimer()
.observe(getViewLifecycleOwner(), timer -> {
LOG.info("Received auto delete timer: " + timer);
boolean disappearingMessages =
timer != NO_AUTO_DELETE_TIMER;
switchDisappearingMessages
.setChecked(disappearingMessages);
switchDisappearingMessages.setEnabled(true);
});
return view;
}
private void showLearnMoreDialog() {
OnboardingFullDialogFragment.newInstance(
R.string.disappearing_messages_title,
R.string.disappearing_messages_explanation_long
).show(getChildFragmentManager(), OnboardingFullDialogFragment.TAG);
}
}

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
@@ -28,16 +29,22 @@ import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.attachment.AttachmentRetriever;
import org.briarproject.briar.android.contact.ContactItem;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.view.TextSendController.SendState;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.autodelete.UnexpectedTimerException;
import org.briarproject.briar.api.autodelete.event.AutoDeleteTimerMirroredEvent;
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageFormat;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
@@ -55,6 +62,7 @@ import androidx.lifecycle.MutableLiveData;
import static androidx.lifecycle.Transformations.map;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
@@ -62,6 +70,13 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.android.view.TextSendController.SendState.ERROR;
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
import static org.briarproject.briar.android.view.TextSendController.SendState.UNEXPECTED_TIMER;
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.autodelete.AutoDeleteManager.DEFAULT_TIMER_DURATION;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES;
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
@NotNullByDefault
public class ConversationViewModel extends DbViewModel
@@ -84,6 +99,8 @@ public class ConversationViewModel extends DbViewModel
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentRetriever attachmentRetriever;
private final AttachmentCreator attachmentCreator;
private final AutoDeleteManager autoDeleteManager;
private final ConversationManager conversationManager;
@Nullable
private ContactId contactId = null;
@@ -92,7 +109,7 @@ public class ConversationViewModel extends DbViewModel
private final LiveData<String> contactName = map(contactItem, c ->
UiUtils.getContactDisplayName(c.getContact()));
private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport =
private final MutableLiveData<PrivateMessageFormat> privateMessageFormat =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding =
new MutableLiveEvent<>();
@@ -100,8 +117,10 @@ public class ConversationViewModel extends DbViewModel
new MutableLiveEvent<>();
private final MutableLiveData<Boolean> showIntroductionAction =
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
private final MutableLiveData<Long> autoDeleteTimer =
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>(false);
private final MutableLiveEvent<PrivateMessageHeader> addedHeader =
new MutableLiveEvent<>();
@@ -118,7 +137,9 @@ public class ConversationViewModel extends DbViewModel
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
AttachmentCreator attachmentCreator,
AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.db = db;
this.eventBus = eventBus;
@@ -129,10 +150,10 @@ public class ConversationViewModel extends DbViewModel
this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
messagingGroupId = map(contactItem, c ->
messagingManager.getContactGroup(c.getContact()).getId());
contactDeleted.setValue(false);
eventBus.addListener(this);
}
@@ -152,6 +173,11 @@ public class ConversationViewModel extends DbViewModel
runOnDbThread(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId()));
}
} else if (e instanceof AutoDeleteTimerMirroredEvent) {
AutoDeleteTimerMirroredEvent a = (AutoDeleteTimerMirroredEvent) e;
if (a.getContactId().equals(contactId)) {
autoDeleteTimer.postValue(a.getNewTimer());
}
} else if (e instanceof AvatarUpdatedEvent) {
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
if (a.getContactId().equals(contactId)) {
@@ -201,6 +227,11 @@ public class ConversationViewModel extends DbViewModel
contactItem.postValue(new ContactItem(c, authorInfo));
logDuration(LOG, "Loading contact", start);
start = now();
long timer = db.transactionWithResult(true, txn ->
autoDeleteManager.getAutoDeleteTimer(txn, contactId));
autoDeleteTimer.postValue(timer);
logDuration(LOG, "Getting auto-delete timer", start);
start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) {
@@ -235,20 +266,6 @@ public class ConversationViewModel extends DbViewModel
});
}
@UiThread
void sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long timestamp) {
// messagingGroupId is loaded with the contact
observeForeverOnce(messagingGroupId, groupId -> {
requireNonNull(groupId);
observeForeverOnce(imageSupport, hasImageSupport -> {
requireNonNull(hasImageSupport);
createMessage(groupId, text, headers, timestamp,
hasImageSupport);
});
});
}
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
@@ -275,10 +292,12 @@ public class ConversationViewModel extends DbViewModel
@DatabaseExecutor
private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
// check if images are supported
boolean imagesSupported = db.transactionWithResult(true, txn ->
messagingManager.contactSupportsImages(txn, c));
imageSupport.postValue(imagesSupported);
// check if images and auto-deletion are supported
PrivateMessageFormat format = db.transactionWithResult(true, txn ->
messagingManager.getContactMessageFormat(txn, c));
if (LOG.isLoggable(INFO))
LOG.info("PrivateMessageFormat loaded: " + format.name());
privateMessageFormat.postValue(format);
// check if introductions are supported
Collection<Contact> contacts = contactManager.getContacts();
@@ -287,7 +306,7 @@ public class ConversationViewModel extends DbViewModel
// we only show one onboarding dialog at a time
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
if (imagesSupported &&
if (format != TEXT_ONLY &&
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
onOnboardingShown(SHOW_ONBOARDING_IMAGE);
showImageOnboarding.postEvent(true);
@@ -306,39 +325,82 @@ public class ConversationViewModel extends DbViewModel
}
@UiThread
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> headers, long timestamp,
boolean hasImageSupport) {
LiveData<SendState> sendMessage(@Nullable String text,
List<AttachmentHeader> headers, long expectedTimer) {
MutableLiveData<SendState> liveData = new MutableLiveData<>();
runOnDbThread(() -> {
try {
db.transaction(false, txn -> {
long start = now();
PrivateMessage m = createMessage(txn, text, headers,
expectedTimer);
messagingManager.addLocalMessage(txn, m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
m.hasText(), m.getAttachmentHeaders(),
m.getAutoDeleteTimer());
// TODO add text to cache when available here
MessageId id = message.getId();
txn.attach(() -> {
attachmentCreator.onAttachmentsSent(id);
liveData.setValue(SENT);
addedHeader.setEvent(h);
});
});
} catch (UnexpectedTimerException e) {
liveData.postValue(UNEXPECTED_TIMER);
} catch (DbException e) {
logException(LOG, WARNING, e);
liveData.postValue(ERROR);
}
});
return liveData;
}
private PrivateMessage createMessage(Transaction txn, @Nullable String text,
List<AttachmentHeader> headers, long expectedTimer)
throws DbException {
// Sending is only possible (setReady(true)) after loading all messages
// which happens after the contact has been loaded.
// privateMessageFormat is loaded together with contact
Contact contact = requireNonNull(contactItem.getValue()).getContact();
GroupId groupId = messagingManager.getContactGroup(contact).getId();
PrivateMessageFormat format =
requireNonNull(privateMessageFormat.getValue());
long timestamp = conversationManager
.getTimestampForOutgoingMessage(txn, requireNonNull(contactId));
try {
PrivateMessage pm;
if (hasImageSupport) {
pm = privateMessageFactory.createPrivateMessage(groupId,
if (format == TEXT_ONLY) {
return privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
} else if (format == TEXT_IMAGES) {
return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers);
} else {
pm = privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
long timer = autoDeleteManager
.getAutoDeleteTimer(txn, contactId, timestamp);
if (timer != expectedTimer)
throw new UnexpectedTimerException();
return privateMessageFactory.createPrivateMessage(groupId,
timestamp, text, headers, timer);
}
storeMessage(pm);
} catch (FormatException e) {
throw new AssertionError(e);
}
}
@UiThread
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
void setAutoDeleteTimerEnabled(boolean enabled) {
long timer = enabled ? DEFAULT_TIMER_DURATION : NO_AUTO_DELETE_TIMER;
// ContactId is set before menu gets inflated and UI interaction
final ContactId c = requireNonNull(contactId);
runOnDbThread(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here
addedHeader.postEvent(h);
db.transaction(false, txn ->
autoDeleteManager.setAutoDeleteTimer(txn, c, timer));
autoDeleteTimer.postValue(timer);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -357,8 +419,8 @@ public class ConversationViewModel extends DbViewModel
return contactName;
}
LiveData<Boolean> hasImageSupport() {
return imageSupport;
LiveData<PrivateMessageFormat> getPrivateMessageFormat() {
return privateMessageFormat;
}
LiveEvent<Boolean> showImageOnboarding() {
@@ -373,6 +435,10 @@ public class ConversationViewModel extends DbViewModel
return showIntroductionAction;
}
LiveData<Long> getAutoDeleteTimer() {
return autoDeleteTimer;
}
LiveData<Boolean> isContactDeleted() {
return contactDeleted;
}

View File

@@ -60,10 +60,12 @@ class ConversationVisitor implements
}
if (h.isLocal()) {
item = new ConversationMessageItem(
R.layout.list_item_conversation_msg_out, h, attachments);
R.layout.list_item_conversation_msg_out, h, contactName,
attachments);
} else {
item = new ConversationMessageItem(
R.layout.list_item_conversation_msg_in, h, attachments);
R.layout.list_item_conversation_msg_in, h, contactName,
attachments);
}
if (h.hasText()) {
String text = textCache.getText(h.getId());
@@ -79,13 +81,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.blogs_sharing_invitation_sent,
r.getName(), contactName.getValue());
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text = ctx.getString(
R.string.blogs_sharing_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, BLOG, r);
R.layout.list_item_conversation_request, text, contactName,
BLOG, r);
}
}
@@ -98,13 +102,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.blogs_sharing_response_accepted_sent,
contactName.getValue());
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.blogs_sharing_response_declined_auto,
contactName.getValue());
} else {
text = ctx.getString(
R.string.blogs_sharing_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -117,7 +126,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}
@@ -128,13 +138,15 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.forum_invitation_sent,
r.getName(), contactName.getValue());
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text = ctx.getString(
R.string.forum_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, FORUM, r);
R.layout.list_item_conversation_request, text, contactName,
FORUM, r);
}
}
@@ -147,13 +159,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.forum_invitation_response_accepted_sent,
contactName.getValue());
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.forum_invitation_response_declined_auto,
contactName.getValue());
} else {
text = ctx.getString(
R.string.forum_invitation_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -166,7 +183,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}
@@ -178,13 +196,15 @@ class ConversationVisitor implements
R.string.groups_invitations_invitation_sent,
contactName.getValue(), r.getName());
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text = ctx.getString(
R.string.groups_invitations_invitation_received,
contactName.getValue(), r.getName());
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text, GROUP, r);
R.layout.list_item_conversation_request, text, contactName,
GROUP, r);
}
}
@@ -197,13 +217,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.groups_invitations_response_accepted_sent,
contactName.getValue());
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.groups_invitations_response_declined_auto,
contactName.getValue());
} else {
text = ctx.getString(
R.string.groups_invitations_response_declined_sent,
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -216,7 +241,8 @@ class ConversationVisitor implements
contactName.getValue());
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}
@@ -227,7 +253,8 @@ class ConversationVisitor implements
String text = ctx.getString(R.string.introduction_request_sent,
contactName.getValue(), name);
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAnswered()) {
@@ -243,7 +270,7 @@ class ConversationVisitor implements
contactName.getValue(), name);
}
return new ConversationRequestItem(
R.layout.list_item_conversation_request, text,
R.layout.list_item_conversation_request, text, contactName,
INTRODUCTION, r);
}
}
@@ -262,13 +289,18 @@ class ConversationVisitor implements
text = ctx.getString(
R.string.introduction_response_accepted_sent,
introducedAuthor) + suffix;
} else if (r.isAutoDecline()) {
text = ctx.getString(
R.string.introduction_response_declined_auto,
introducedAuthor);
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,
introducedAuthor);
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_out, text, r);
R.layout.list_item_conversation_notice_out, text,
contactName, r);
} else {
String text;
if (r.wasAccepted()) {
@@ -288,7 +320,8 @@ class ConversationVisitor implements
introducedAuthor);
}
return new ConversationNoticeItem(
R.layout.list_item_conversation_notice_in, text, r);
R.layout.list_item_conversation_notice_in, text,
contactName, r);
}
}

View File

@@ -79,12 +79,6 @@ public class ForumActivity extends
}
}
@Override
public void onStart() {
super.onStart();
viewModel.clearForumPostNotification();
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
@@ -109,16 +103,16 @@ public class ForumActivity extends
// Handle presses on the action bar items
int itemId = item.getItemId();
if (itemId == R.id.action_forum_share) {
Intent i2 = new Intent(this, ShareForumActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_FORUM);
Intent i = new Intent(this, ShareForumActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_SHARE_FORUM);
return true;
} else if (itemId == R.id.action_forum_sharing_status) {
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
Intent i = new Intent(this, ForumSharingStatusActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_forum_delete) {
showUnsubscribeDialog();

View File

@@ -26,7 +26,6 @@ import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.forum.ForumManager;
@@ -94,8 +93,9 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
if (f.getGroupId().equals(groupId)) {
LOG.info("Forum post received, adding...");
ForumPostItem item = buildItem(f.getHeader(), f.getText());
addItem(item);
ForumPostItem item =
new ForumPostItem(f.getHeader(), f.getText());
addItem(item, false);
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
ForumInvitationResponseReceivedEvent f =
@@ -116,7 +116,7 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
}
}
void clearForumPostNotification() {
protected void clearNotifications() {
notificationManager.clearForumPostNotification(groupId);
}
@@ -140,10 +140,22 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
List<ForumPostHeader> headers =
forumManager.getPostHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
return createItems(txn, headers, this::buildItem);
start = now();
List<ForumPostItem> items = new ArrayList<>();
for (ForumPostHeader header : headers) {
items.add(loadItem(txn, header));
}
logDuration(LOG, "Loading bodies and creating items", start);
return items;
}, this::setItems);
}
private ForumPostItem loadItem(Transaction txn, ForumPostHeader header)
throws DbException {
String text = forumManager.getPostText(txn, header.getId());
return new ForumPostItem(header, text);
}
@Override
public void createAndStoreMessage(String text,
@Nullable MessageId parentId) {
@@ -171,26 +183,15 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
}
private void storePost(ForumPost msg, String text) {
runOnDbThread(() -> {
try {
long start = now();
ForumPostHeader header = forumManager.addLocalPost(msg);
addItemAsync(buildItem(header, text));
logDuration(LOG, "Storing forum post", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private ForumPostItem buildItem(ForumPostHeader header, String text) {
return new ForumPostItem(header, text);
}
@Override
protected String loadMessageText(Transaction txn, PostHeader header)
throws DbException {
return forumManager.getPostText(txn, header.getId());
runOnDbThread(false, txn -> {
long start = now();
ForumPostHeader header = forumManager.addLocalPost(txn, msg);
logDuration(LOG, "Storing forum post", start);
txn.attach(() -> {
ForumPostItem item = new ForumPostItem(header, text);
addItem(item, true);
});
}, e -> logException(LOG, WARNING, e));
}
@Override
@@ -205,18 +206,13 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
}
public void loadSharingContacts() {
runOnDbThread(() -> {
try {
Collection<Contact> contacts =
forumSharingManager.getSharedWith(groupId);
Collection<ContactId> contactIds =
new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
sharingController.addAll(contactIds);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
runOnDbThread(true, txn -> {
Collection<Contact> contacts =
forumSharingManager.getSharedWith(txn, groupId);
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
txn.attach(() -> sharingController.addAll(contactIds));
}, e -> logException(LOG, WARNING, e));
}
void deleteForum() {

View File

@@ -10,9 +10,8 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -47,13 +46,11 @@ public abstract class BaseFragment extends Fragment
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
listener.onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == android.R.id.home) {
requireActivity().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@UiThread
@@ -79,6 +76,7 @@ public abstract class BaseFragment extends Fragment
void handleException(Exception e);
}
@Deprecated
@CallSuper
@Override
public void runOnUiThreadUnlessDestroyed(Runnable r) {

View File

@@ -35,6 +35,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import de.hdodenhof.circleimageview.CircleImageView;
import static android.app.Activity.RESULT_OK;
@@ -46,6 +48,8 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
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.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -201,8 +205,8 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
public LiveData<SendState> onSendClick(@Nullable String text,
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
// disable button to prevent accidental double invitations
ui.message.setReady(false);
@@ -212,6 +216,7 @@ public class IntroductionMessageFragment extends BaseFragment
hideSoftKeyboard(ui.message);
introductionActivity.setResult(RESULT_OK);
introductionActivity.supportFinishAfterTransition();
return new MutableLiveData<>(SENT);
}
private void makeIntroduction(Contact c1, Contact c2,
@@ -219,8 +224,7 @@ public class IntroductionMessageFragment extends BaseFragment
introductionActivity.runOnDbThread(() -> {
// actually make the introduction
try {
long timestamp = System.currentTimeMillis();
introductionManager.makeIntroduction(c1, c2, text, timestamp);
introductionManager.makeIntroduction(c1, c2, text);
} catch (DbException e) {
logException(LOG, WARNING, e);
introductionError();

View File

@@ -411,6 +411,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
@UiThread
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults);
if (requestCode != REQUEST_PERMISSION_CAMERA_LOCATION)
throw new AssertionError();
if (gotPermission(CAMERA, permissions, grantResults)) {

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.TimeZone;
import java.util.logging.Formatter;
@@ -17,6 +18,16 @@ import static java.util.Locale.US;
@NotNullByDefault
public class BriefLogFormatter extends Formatter {
public static String formatLog(Formatter formatter,
Collection<LogRecord> logRecords) {
StringBuilder sb = new StringBuilder();
for (LogRecord record : logRecords) {
String formatted = formatter.format(record);
sb.append(formatted).append('\n');
}
return sb.toString();
}
private final Object lock = new Object();
private final DateFormat dateFormat; // Locking: lock
private final Date date; // Locking: lock

View File

@@ -21,6 +21,10 @@ public class CachingLogHandler extends Handler {
// Locking: lock
private final Queue<LogRecord> recent = new LinkedList<>();
// package-private constructor
CachingLogHandler() {
}
@Override
public void publish(LogRecord record) {
synchronized (lock) {

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.android.logging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.AndroidUtils;
import androidx.annotation.Nullable;
@NotNullByDefault
public interface LogDecrypter {
/**
* Returns decrypted log records from {@link AndroidUtils#getLogcatFile}
* or null if there was an error reading the logs.
*/
@Nullable
String decryptLogs(@Nullable byte[] logKey);
}

View File

@@ -0,0 +1,61 @@
package org.briarproject.briar.android.logging;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.reporting.DevConfig;
import org.briarproject.bramble.api.transport.StreamReaderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class LogDecrypterImpl implements LogDecrypter {
private static final Logger LOG =
getLogger(LogDecrypterImpl.class.getName());
private final DevConfig devConfig;
private final StreamReaderFactory streamReaderFactory;
@Inject
LogDecrypterImpl(DevConfig devConfig,
StreamReaderFactory streamReaderFactory) {
this.devConfig = devConfig;
this.streamReaderFactory = streamReaderFactory;
}
@Nullable
@Override
public String decryptLogs(@Nullable byte[] logKey) {
if (logKey == null) return null;
SecretKey key = new SecretKey(logKey);
File logFile = devConfig.getLogcatFile();
try (InputStream in = new FileInputStream(logFile)) {
InputStream reader =
streamReaderFactory.createLogStreamReader(in, key);
Scanner s = new Scanner(reader);
StringBuilder sb = new StringBuilder();
while (s.hasNextLine()) sb.append(s.nextLine()).append("\n");
s.close();
return sb.toString();
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
} finally {
//noinspection ResultOfMethodCallIgnored
logFile.delete();
}
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.android.logging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.AndroidUtils;
import androidx.annotation.Nullable;
@NotNullByDefault
public interface LogEncrypter {
/**
* Writes encrypted log records to {@link AndroidUtils#getLogcatFile}
* and returns the encryption key if everything went fine.
*/
@Nullable
byte[] encryptLogs();
}

View File

@@ -0,0 +1,77 @@
package org.briarproject.briar.android.logging;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.reporting.DevConfig;
import org.briarproject.bramble.api.transport.StreamWriter;
import org.briarproject.bramble.api.transport.StreamWriterFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class LogEncrypterImpl implements LogEncrypter {
private static final Logger LOG =
getLogger(LogEncrypterImpl.class.getName());
private final DevConfig devConfig;
private final CachingLogHandler logHandler;
private final CryptoComponent crypto;
private final StreamWriterFactory streamWriterFactory;
@Inject
LogEncrypterImpl(DevConfig devConfig,
CachingLogHandler logHandler,
CryptoComponent crypto,
StreamWriterFactory streamWriterFactory) {
this.devConfig = devConfig;
this.logHandler = logHandler;
this.crypto = crypto;
this.streamWriterFactory = streamWriterFactory;
}
@Nullable
@Override
public byte[] encryptLogs() {
SecretKey logKey = crypto.generateSecretKey();
File logFile = devConfig.getLogcatFile();
try (OutputStream out = new FileOutputStream(logFile)) {
StreamWriter streamWriter =
streamWriterFactory.createLogStreamWriter(out, logKey);
Writer writer =
new OutputStreamWriter(streamWriter.getOutputStream());
writeLogString(writer);
writer.close();
return logKey.getBytes();
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
}
private void writeLogString(Writer writer) throws IOException {
Formatter formatter = new BriefLogFormatter();
for (LogRecord record : logHandler.getRecentLogRecords()) {
String formatted = formatter.format(record);
writer.append(formatted).append('\n');
}
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.briar.android.logging;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class LoggingModule {
@Provides
@Singleton
CachingLogHandler provideCachingLogHandler() {
return new CachingLogHandler();
}
@Provides
@Singleton
LogEncrypter provideLogEncrypter(LogEncrypterImpl logEncrypter) {
return logEncrypter;
}
@Provides
@Singleton
LogDecrypter provideLogDecrypter(LogDecrypterImpl logDecrypter) {
return logDecrypter;
}
}

View File

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

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.navdrawer;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -48,25 +49,23 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.core.view.GravityCompat.START;
import static androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE;
import static java.util.Objects.requireNonNull;
import static androidx.lifecycle.Lifecycle.State.STARTED;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
@@ -143,7 +142,7 @@ public class NavDrawerActivity extends BriarActivity implements
if (ask) showDozeDialog(getString(R.string.setup_doze_intro));
});
Toolbar toolbar = findViewById(R.id.toolbar);
Toolbar toolbar = setUpCustomToolbar(false);
drawerLayout = findViewById(R.id.drawer_layout);
navigation = findViewById(R.id.navigation);
GridView transportsView = findViewById(R.id.transportsView);
@@ -153,11 +152,6 @@ public class NavDrawerActivity extends BriarActivity implements
startActivity(new Intent(this, TransportsActivity.class));
});
setSupportActionBar(toolbar);
ActionBar actionBar = requireNonNull(getSupportActionBar());
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar,
R.string.nav_drawer_open_description,
R.string.nav_drawer_close_description) {
@@ -181,9 +175,6 @@ public class NavDrawerActivity extends BriarActivity implements
if (lifecycleManager.getLifecycleState().isAfter(RUNNING)) {
showSignOutFragment();
} else if (state == null) {
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
}
if (state == null) {
// do not call this again when there's existing state
@@ -273,7 +264,6 @@ public class NavDrawerActivity extends BriarActivity implements
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
drawerLayout.closeDrawer(START);
clearBackStack();
if (item.getItemId() == R.id.nav_btn_lock) {
lockManager.setLocked(true);
ActivityCompat.finishAfterTransition(this);
@@ -293,8 +283,14 @@ public class NavDrawerActivity extends BriarActivity implements
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(SignOutFragment.TAG) != null) {
finish();
} else if (fm.getBackStackEntryCount() == 0
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) {
} else if (fm.getBackStackEntryCount() == 0 &&
fm.findFragmentByTag(ContactListFragment.TAG) == null) {
// don't start fragments in the wrong part of lifecycle (#1904)
if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) {
LOG.warning("Tried to start contacts fragment in state " +
getLifecycle().getCurrentState().name());
return;
}
/*
* This makes sure that the first fragment (ContactListFragment) the
* user sees is the same as the last fragment the user sees before
@@ -337,30 +333,12 @@ public class NavDrawerActivity extends BriarActivity implements
startFragment(fragment);
}
private void startFragment(BaseFragment fragment) {
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
startFragment(fragment, false);
else startFragment(fragment, true);
}
private void startFragment(BaseFragment fragment,
boolean isAddedToBackStack) {
FragmentTransaction trans =
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in,
R.anim.fade_out, R.anim.fade_in,
R.anim.fade_out)
.replace(R.id.fragmentContainer, fragment,
fragment.getUniqueTag());
if (isAddedToBackStack) {
trans.addToBackStack(fragment.getUniqueTag());
}
trans.commit();
}
private void clearBackStack() {
getSupportFragmentManager().popBackStackImmediate(null,
POP_BACK_STACK_INCLUSIVE);
private void startFragment(BaseFragment f) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out,
R.anim.fade_in, R.anim.fade_out)
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.commit();
}
@Override
@@ -433,8 +411,7 @@ public class NavDrawerActivity extends BriarActivity implements
Transport t = getItem(position);
ImageView icon = view.findViewById(R.id.imageView);
icon.setImageDrawable(ContextCompat.getDrawable(
NavDrawerActivity.this, t.iconDrawable));
icon.setImageResource(t.iconDrawable);
icon.setColorFilter(ContextCompat.getColor(
NavDrawerActivity.this, t.iconColor));
@@ -476,11 +453,13 @@ public class NavDrawerActivity extends BriarActivity implements
private void showTransportsOnboarding(boolean show, ImageView imageView) {
if (show) {
int color = resolveColorAttribute(this, R.attr.colorControlNormal);
Drawable drawable = VectorDrawableCompat
.create(getResources(), R.drawable.transport_tor, null);
new MaterialTapTargetPrompt.Builder(NavDrawerActivity.this,
R.style.OnboardingDialogTheme).setTarget(imageView)
.setPrimaryText(R.string.network_settings_title)
.setSecondaryText(R.string.transports_onboarding_text)
.setIcon(R.drawable.transport_tor)
.setIconDrawable(drawable)
.setIconDrawableColourFilter(color)
.setBackgroundColour(
ContextCompat.getColor(this, R.color.briar_primary))

View File

@@ -141,8 +141,7 @@ public class TransportsActivity extends BriarActivity {
Transport t = getItem(position);
ImageView icon = view.findViewById(R.id.icon);
icon.setImageDrawable(ContextCompat.getDrawable(
TransportsActivity.this, t.iconDrawable));
icon.setImageResource(t.iconDrawable);
icon.setColorFilter(ContextCompat.getColor(
TransportsActivity.this, t.iconColor));

View File

@@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_GROUP_INVITE;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_TEXT_LENGTH;
@@ -80,16 +81,11 @@ public class GroupActivity extends
setGroupEnabled(false);
viewModel.isDissolved().observe(this, dissolved -> {
setGroupEnabled(!dissolved);
if (dissolved) onGroupDissolved();
// only show dialog when no prior state
if (dissolved && state == null) onGroupDissolved();
});
}
@Override
public void onStart() {
super.onStart();
viewModel.clearGroupMessageNotifications();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
@@ -116,26 +112,26 @@ public class GroupActivity extends
startActivity(i);
return true;
} else if (itemId == R.id.action_group_reveal) {
if (viewModel.isCreator().getValue())
if (requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException();
Intent i = new Intent(this, RevealContactsActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_group_invite) {
if (!viewModel.isCreator().getValue())
if (!requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException();
Intent i = new Intent(this, GroupInviteActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_GROUP_INVITE);
return true;
} else if (itemId == R.id.action_group_leave) {
if (viewModel.isCreator().getValue())
if (requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException();
showLeaveGroupDialog();
return true;
} else if (itemId == R.id.action_group_dissolve) {
if (!viewModel.isCreator().getValue())
if (!requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException();
showDissolveGroupDialog();
return true;
@@ -158,7 +154,8 @@ public class GroupActivity extends
@Override
public void onReplyClick(GroupMessageItem item) {
if (!viewModel.isDissolved().getValue()) super.onReplyClick(item);
Boolean isDissolved = viewModel.isDissolved().getValue();
if (isDissolved != null && !isDissolved) super.onReplyClick(item);
}
private void setGroupEnabled(boolean enabled) {
@@ -201,7 +198,7 @@ public class GroupActivity extends
viewModel.deletePrivateGroup();
}
public void onGroupDissolved() {
private void onGroupDissolved() {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_dissolved_dialog_title));

View File

@@ -25,7 +25,6 @@ import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.api.privategroup.GroupMember;
import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -103,7 +102,10 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
if (!g.isLocal() && g.getGroupId().equals(groupId)) {
LOG.info("Group message received, adding...");
GroupMessageItem item = buildItem(g.getHeader(), g.getText());
addItem(item);
addItem(item, false);
// In case the join message comes from the creator,
// we need to reload the sharing contacts
// in case it was delayed and the sharing count is wrong (#850).
if (item instanceof JoinMessageItem &&
(((JoinMessageItem) item).isInitial())) {
loadSharingContacts();
@@ -133,12 +135,12 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
}
@Override
public void setGroupId(GroupId groupId) {
super.setGroupId(groupId);
protected void performInitialLoad() {
super.performInitialLoad();
loadPrivateGroup(groupId);
}
public void clearGroupMessageNotifications() {
protected void clearNotifications() {
notificationManager.clearGroupMessageNotification(groupId);
}
@@ -166,10 +168,28 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
List<GroupMessageHeader> headers =
privateGroupManager.getHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
return createItems(txn, headers, this::buildItem);
start = now();
List<GroupMessageItem> items = new ArrayList<>();
for (GroupMessageHeader header : headers) {
items.add(loadItem(txn, header));
}
logDuration(LOG, "Loading bodies and creating items", start);
return items;
}, this::setItems);
}
private GroupMessageItem loadItem(Transaction txn,
GroupMessageHeader header) throws DbException {
String text;
if (header instanceof JoinMessageHeader) {
// will be looked up later
text = "";
} else {
text = privateGroupManager.getMessageText(txn, header.getId());
}
return buildItem(header, text);
}
private GroupMessageItem buildItem(GroupMessageHeader header, String text) {
if (header instanceof JoinMessageHeader) {
return new JoinMessageItem((JoinMessageHeader) header, text);
@@ -177,16 +197,6 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
return new GroupMessageItem(header, text);
}
@Override
protected String loadMessageText(
Transaction txn, PostHeader header) throws DbException {
if (header instanceof JoinMessageHeader) {
// will be looked up later
return "";
}
return privateGroupManager.getMessageText(txn, header.getId());
}
@Override
public void createAndStoreMessage(String text,
@Nullable MessageId parentId) {
@@ -217,17 +227,15 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
}
private void storePost(GroupMessage msg, String text) {
runOnDbThread(() -> {
try {
long start = now();
GroupMessageHeader header =
privateGroupManager.addLocalMessage(msg);
addItemAsync(buildItem(header, text));
logDuration(LOG, "Storing group message", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
runOnDbThread(false, txn -> {
long start = now();
GroupMessageHeader header =
privateGroupManager.addLocalMessage(txn, msg);
logDuration(LOG, "Storing group message", start);
txn.attach(() ->
addItem(buildItem(header, text), true)
);
}, e -> logException(LOG, WARNING, e));
}
@Override
@@ -242,20 +250,16 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
}
public void loadSharingContacts() {
runOnDbThread(() -> {
try {
Collection<GroupMember> members =
privateGroupManager.getMembers(groupId);
Collection<ContactId> contactIds = new ArrayList<>();
for (GroupMember m : members) {
if (m.getContactId() != null)
contactIds.add(m.getContactId());
}
sharingController.addAll(contactIds);
} catch (DbException e) {
logException(LOG, WARNING, e);
runOnDbThread(true, txn -> {
Collection<GroupMember> members =
privateGroupManager.getMembers(txn, groupId);
Collection<ContactId> contactIds = new ArrayList<>();
for (GroupMember m : members) {
if (m.getContactId() != null)
contactIds.add(m.getContactId());
}
});
txn.attach(() -> sharingController.addAll(contactIds));
}, e -> logException(LOG, WARNING, e));
}
void deletePrivateGroup() {

View File

@@ -16,7 +16,7 @@ class JoinMessageItem extends GroupMessageItem {
JoinMessageItem(JoinMessageHeader h, String text) {
super(h, text);
this.isInitial = h.isInitial();
isInitial = h.isInitial();
}
@Override

View File

@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -15,6 +17,8 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.identity.AuthorManager;
import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
@@ -36,6 +40,8 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@@ -44,9 +50,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
implements CreateGroupController {
private static final Logger LOG =
Logger.getLogger(CreateGroupControllerImpl.class.getName());
getLogger(CreateGroupControllerImpl.class.getName());
private final Executor cryptoExecutor;
private final TransactionManager db;
private final AutoDeleteManager autoDeleteManager;
private final ConversationManager conversationManager;
private final ContactManager contactManager;
private final IdentityManager identityManager;
private final PrivateGroupFactory groupFactory;
@@ -57,17 +66,27 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private final Clock clock;
@Inject
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
CreateGroupControllerImpl(
@DatabaseExecutor Executor dbExecutor,
@CryptoExecutor Executor cryptoExecutor,
LifecycleManager lifecycleManager, ContactManager contactManager,
AuthorManager authorManager, IdentityManager identityManager,
TransactionManager db,
AutoDeleteManager autoDeleteManager,
ConversationManager conversationManager,
LifecycleManager lifecycleManager,
ContactManager contactManager,
AuthorManager authorManager,
IdentityManager identityManager,
PrivateGroupFactory groupFactory,
GroupMessageFactory groupMessageFactory,
PrivateGroupManager groupManager,
GroupInvitationFactory groupInvitationFactory,
GroupInvitationManager groupInvitationManager, Clock clock) {
GroupInvitationManager groupInvitationManager,
Clock clock) {
super(dbExecutor, lifecycleManager, contactManager, authorManager);
this.cryptoExecutor = cryptoExecutor;
this.db = db;
this.autoDeleteManager = autoDeleteManager;
this.conversationManager = conversationManager;
this.contactManager = contactManager;
this.identityManager = identityManager;
this.groupFactory = groupFactory;
@@ -131,16 +150,14 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
List<Contact> contacts = new ArrayList<>();
for (ContactId c : contactIds) {
try {
contacts.add(contactManager.getContact(c));
} catch (NoSuchContactException e) {
// Continue
}
}
signInvitations(g, localAuthor, contacts, text, handler);
db.transaction(false, txn -> {
LocalAuthor localAuthor =
identityManager.getLocalAuthor(txn);
List<InvitationContext> contexts =
createInvitationContexts(txn, contactIds);
txn.attach(() -> signInvitations(g, localAuthor, contexts,
text, handler));
});
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
@@ -148,17 +165,32 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
});
}
private List<InvitationContext> createInvitationContexts(Transaction txn,
Collection<ContactId> contactIds) throws DbException {
List<InvitationContext> contexts = new ArrayList<>();
for (ContactId c : contactIds) {
try {
Contact contact = contactManager.getContact(txn, c);
long timestamp = conversationManager
.getTimestampForOutgoingMessage(txn, c);
long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
timestamp);
contexts.add(new InvitationContext(contact, timestamp, timer));
} catch (NoSuchContactException e) {
// Continue
}
}
return contexts;
}
private void signInvitations(GroupId g, LocalAuthor localAuthor,
Collection<Contact> contacts, @Nullable String text,
List<InvitationContext> contexts, @Nullable String text,
ResultExceptionHandler<Void, DbException> handler) {
cryptoExecutor.execute(() -> {
long timestamp = clock.currentTimeMillis();
List<InvitationContext> contexts = new ArrayList<>();
for (Contact c : contacts) {
byte[] signature = groupInvitationFactory.signInvitation(c, g,
timestamp, localAuthor.getPrivateKey());
contexts.add(new InvitationContext(c.getId(), timestamp,
signature));
for (InvitationContext ctx : contexts) {
ctx.signature = groupInvitationFactory.signInvitation(
ctx.contact, g, ctx.timestamp,
localAuthor.getPrivateKey());
}
sendInvitations(g, contexts, text, handler);
});
@@ -169,11 +201,12 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(() -> {
try {
for (InvitationContext context : contexts) {
for (InvitationContext ctx : contexts) {
try {
groupInvitationManager.sendInvitation(g,
context.contactId, text, context.timestamp,
context.signature);
ctx.contact.getId(), text, ctx.timestamp,
requireNonNull(ctx.signature),
ctx.autoDeleteTimer);
} catch (NoSuchContactException e) {
// Continue
}
@@ -188,15 +221,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
private static class InvitationContext {
private final ContactId contactId;
private final long timestamp;
private final byte[] signature;
private final Contact contact;
private final long timestamp, autoDeleteTimer;
@Nullable
private byte[] signature = null;
private InvitationContext(ContactId contactId, long timestamp,
byte[] signature) {
this.contactId = contactId;
private InvitationContext(Contact contact, long timestamp,
long autoDeleteTimer) {
this.contact = contact;
this.timestamp = timestamp;
this.signature = signature;
this.autoDeleteTimer = autoDeleteTimer;
}
}
}

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