Compare commits

..

298 Commits

Author SHA1 Message Date
akwizgran
2f719d7f2c Bump version numbers for 1.3.2 release. 2021-04-26 13:50:35 +01:00
akwizgran
eb2e8d75f4 Enable features for 1.3 release. 2021-04-26 13:45:53 +01:00
akwizgran
338d288290 Update translations. 2021-04-26 13:43:33 +01:00
Torsten Grote
03fe1a2d2c Merge branch '1743-huawei-app-launch' into 'master'
Add button for opening Huawei battery settings during setup

See merge request briar/briar!1441
2021-04-23 13:54:55 +00:00
akwizgran
ade48c7bea Merge branch 'fastlane-metadata-upload' into 'master'
Update Google play listing and language mappings

See merge request briar/briar!1440
2021-04-23 13:43:10 +00:00
akwizgran
4a8d89e2bb Add button for opening Huawei battery settings during setup. 2021-04-23 14:25:24 +01:00
Torsten Grote
0fed1681ed Google play listing now available in:
* Persian
* Galician
* Lithuanian
* Turkish
* Chinese (Simplified)
2021-04-23 10:22:27 -03:00
akwizgran
93ad483066 Merge branch 'fastlane-metadata-upload' into 'master'
Update Play Store listing and add Catalan translation

See merge request briar/briar!1439
2021-04-23 10:10:08 +00:00
Torsten Grote
d995e73996 Update Play Store listing and add Catalan translation 2021-04-22 14:06:37 -03:00
Torsten Grote
1d271bab18 Merge branch '1914-create-viewmodels-directly-after-injection' into 'master'
Create ViewModels directly after injection

Closes #1914

See merge request briar/briar!1432
2021-04-22 14:59:05 +00:00
Torsten Grote
a9d2aa9366 Merge branch '1883-resource-ids-will-be-non-final' into 'master'
Replace switch statements with resource ids with if-then-else

Closes #1883

See merge request briar/briar!1438
2021-04-22 13:34:35 +00:00
Sebastian Kürten
2a48d43e5b Create ViewModels directly after injection 2021-04-22 15:31:55 +02:00
Sebastian Kürten
0a4e23118a Replace switch statements with resource ids with if-then-else 2021-04-22 15:19:11 +02:00
akwizgran
528a15962f Merge branch 'fastlane-metadata' into 'master'
Make Google Play descriptions translatable

See merge request briar/briar!1437
2021-04-22 13:00:13 +00:00
Torsten Grote
0a0cc4e79c Extend full description 2021-04-22 09:50:16 -03:00
Torsten Grote
2b3ba42d70 Make Google Play descriptions translable 2021-04-21 13:35:10 -03:00
akwizgran
9465331ece Merge branch '1999-bluetooth-killed-activity-fix' into 'master'
Reset plugin assignments when starting AddNearbyContactIntroFragment

Closes #1999

See merge request briar/briar!1434
2021-04-21 15:57:17 +00:00
akwizgran
b0d86bef3e Merge branch 'scheduled-tests' into 'master'
Run bridge tests in scheduled nightly tests (and fix merge pipelines)

See merge request briar/briar!1431
2021-04-21 13:55:48 +00:00
Torsten Grote
a28a2360f1 Merge branch 'remove-redundant-casts' into 'master'
Remove a few redundant casts

See merge request briar/briar!1436
2021-04-21 12:52:04 +00:00
Torsten Grote
f7a957150e Tweak CI rules for optional tests
so they are not needed for merge requests
2021-04-21 09:49:11 -03:00
Torsten Grote
67bd065bc3 Allow manual emulator tests in all situations 2021-04-21 09:48:59 -03:00
Torsten Grote
5a3a12767c Increase SetupActivity timeout in PromoVideoTest as the current one doesn't seem to be sufficient 2021-04-21 09:48:58 -03:00
Torsten Grote
2f86bc312c Catch UnableToTakeScreenshotException to make CI more stable 2021-04-21 09:48:58 -03:00
Torsten Grote
f0fcadfaf4 Also run optional bridge test with schedules 2021-04-21 09:48:57 -03:00
akwizgran
a9d88c849a Merge branch '1961-connect-via-bt-ui' into 'master'
Simple version of Connect via Bluetooth UI

See merge request briar/briar!1398
2021-04-21 11:57:53 +00:00
Sebastian Kürten
7397efca80 Remove a few redundant casts 2021-04-21 13:53:53 +02:00
Torsten Grote
17bd6f9a33 Merge branch '1778-onboarding-tap-target' into 'master'
Wait for layout before showing onboarding tap target

Closes #1778

See merge request briar/briar!1435
2021-04-21 11:45:19 +00:00
akwizgran
2d4e7a9fb0 Wait for layout before showing onboarding tap target. 2021-04-21 12:20:21 +01:00
Torsten Grote
0266da993d Reset plugin assignments when starting AddNearbyContactIntroFragment 2021-04-20 14:38:58 -03:00
Torsten Grote
ea5280713f Address review feedback for Connect via Bluetooth UI 2021-04-20 14:24:05 -03:00
Torsten Grote
0b89d29c7d Reset BluetoothPlugin reference
When it is assigned before we are signed-in in (like when returning to a killed activity), it would be null and without a reset, we would consider Bluetooth to not be supported.
2021-04-20 14:23:07 -03:00
Torsten Grote
688bac77a8 Use a feature flag to hide connect via Bluetooth option 2021-04-19 11:36:35 -03:00
Torsten Grote
c736bf7c06 Move Connect via Bluetooth UI into DialogFragment
so it can stay active when leaving the context to enable location or permissions
2021-04-19 11:08:02 -03:00
Torsten Grote
539730f8ec Show dialog when permission was denied for good 2021-04-19 11:07:21 -03:00
Torsten Grote
a9c4669c75 Simple version of Connect via Bluetooth UI 2021-04-19 11:07:20 -03:00
Torsten Grote
4c11f93ee2 Merge branch '1973-make-use-of-handle-exception' into 'master'
Make more use of DbViewModel#handleException()

Closes #1973

See merge request briar/briar!1427
2021-04-16 17:29:20 +00:00
Sebastian Kürten
f8dba6fd7f Add static version of DbViewModel#handleException() in UiUtils 2021-04-15 19:34:14 +02:00
Sebastian Kürten
419247074f Make more use of DbViewModel#handleException() 2021-04-15 17:53:53 +02:00
akwizgran
2ddb7b5b64 Merge branch '1944-update-contact-list-when-changing-aliases' into 'master'
Broadcast ContactAliasChangedEvent to update contact list

Closes #1944

See merge request briar/briar!1425
2021-04-15 15:35:46 +00:00
akwizgran
c14b59661e Merge branch 'fix-settings-test' into 'master'
Fix SettingsActivityScreenshotTest and enable scheduled emulator tests

See merge request briar/briar!1429
2021-04-15 15:30:31 +00:00
akwizgran
d70d27e665 Merge branch '804-self-destructing-messages' into 'master'
Merge 'Self-destruct timer for messages' to master

Closes #1863

See merge request briar/briar!1396
2021-04-15 15:24:19 +00:00
akwizgran
8991762b0c Final code review nitpicks. 2021-04-15 16:12:38 +01:00
akwizgran
2fc6741c99 Remove redundant method. 2021-04-15 16:03:01 +01:00
akwizgran
1075af73f2 Change default timer duration to 7 days. 2021-04-15 11:54:48 +01:00
Torsten Grote
8671229f76 Merge branch '1672-speed-dial-crash' into 'master'
Close the speed dial to prevent a crash

Closes #1672

See merge request briar/briar!1430
2021-04-14 17:22:06 +00:00
akwizgran
0fb67583ff Close the speed dial to prevent a crash. 2021-04-14 18:10:39 +01:00
Torsten Grote
2fc0fd17a2 Fix SettingsActivityScreenshotTest 2021-04-14 11:43:31 -03:00
Torsten Grote
f185860213 Allow emulator tests to run also on a schedule
Also do 1 retry in case of failure
2021-04-14 10:57:49 -03:00
akwizgran
8ffcdbfc21 Merge branch 'sort-blogposts' into 'master'
Sort the loaded posts, like FeedViewModel already does

See merge request briar/briar!1428
2021-04-14 10:14:34 +00:00
Daniel Lublin
6a38e2cca8 Sort the loaded posts, like FeedViewModel already does 2021-04-14 12:02:02 +02:00
Sebastian Kürten
10d9d78ca8 Broadcast ContactAliasChangedEvent to update contact list 2021-04-14 07:43:50 +02:00
Torsten Grote
03a68038e7 Merge branch 'v3-address-for-feedback-and-crash-reports' into 'master'
Use a v3 hidden service for receiving feedback and crash reports

See merge request briar/briar!1426
2021-04-13 17:01:58 +00:00
akwizgran
fabefcdf4b Use a v3 hidden service for receiving feedback and crash reports. 2021-04-13 17:50:36 +01:00
Sebastian Kürten
9e4d8ecddf Make introducees send auto-declines even if reponse already received 2021-04-13 08:49:04 -03:00
Sebastian Kürten
1dffbfd8dc Add two tests for auto-declines happening after response already received 2021-04-13 08:49:04 -03:00
Sebastian Kürten
950db5a87a Assert that IntroductionManager return true for canIntroduce() 2021-04-13 08:49:04 -03:00
Sebastian Kürten
21348d5557 IntroductionManager: don't throw IllegalArgumentException when deleting invisible msgs 2021-04-13 08:49:03 -03:00
Sebastian Kürten
eb1a089437 Revert changes to AbstractProtocolEngine
This reverts parts of commit 5d143f8b33adc07d990627036b806a2becea7074.
2021-04-13 08:49:03 -03:00
Sebastian Kürten
17fc81ab7a Re-add integration test that goes through two full introduction cycles 2021-04-13 08:49:03 -03:00
Torsten Grote
1c54fd1101 Don't auto-delete invisible forwarded responses on sender side
They will be visible for the recipient, so send with timer
2021-04-13 08:49:02 -03:00
Sebastian Kürten
263bce38cd Create new test that reproduces #1975 2021-04-13 08:49:02 -03:00
Sebastian Kürten
a923c1151c Move setReadFlag() from ConversationClient to ConversationManager 2021-04-13 08:49:02 -03:00
Sebastian Kürten
0bf10a827f Implement auto-declining for self-destructed introductions 2021-04-13 08:49:01 -03:00
Daniel Lublin
49850e4198 Make sure invitation accept msg is linking to shareable 2021-04-13 08:49:01 -03:00
Daniel Lublin
95b437b311 Add test for invitee responding after sharer deleted invitation 2021-04-13 08:49:00 -03:00
Daniel Lublin
7006f765a6 Keep members in subclasses and use getters 2021-04-13 08:49:00 -03:00
Daniel Lublin
ec3360400c Assert that expected event is broadcasted 2021-04-13 08:49:00 -03:00
Daniel Lublin
5c41d09c52 Remember when invitation was auto-declined due to deletion
And render differently
2021-04-13 08:48:59 -03:00
Daniel Lublin
a7590956fd Auto-decline/auto-delete Forum & Blog sharing invitations/responses 2021-04-13 08:48:59 -03:00
Torsten Grote
a581960121 Don't show notification for own auto-decline responses 2021-04-13 08:48:58 -03:00
Torsten Grote
dc57a0b925 Use AtomicReference in TestEventListener to store event 2021-04-13 08:48:58 -03:00
Torsten Grote
d6082162ab Add a way to check for expected events
and use it for private group auto-declines
2021-04-13 08:48:58 -03:00
Torsten Grote
1801afdbb7 Update support for disappearing messages in real time 2021-04-13 08:48:57 -03:00
akwizgran
458add0c9c Don't advertise support for messaging features that are disabled by flags. 2021-04-13 08:48:57 -03:00
akwizgran
f2374eb141 Count sent messages in integration tests. 2021-04-13 08:48:56 -03:00
Torsten Grote
5db5897793 Use stored session metadata instead of fetching it again 2021-04-13 08:48:56 -03:00
Torsten Grote
f3d628afa7 Render automatic declines differently in the UI
and show them as they happen via an Event
2021-04-13 08:48:55 -03:00
Torsten Grote
4d3482e40e Remember when declines were automatic due to deletion
so they can be shown differently for sender
2021-04-13 08:48:55 -03:00
Torsten Grote
a8cff454ec Add integration tests for auto-deletion of private group invitations and responses 2021-04-13 08:48:55 -03:00
Torsten Grote
f66cae4749 Factor out auto-delete integration test code
so we can re-use it in other tests
2021-04-13 08:48:54 -03:00
Torsten Grote
aded1daf92 Auto-delete PrivateGroup invitations and responses as well 2021-04-13 08:48:54 -03:00
Torsten Grote
aa1ba0d950 Turn 'Learn more' link into a button to have a larger tap area
and a selectable background
2021-04-13 08:48:53 -03:00
Torsten Grote
071010e438 Turn ConversationSettingsLearnMoreDialog into a generic Onboarding fragment 2021-04-13 08:48:53 -03:00
Torsten Grote
f6d8e364d6 Update auto-delete onboarding text 2021-04-13 08:48:53 -03:00
akwizgran
f1453ed4c4 Show disabled menu item if we support feature but contact doesn't. 2021-04-13 08:48:52 -03:00
akwizgran
3a2146cb03 Don't advertise support for disappearing messages unless flag is enabled. 2021-04-13 08:48:52 -03:00
akwizgran
24eb76de20 Hide disappearing messages menu item unless feature flag is enabled. 2021-04-13 08:48:51 -03:00
akwizgran
693478e0a5 Add feature flag for self-destructing messages. 2021-04-13 08:48:51 -03:00
Torsten Grote
bf6be5c5a7 Replace all messages when re-loading
to ensure that messages deleted in the meantime get removed
2021-04-13 08:48:51 -03:00
Torsten Grote
a12a639cd3 Remove auto-deleted messages immediately from conversation 2021-04-13 08:48:50 -03:00
Torsten Grote
ef37428714 Replace MessagesCleanedUpEvent with ConversationMessagesDeletedEvent 2021-04-13 08:48:50 -03:00
akwizgran
644afe8995 Wait for events to be delivered before continuing with test. 2021-04-13 08:48:50 -03:00
akwizgran
c66c428124 Log how long it takes to deliver private messages and attachments. 2021-04-13 08:48:49 -03:00
akwizgran
db5b2ea9b6 Remove redundant call to getAutoDeleteTimer(). 2021-04-13 08:48:49 -03:00
akwizgran
d84603bce2 Check group counts in AutoDeleteIntegrationTest. 2021-04-13 08:48:48 -03:00
akwizgran
b128370299 Include legacy messages when recalculating group count. 2021-04-13 08:48:48 -03:00
akwizgran
240e619248 Delete private messages when their timers expire (needs UI support). 2021-04-13 08:48:48 -03:00
akwizgran
c89bde08db Move ConversationManagerImpl to conversation package. 2021-04-13 08:48:47 -03:00
akwizgran
3ecd1c62b8 Set default timer duration to 1 minute for testing. 2021-04-13 08:48:47 -03:00
akwizgran
e3c5497283 Update javadoc to explain that a new timer can be set. 2021-04-13 08:48:46 -03:00
akwizgran
4bd8ee8ccf Pass message IDs to cleanup hooks in batches. 2021-04-13 08:48:46 -03:00
akwizgran
43b437af92 Group messages by group ID when fetching them from database. 2021-04-13 08:48:46 -03:00
akwizgran
56e0d62597 Throw an exception if no cleanup hook was registered. 2021-04-13 08:48:45 -03:00
akwizgran
d10e5f025d Remove copypasta. 2021-04-13 08:48:45 -03:00
akwizgran
b1a80691db Add comment to explain that starting timer may be a no-op. 2021-04-13 08:48:44 -03:00
akwizgran
049aa61e85 Stop the timer if no hook has been registered. 2021-04-13 08:48:44 -03:00
akwizgran
7026361234 Add javadocs for CleanupManager and CleanupHook. 2021-04-13 08:48:44 -03:00
akwizgran
5e30dc5bf4 Simplify deadline comparison logic. 2021-04-13 08:48:43 -03:00
akwizgran
024bfc8ec8 Stop the cleanup timer if the hook returns false. 2021-04-13 08:48:42 -03:00
akwizgran
04e5e8e4d0 Add cleanup manager. 2021-04-13 08:48:38 -03:00
akwizgran
7c5d47733f Query message IDs rather than metadata when only IDs are needed. 2021-04-13 08:46:11 -03:00
Torsten Grote
b24f2a1818 Add support for showing auto-delete timers in minutes 2021-04-13 08:46:11 -03:00
Torsten Grote
ee6664ce9d Show actual auto-delete timer duration in UI
(only days and hours for now)
2021-04-13 08:46:11 -03:00
Torsten Grote
ab434946b5 Show outgoing message status icon in same color as time 2021-04-13 08:46:10 -03:00
Torsten Grote
35e431eb99 Fix bomb icon color
in incoming image messages without text (on old phones)
2021-04-13 08:46:10 -03:00
Torsten Grote
aa8cddf509 Get rid of SENDING state and publish new live data in order on UiThread 2021-04-13 08:46:10 -03:00
Torsten Grote
c9ede0bfc1 Return LiveData when sending message 2021-04-13 08:46:09 -03:00
Torsten Grote
6ec9a0f2b2 Show warning dialog when auto-delete timer has changed since starting to compose message 2021-04-13 08:46:09 -03:00
Torsten Grote
2f86112801 Add "Tap to learn more" to message bubbles for timer changes 2021-04-13 08:46:09 -03:00
akwizgran
c032befe6f Provide clock for UI tests. 2021-04-13 08:46:08 -03:00
akwizgran
55eccde031 Add some comments. 2021-04-13 08:46:08 -03:00
akwizgran
5716820439 Sync acks for initial messages when setting up integration tests. 2021-04-13 08:46:08 -03:00
akwizgran
17d433dd9b Allow time travel in integration tests. 2021-04-13 08:46:07 -03:00
akwizgran
000812bf6d Inject DefaultTaskSchedulerModule.EagerSingletons at startup in headless app. 2021-04-13 08:46:07 -03:00
akwizgran
5e2187a877 Refactor integration tests to allow clock to be replaced. 2021-04-13 08:46:06 -03:00
Sebastian Kürten
e10b6334f5 Introduce conversation settings screen 2021-04-13 08:46:06 -03:00
Torsten Grote
baa0341727 Create group invitation with read-write transaction
because the AutoDeleteManager needs to change the DB
and otherwise crashes.

Closes #1863
2021-04-13 08:46:06 -03:00
Torsten Grote
814b2b2582 Make view state of text send UI easier to reason about
and fix bugs with bomb badge and hint display
2021-04-13 08:46:05 -03:00
Torsten Grote
56705bde74 Show bomb badge in same style as send button 2021-04-13 08:46:05 -03:00
Torsten Grote
dceb38b777 Show a bomb badge on the send button when disappearing messages is active 2021-04-13 08:46:05 -03:00
Torsten Grote
9947a6aa1b Use a different hint in conversation when message will disappear
and keep the hint updated when the auto-delete timer changes
2021-04-13 08:46:04 -03:00
Torsten Grote
7a3be374c8 Broadcast event when auto delete timer is mirrored 2021-04-13 08:46:04 -03:00
Torsten Grote
4ea3ce0e3c 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-04-13 08:46:04 -03:00
Torsten Grote
923185b3f4 Show timer change notices in private conversations 2021-04-13 08:46:03 -03:00
Torsten Grote
d91e6c6c1a Allow setting a self-destruct timer
This is a rough prototype of #1837 meant to make testing the UI easier.
2021-04-13 08:46:03 -03:00
akwizgran
1c93a79448 Use Collections.sort() to satisfy Animal Sniffer. 2021-04-13 08:46:03 -03:00
akwizgran
e12ad0cd79 Add integration tests for timer mirroring. 2021-04-13 08:46:02 -03:00
akwizgran
8d6bd29b93 Add method for UI and tests to get current timer. 2021-04-13 08:46:02 -03:00
akwizgran
f941a73999 Update integration tests. 2021-04-13 08:46:02 -03:00
akwizgran
c3057141d8 Don't receive auto-delete timer from remote accept message as introducee. 2021-04-13 08:46:01 -03:00
akwizgran
49080cb64c Hook up incoming messages to the auto-delete manager. 2021-04-13 08:46:01 -03:00
akwizgran
27dbe23914 Mirror the remote auto-delete timer. 2021-04-13 08:46:01 -03:00
akwizgran
d7a2de5817 Add integration tests for auto-delete timer. 2021-04-13 08:46:00 -03:00
akwizgran
0328aa0630 Forwarded accept messages aren't visible to the introducee. 2021-04-13 08:46:00 -03:00
akwizgran
b6cf302131 Only use conversation timestamp for messages that will be visible in conversation. 2021-04-13 08:46:00 -03:00
akwizgran
e2a894acd3 Get timestamp for abort message in same way as other messages. 2021-04-13 08:45:59 -03:00
akwizgran
00ed6d9bb8 Look up auto-delete timer when creating private group invitation. 2021-04-13 08:45:59 -03:00
akwizgran
c9a9734368 Use the right timestamp when signing private group invitation. 2021-04-13 08:45:58 -03:00
akwizgran
efc56a8724 Provide TransactionManager. 2021-04-13 08:45:58 -03:00
akwizgran
6e6923b108 Look up conversation timestamp when creating group invitation messages. 2021-04-13 08:45:58 -03:00
akwizgran
f459beccdb Move lookup of latest conversation timestamp to core for blog and forum sharing. 2021-04-13 08:45:57 -03:00
akwizgran
751c5a3245 Move lookup of latest conversation timestamp to core. 2021-04-13 08:45:57 -03:00
akwizgran
8488499da6 Add transactional variant of getGroupCount(). 2021-04-13 08:45:57 -03:00
akwizgran
96a7e3c425 Send current minor version of messaging client to contacts. 2021-04-13 08:45:56 -03:00
Torsten Grote
0dcf510466 Show bomb icon for messages with auto-destruct timer 2021-04-13 08:45:56 -03:00
akwizgran
0427b12d52 Check that timer argument is legal before storing. 2021-04-13 08:45:56 -03:00
akwizgran
9256c66fcc Add unit tests for AutoDeleteManagerImpl. 2021-04-13 08:45:55 -03:00
akwizgran
706f4e1c4c Implement AutoDeleteManager. 2021-04-13 08:45:55 -03:00
akwizgran
96debcd616 Add dummy implementation of AutoDeleteManager. 2021-04-13 08:45:54 -03:00
akwizgran
07f20e1e0d Refactor auto-delete code from Bramble to Briar. 2021-04-13 08:45:54 -03:00
akwizgran
fee2e503bd Rewrap lines. 2021-04-13 08:45:54 -03:00
akwizgran
f9f260bbc1 Factor out methods for storing and retrieving contact ID. 2021-04-13 08:45:53 -03:00
akwizgran
61718192ee Factor out method for validating auto-delete timers. 2021-04-13 08:45:53 -03:00
akwizgran
27893f9cdd Update comments. 2021-04-13 08:45:53 -03:00
akwizgran
9b0b80ef04 Add unit tests for validating auto-delete timer. 2021-04-13 08:45:52 -03:00
akwizgran
3e1c2df4b1 Update private group invitation client to include self-destruct timers. 2021-04-13 08:45:52 -03:00
akwizgran
fa745410cc Update blog and forum sharing clients to include self-destruct timers. 2021-04-13 08:45:51 -03:00
akwizgran
a427624e8d Update message parsing and encoding to include auto-delete timer. 2021-04-13 08:45:51 -03:00
akwizgran
3798ca1e17 Update introduction validator to support auto-delete timers. 2021-04-13 08:45:51 -03:00
akwizgran
113120b3ab Add constant for NO_AUTO_DELETE_TIMER, address review comments. 2021-04-13 08:45:50 -03:00
akwizgran
b10ca5b77f Add unit tests for private message validation. 2021-04-13 08:45:50 -03:00
akwizgran
f10e3d756a Fix comments in PrivateMessageValidator. 2021-04-13 08:45:50 -03:00
akwizgran
9608b974ec Add integration test for auto-delete timer in private messages. 2021-04-13 08:45:49 -03:00
akwizgran
3b6cc9c633 Add auto-deletion timer to private messages. 2021-04-13 08:45:47 -03:00
akwizgran
5305dd62d1 Merge branch '1872-key-agreement' into 'master'
Finish migrating KeyAgreementActivity to ViewModel

Closes #1982 and #1872

See merge request briar/briar!1357
2021-04-12 13:19:51 +00:00
akwizgran
a066190c60 Merge branch '57-detect-db-durability-failures' into 'master'
Implement dirty flag to detect durability failures

Closes #57

See merge request briar/briar!1424
2021-04-12 13:17:28 +00:00
akwizgran
cdae8b35f5 Another small refactoring to make control flow easier to understand. 2021-04-12 10:06:49 -03:00
Torsten Grote
6ee57315dd Prevent NPE when onQrCodeDecoded() is called after we stop to listen 2021-04-12 10:06:48 -03:00
Sebastian Kürten
64f682146d Integrate merge request feedback 2021-04-12 13:04:42 +02:00
akwizgran
36525fbe9d Merge branch 'promo-video' into 'master'
Instrumentation test for tutorial video and sign-in

Closes #1967

See merge request briar/briar!1423
2021-04-12 10:46:03 +00:00
Torsten Grote
8f628f2d45 Fix PromoVideoTest for CI 2021-04-09 15:24:13 -03:00
Torsten Grote
5e84e5b8b6 Stop listening to key agreement connections when leaving fragment
Also don't liberate screen orientation when backing out of adding a contact nearby
2021-04-09 15:20:39 -03:00
Torsten Grote
a64878bd00 Also reset payload flags when resetting AddNearbyContact state 2021-04-08 14:21:17 -03:00
Torsten Grote
c53fc5eaa3 Add inspection profile to repo
so we can all share the same inspection profile and benefit from its warnings
2021-04-08 13:55:26 -03:00
akwizgran
212751c835 Return a value instead of passing a runnable argument. 2021-04-08 17:11:07 +01:00
akwizgran
fe1c6acebb Remove workaround for Android issue #190966. 2021-04-08 17:03:28 +01:00
Sebastian Kürten
ce47bfe018 Write tests for the dirty flag 2021-04-08 12:24:23 +02:00
Torsten Grote
0ee4ade404 One more round of addressing AddNearbyContact review feedback 2021-04-07 16:18:18 -03:00
Sebastian Kürten
e99df2b69e Log dirty flag when opening database 2021-04-06 20:19:28 +02:00
Sebastian Kürten
db84d07c38 Store a dirty flag in the database 2021-04-06 19:45:26 +02:00
Torsten Grote
db610cfb4c Run only tests from android package on emulator
Otherwise, it re-runs bramble tests for some reason
2021-04-01 17:12:04 -03:00
Torsten Grote
5b52417d20 Check if Bluetooth is supported before requesting discoverability 2021-04-01 15:36:55 -03:00
Torsten Grote
8a768cf933 Add a test for sign-in
This requires an account to exist before as we can't restart our lifecycle.
So we don't automatically clear app data after each test, but rather need to delete an existing account manually before each test.
2021-04-01 14:31:34 -03:00
akwizgran
bebf3bbc39 Merge branch '1826-settings-view-model' into 'master'
Finish migrating SettingsFragment to ViewModel

Closes #1942 and #1826

See merge request briar/briar!1350
2021-04-01 13:20:12 +00:00
Torsten Grote
8a3dd5472b Make a screenshot when test fails to help with debugging 2021-03-31 13:36:55 -03:00
Torsten Grote
f971533a5b Add a way to detect if code runs as instrumentation test
and disable some dialogs to make tests easier to write
2021-03-31 13:36:55 -03:00
Torsten Grote
a12166c13b Use Android Test Orchestrator to have a clean state for each test
fixes flaky/broken espresso tests
2021-03-31 13:36:54 -03:00
Torsten Grote
51624a31e3 Add a first PromoVideoTest 2021-03-31 13:36:54 -03:00
Torsten Grote
cdc632e1af Don't show screen filter and expiry warnings in screenshot tests 2021-03-30 16:56:03 -03:00
Torsten Grote
31f87f647e Create an OverlayView so we can show taps in espresso tests 2021-03-30 16:56:03 -03:00
Torsten Grote
dcd37f71d1 Turn splash screen duration into a resource variable
so screenshot tests can define a different duration
2021-03-30 16:56:03 -03:00
Torsten Grote
4ca286b28e Allow to decide whether test contacts should have alias 2021-03-30 16:56:02 -03:00
Torsten Grote
4f3e4b019a Request user to turn on location for adding contact nearby on API 28+ 2021-03-29 11:30:17 -03:00
akwizgran
62cca1335f Bump version numbers for 1.2.20 release. 2021-03-29 13:33:12 +01:00
akwizgran
11a18859fb Update translations. 2021-03-29 13:33:12 +01:00
akwizgran
1116a7e125 Merge branch 'update-bridges-again' into 'master'
Remove a failing bridge

See merge request briar/briar!1422
2021-03-29 12:32:11 +00:00
akwizgran
415b315292 Add a Tor Browser default bridge. 2021-03-29 13:01:27 +01:00
akwizgran
9818ec2b66 Remove a failing bridge. 2021-03-29 12:55:04 +01:00
Torsten Grote
95ef061a34 Pick up screen lock changes when returning to SecurityFragment 2021-03-26 14:33:58 -03:00
Torsten Grote
aaaf8aa66f Go back to security settings when pressing navigation icon in ChangePasswordActivity 2021-03-26 14:12:02 -03:00
Torsten Grote
29965e38d0 Don't show Toast off the UiThread 2021-03-26 14:10:37 -03:00
Torsten Grote
371d49a213 Use SwitchPreferenceCompat for panic preferences
Addresses #1991
2021-03-26 14:10:36 -03:00
Torsten Grote
6ed95e145e Re-open DisplayFragment after changing theme 2021-03-26 13:48:20 -03:00
Torsten Grote
8c025c1173 review: fix nullability and visibility of settings 2021-03-26 13:48:19 -03:00
Torsten Grote
9ce541cc31 Allow settings titles on more than a single line 2021-03-26 13:48:19 -03:00
Torsten Grote
aa57a4c123 lint ignore icon tinting since it seems to work on Android 4 with VectorDrawableCompat 2021-03-26 13:48:18 -03:00
Torsten Grote
58d9deb3b8 Move avatar layout into own preference
which is only shown on main settings fragment
2021-03-26 13:48:18 -03:00
Torsten Grote
f0685c4a43 Get rid of custom switch preference 2021-03-26 13:48:18 -03:00
Torsten Grote
484817db08 Move notifications settings into own screen 2021-03-26 13:48:17 -03:00
Torsten Grote
670bf15d31 Move security settings into own screen 2021-03-26 13:48:17 -03:00
Torsten Grote
6df1e0fd77 Move connections settings into own screen 2021-03-26 13:48:17 -03:00
Torsten Grote
ec910cb80f Move Display category into its own settings screen 2021-03-26 13:48:16 -03:00
akwizgran
372516646d Merge branch '1970-blog-bugs' into 'master'
Fix issues with blogs after refactoring

See merge request briar/briar!1421
2021-03-26 14:32:11 +00:00
Torsten Grote
72e721b0d3 Don't show snackbar about local blog post again after screen rotation 2021-03-26 10:56:57 -03:00
Torsten Grote
6599093611 Improve blog author clickability
resolves issue where clicking reblogged author opened reblogging author's blog
2021-03-26 10:40:51 -03:00
Torsten Grote
dceeecf1fe Open blog posts from blog feed in BlogActivity 2021-03-26 10:23:31 -03:00
Torsten Grote
ace0b9a3d8 Merge branch 'update-bridges' into 'master'
Replace a failing bridge with a Tor Browser default bridge

See merge request briar/briar!1420
2021-03-26 11:59:03 +00:00
akwizgran
7c45c90de9 Replace a failing bridge with a Tor Browser default bridge. 2021-03-26 09:27:36 +00:00
akwizgran
c2a4b5e26a Bump version numbers for 1.2.19 release. 2021-03-25 17:36:04 +00:00
akwizgran
feac0ad802 Update translations. 2021-03-25 17:34:02 +00:00
akwizgran
60478eba3f Merge branch '1866-blog-controller' into 'master'
Migrate BlogController and FeedController to ViewModel

Closes #1891 and #1866

See merge request briar/briar!1342
2021-03-25 17:25:43 +00:00
akwizgran
3639952612 Merge branch 'espresso-ci' into 'master'
Run instrumentation tests in CI when briar-android changes

Closes admin#20

See merge request briar/briar!1413
2021-03-25 15:47:18 +00:00
akwizgran
c4a654b267 Merge branch '1979-feedback-crash' into 'master'
Don't crash when pressing SHOW with user information when sending feedback

Closes #1979

See merge request briar/briar!1418
2021-03-25 13:29:43 +00:00
Torsten Grote
ecb31a4d32 Don't crash when pressing SHOW with user information when sending feedback 2021-03-25 08:47:18 -03:00
Torsten Grote
76f201bb2f Run Espresso tests manually as they are still too flaky 2021-03-24 16:10:33 -03:00
Torsten Grote
1d44305e34 Catch exception when calling Camera#getParameters()
Fixes #1982
2021-03-24 15:23:16 -03:00
Torsten Grote
a37af592cd Use new ActivityResultLauncher to request permissions for AddNearbyContact 2021-03-24 15:03:53 -03:00
akwizgran
87799b743c Add Burmese translation to language chooser. 2021-03-24 15:28:33 +00:00
akwizgran
b898a7c370 Update translations, add Burmese translation. 2021-03-24 13:54:00 +00:00
Torsten Grote
7f486eef4c Refactor more code into AddNearbyContactViewModel
thus concentrating the logic there needing less back and forth with the activity
2021-03-23 18:09:57 -03:00
Torsten Grote
f3210e3af2 Allow DbViewModel work on things other than lists. 2021-03-23 12:59:16 -03:00
akwizgran
225fd6fd49 Merge branch 'headless-remove-type-args-in-jar-sorting-algorithm' into 'master'
Remove redundant type args in briar-headless/build.gradle

See merge request briar/briar!1416
2021-03-23 12:38:53 +00:00
Sebastian Kürten
400d259a60 Remove redundant type args in briar-headless/build.gradle
The TreeMap<> doesn't need to repeat <String, JarEntry> from
Map<String, JarEntry>.
2021-03-23 07:44:02 +01:00
Torsten Grote
4074ac8578 Add handleException() to DbViewModel
and use it for blogs
2021-03-22 15:17:30 -03:00
Torsten Grote
b2e6dd4138 publish log files as artifacts when emulator job fails 2021-03-19 14:19:07 -03:00
Torsten Grote
b608b42174 Run instrumentation tests in CI when briar-android changes 2021-03-18 12:15:47 -03:00
Torsten Grote
f603254153 Fix instrumentation tests 2021-03-18 12:15:46 -03:00
Torsten Grote
c851dd228b Add a different (faster) way to exclude large/slow tests 2021-03-18 12:15:46 -03:00
Torsten Grote
e97478a21a Don't reload blog data when configuration changes 2021-03-17 14:16:02 -03:00
Torsten Grote
726ebcea3f Make blog post author clickable when not already in their blog 2021-03-17 14:16:02 -03:00
Torsten Grote
2f969775d8 Remove TransactionManager from blog's BaseViewModel 2021-03-17 14:16:02 -03:00
Torsten Grote
d3b855318c Anticipate review feedback for blog view models after re-basing 2021-03-17 14:16:01 -03:00
Torsten Grote
95104d3383 Clean up after migrating blog controllers to view model 2021-03-17 14:16:01 -03:00
Torsten Grote
6860a04e8b Don't use layoutManager hack to restore scrolling position of blogs
not needed anymore when posts are cached in viewmodels
2021-03-17 14:16:01 -03:00
Torsten Grote
33c24f8655 Migrate blogs to new SharingController
and get rid of the deprecated one
2021-03-17 14:16:00 -03:00
Torsten Grote
1fa4b78474 Migrate BlogController to BlogViewModel 2021-03-17 14:16:00 -03:00
Torsten Grote
b678de7529 Make BlogAdapter final and don't pass in a FragmentManager 2021-03-17 14:16:00 -03:00
Torsten Grote
ab1ed0ff5a Turn FeedController into FeedViewModel 2021-03-17 14:15:59 -03:00
Torsten Grote
ad20e5230a Allow blog posts to be loaded within one transaction 2021-03-17 14:15:59 -03:00
Torsten Grote
bcc0442add Merge activities for adding contact nearby
and rename related classes to consolidate names
2021-03-17 14:05:15 -03:00
Torsten Grote
700f6e05bf Factor out permission related code from KeyAgreementActivity to AddNearbyContactPermissionManager 2021-03-17 14:02:54 -03:00
Torsten Grote
d8327d6de2 Re-set orientation lock when fragment is left 2021-03-17 14:02:54 -03:00
Torsten Grote
5a55b3d7e3 Move Plugin related code from activity to ViewModel 2021-03-17 14:02:54 -03:00
Torsten Grote
bed87ed439 Move backend comms and logic out of KeyAgreementFragment
into ViewModel
2021-03-17 14:02:53 -03:00
Torsten Grote
6d1f1c7852 Get rid of KeyAgreementEventListener
and communicate via ViewModel
2021-03-17 14:02:53 -03:00
Torsten Grote
f6b3bde724 Introduce ContactExchangeResult
to include all result information in LiveData
2021-03-17 14:02:53 -03:00
Torsten Grote
94ec22bef8 Move keyagreement package into contact.add.nearby
and fix some small warnings in the process
2021-03-17 14:02:50 -03:00
Torsten Grote
ae923e5777 Merge branch '1871-viewmodel-for-introduction' into 'master'
Introduce ViewModel for IntroductionActivity (and ContactChooserFragment)

See merge request briar/briar!1349
2021-03-16 18:46:45 +00:00
Sebastian Kürten
46b4204805 Introduce view model for IntroductionActivity 2021-03-16 19:34:09 +01:00
akwizgran
2257c005b3 Merge branch 'faster-animations' into 'master'
Use a central attribute for animation speed

See merge request briar/briar!1368
2021-03-15 15:57:26 +00:00
Torsten Grote
eb9ff9c954 Use a central attribute for animation speed 2021-03-15 12:02:43 -03:00
Torsten Grote
4f08f81779 Merge branch 'raise-max-mime-type-length' into 'master'
Test that a max-length attachment fits into a record.

See merge request briar/briar!1411
2021-03-15 13:37:12 +00:00
akwizgran
2b0815aaac Merge branch '1951-exclude-files-from-backup' into 'master'
Exclude all our files from backup

Closes #1951

See merge request briar/briar!1408
2021-03-15 13:25:52 +00:00
akwizgran
a9e83491d3 Test that a max-length attachment fits into a record. 2021-03-15 13:17:07 +00:00
Torsten Grote
ee967c5d8f Merge branch 'raise-max-mime-type-length' into 'master'
Raise MAX_CONTENT_TYPE_BYTES to 80, lower MAX_PRIVATE_MESSAGE_TEXT_LENGTH

See merge request briar/briar!1409
2021-03-15 13:06:24 +00:00
akwizgran
43740777d4 Raise MAX_CONTENT_TYPE_BYTES to 80, lower MAX_PRIVATE_MESSAGE_TEXT_LENGTH.
In case we ever want to send "application/vnd.openxmlformats-officedocument.wordprocessingml.document" attachments.
2021-03-12 09:45:19 +00:00
Torsten Grote
d5b0556ea2 Exclude all our files from backup
Even though we don't allow backup at all, Android seems to go into the direction of overriding this. For now only for device-to-device backups, even though we could not verify this.
2021-03-11 16:29:44 -03:00
Torsten Grote
227f00c10c Merge branch '1899-catch-npe-from-bluetooth-socket' into 'master'
Catch NPE from BluetoothSocket#connect()

Closes #1899

See merge request briar/briar!1407
2021-03-11 18:09:59 +00:00
akwizgran
8b4ff2dc8a Catch NPE from BluetoothSocket#connect(). 2021-03-11 18:00:45 +00:00
akwizgran
4be2afb915 Merge branch 'do-not-try-to-load-unsupported-content-types' into 'master'
Don't try to load attachments with unsupported content types

See merge request briar/briar!1405
2021-03-11 17:48:55 +00:00
Torsten Grote
74447b8ec3 Merge branch 'allow-gifs' into 'master'
Allow GIFs to be chosen on all API levels

See merge request briar/briar!1402
2021-03-11 17:25:50 +00:00
akwizgran
d95242bd7e Don't try to load attachments with unsupported content types. 2021-03-11 16:59:56 +00:00
akwizgran
51794424ce Bump version numbers for 1.2.18 release. 2021-03-11 15:25:32 +00:00
Torsten Grote
5db099bae6 Merge branch 'update-bridges' into 'master'
Update list of Tor bridges

See merge request briar/briar!1403
2021-03-11 15:20:11 +00:00
Torsten Grote
a2faa3bd3b Merge branch '1612-do-not-strip-libs' into 'master'
Don't strip libraries even if the NDK is installed

See merge request briar/briar!1401
2021-03-11 15:14:20 +00:00
akwizgran
a3fb7b5680 Update list of Tor bridges. 2021-03-11 14:24:46 +00:00
akwizgran
264d110dbd Bump version numbers for 1.2.17 release. 2021-03-11 12:35:19 +00:00
akwizgran
839b871a45 Merge branch 'aarch64-finalization' into 'master'
Make headless work on aarch64 and armhf (armv7)

Closes #1854

See merge request briar/briar!1376
2021-03-11 12:28:55 +00:00
akwizgran
2fb4825b8f Don't strip libraries even if the NDK is installed.
This allows reproducible builds regardless of whether the NDK is installed.
2021-03-11 12:20:41 +00:00
Torsten Grote
3f9a66b1b6 Merge branch '1964-no-colons' into 'master'
Remove colons from default filename

Closes #1964

See merge request briar/briar!1400
2021-03-11 11:36:49 +00:00
akwizgran
d796916387 Also remove colons on API >= 19. 2021-03-11 10:33:53 +00:00
akwizgran
fe07b760ea Remove colons from default filename. 2021-03-10 15:44:15 +00:00
akwizgran
b4a5fe6772 Allow GIFs to be chosen on API < 24.
We can compress them without resizing.
2021-03-10 15:42:20 +00:00
Nico Alt
e21e6267d7 Update Tor dependency to include armhf binary
Related MR:
https://code.briarproject.org/briar/tor-reproducer/-/merge_requests/13
2021-03-09 10:46:43 +01:00
Nico Alt
d7afbdf690 Use Tor binary for armhf (armv7)
Example devices are Nexus 5 and Raspberry Pi v2.

Based on https://code.briarproject.org/briar/briar/-/merge_requests/1376

Related to https://code.briarproject.org/briar/briar/-/issues/1854
2021-03-09 12:00:00 +00:00
Torsten Grote
c5d2661c1d Merge branch '1919-password-fields-not-focusable' into 'master'
Condition display of progressbar on a isCreatingAccount LiveData

Closes #1819 and #1919

See merge request briar/briar!1355
2021-03-03 13:10:29 +00:00
Nico Alt
b738bdd14e Actually make headless work on arm aarch64
Following the two comments at
https://code.briarproject.org/briar/briar/-/issues/1854#note_44340

.jar files now get built with

    $ ./gradlew --configure-on-demand briar-headless:x86LinuxJar
    $ ./gradlew --configure-on-demand briar-headless:aarch64LinuxJar

Related to #1854
2021-03-03 12:00:00 +00:00
Daniel Lublin
6b61725c6a Condition display of progressbar on a isCreatingAccount LiveData
Avoiding the mess with saving onSaveInstanceState, and the (in this
case) unwanted restoring of it upon back-button tap.

Closes #1919

Test instructions:

- Precondition: fresh install, setting up a new account
  - Testing specific bug fix:
    - Choose a name, tap next
    - Choose a password, tap next
      - Not testable on some devices which display "Create account" instead of "Next"
    - You are now on Background connections screen
    - Tap Back-button ◁
    - Ensure that password can be changed again
  - During setup process, rotate device and ensure that:
    - entered text is kept
    - progressbar is continuously displayed
2021-02-17 13:57:08 +01:00
323 changed files with 9257 additions and 5828 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,30 +1,70 @@
image: briar/ci-image-android:latest image: briar/ci-image-android:latest
stages: stages:
- test - test
- optional_tests - optional_tests
- check_reproducibility - check_reproducibility
test: .base-test:
stage: test
before_script: before_script:
- set -e - set -e
- export GRADLE_USER_HOME=$PWD/.gradle - export GRADLE_USER_HOME=$PWD/.gradle
cache: cache:
key: "$CI_COMMIT_REF_SLUG"
paths: paths:
- .gradle/wrapper - .gradle/wrapper
- .gradle/caches - .gradle/caches
script:
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources
after_script: after_script:
# these file change every time but should not be cached # these file change every time and should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock - rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/ - rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
test:
extends: .base-test
stage: test
script:
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: always
- when: always
android test:
extends: .base-test
stage: optional_tests
image: briar/ci-image-android-emulator:latest
script:
# start emulator first, so it can fail early
- start-emulator.sh
# run normal and screenshot tests together (exclude Large tests)
- ./gradlew -Djava.security.egd=file:/dev/urandom connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=org.briarproject.briar.android -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest
after_script:
- adb pull /sdcard/Pictures/screenshots
artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths:
- kernel.log
- logcat.txt
- briar-android/build/reports/androidTests/connected/flavors/*
- screenshots
expire_in: 3 days
when: on_failure
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: on_success
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- briar-android/**/*
when: manual
allow_failure: true
- if: '$CI_COMMIT_TAG == null'
when: manual
allow_failure: true
retry:
max: 1
tags:
- kvm
test_reproducible: test_reproducible:
stage: check_reproducibility stage: check_reproducibility
@@ -40,6 +80,7 @@ test_reproducible:
- export GRADLE_USER_HOME=$PWD/.gradle - export GRADLE_USER_HOME=$PWD/.gradle
cache: cache:
key: "$CI_COMMIT_REF_SLUG"
paths: paths:
- .gradle/wrapper - .gradle/wrapper
- .gradle/caches - .gradle/caches
@@ -52,11 +93,15 @@ test_reproducible:
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock - rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/ - rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
manual_tests: bridge test:
extends: .optional_tests extends: .optional_tests
when: manual rules:
except: - if: '$CI_PIPELINE_SOURCE == "schedule"'
- tags when: on_success
allow_failure: true
- if: '$CI_COMMIT_TAG == null'
when: manual
allow_failure: true
pre_release_tests: pre_release_tests:
extends: .optional_tests extends: .optional_tests

View File

@@ -0,0 +1,14 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreObjectMethods" value="true" />
<option name="ignoreAnonymousClassMethods" value="false" />
</inspection_tool>
<inspection_tool class="WeakerAccess" enabled="true" level="WARNING" enabled_by_default="true">
<option name="SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS" value="true" />
<option name="SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES" value="true" />
<option name="SUGGEST_PRIVATE_FOR_INNERS" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,51 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Instrumentation Tests (destroys DB)" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="briar.briar-android" />
<option name="TESTING_TYPE" value="1" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="org.briarproject.briar.android" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

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

View File

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

View File

@@ -114,12 +114,8 @@ public class AndroidUtils {
/** /**
* Returns an array of supported content types for image attachments. * Returns an array of supported content types for image attachments.
* GIFs can't be compressed on API < 24 so they're not supported.
* <p>
* TODO: Remove this restriction when large message support is added
*/ */
public static String[] getSupportedImageContentTypes() { public static String[] getSupportedImageContentTypes() {
if (SDK_INT < 24) return new String[] {"image/jpeg", "image/png"}; return new String[] {"image/jpeg", "image/png", "image/gif"};
else return new String[] {"image/jpeg", "image/png", "image/gif"};
} }
} }

View File

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

View File

@@ -0,0 +1,35 @@
package org.briarproject.bramble.api.contact.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when the alias for a contact changed.
*/
@Immutable
@NotNullByDefault
public class ContactAliasChangedEvent extends Event {
private final ContactId contactId;
@Nullable
private final String alias;
public ContactAliasChangedEvent(ContactId contactId,
@Nullable String alias) {
this.contactId = contactId;
this.alias = alias;
}
public ContactId getContactId() {
return contactId;
}
@Nullable
public String getAlias() {
return alias;
}
}

View File

@@ -57,6 +57,7 @@ public class Author implements Nameable {
/** /**
* Returns the author's name. * Returns the author's name.
*/ */
@Override
public String getName() { public String getName() {
return name; return name;
} }

View File

@@ -21,5 +21,6 @@ public interface ReportingConstants {
* Hidden service address for reporting crashes and feedback to the * Hidden service address for reporting crashes and feedback to the
* developers. * developers.
*/ */
String DEV_ONION_ADDRESS = "cwqmubyvnig3wag3.onion"; String DEV_ONION_ADDRESS =
"b2nkt5doeamvdmjzfz7g42hk5vdtlnktlgzhel2bgjcc4v4jhnx2qrqd.onion";
} }

View File

@@ -68,6 +68,13 @@ interface Database<T> {
*/ */
void close() throws DbException; void close() throws DbException;
/**
* Returns true if the dirty flag was set while opening the database,
* indicating that the database has not been shut down properly the last
* time it was closed and some data could be lost.
*/
boolean wasDirtyOnInitialisation();
/** /**
* Starts a new transaction and returns an object representing it. * Starts a new transaction and returns an object representing it.
*/ */

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContact; import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactId; import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent; import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent; import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent; import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
@@ -1013,6 +1014,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
transaction.attach(new ContactAliasChangedEvent(c, alias));
db.setContactAlias(txn, c, alias); db.setContactAlias(txn, c, alias);
} }

View File

@@ -37,4 +37,10 @@ interface DatabaseConstants {
* has passed since the last compaction. * has passed since the last compaction.
*/ */
long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30); long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
/**
* The {@link Settings} key under which the flag is stored indicating
* whether the database is marked as dirty.
*/
String DIRTY_KEY = "dirty";
} }

View File

@@ -84,9 +84,14 @@ class H2Database extends JdbcDatabase {
@Override @Override
public void close() throws DbException { public void close() throws DbException {
// H2 will close the database when the last connection closes // H2 will close the database when the last connection closes
Connection c = null;
try { try {
c = createConnection();
super.closeAllConnections(); super.closeAllConnections();
setDirty(c, false);
c.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(c, LOG, WARNING);
throw new DbException(e); throw new DbException(e);
} }
} }

View File

@@ -81,6 +81,7 @@ class HyperSqlDatabase extends JdbcDatabase {
try { try {
super.closeAllConnections(); super.closeAllConnections();
c = createConnection(); c = createConnection();
setDirty(c, false);
s = c.createStatement(); s = c.createStatement();
s.executeQuery("SHUTDOWN"); s.executeQuery("SHUTDOWN");
s.close(); s.close();

View File

@@ -83,6 +83,7 @@ import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERE
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING; import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN; import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE; import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
import static org.briarproject.bramble.db.DatabaseConstants.DIRTY_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY; import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS; import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY; import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
@@ -365,9 +366,14 @@ abstract class JdbcDatabase implements Database<Connection> {
@GuardedBy("connectionsLock") @GuardedBy("connectionsLock")
private boolean closed = false; private boolean closed = false;
private volatile boolean wasDirtyOnInitialisation = false;
protected abstract Connection createConnection() protected abstract Connection createConnection()
throws DbException, SQLException; throws DbException, SQLException;
// Used exclusively during open to compact the database after schema
// migrations or after DatabaseConstants#MAX_COMPACTION_INTERVAL_MS has
// elapsed
protected abstract void compactAndClose() throws DbException; protected abstract void compactAndClose() throws DbException;
JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory, JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
@@ -392,13 +398,19 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
if (reopen) { if (reopen) {
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE); Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
wasDirtyOnInitialisation = isDirty(s);
compact = migrateSchema(txn, s, listener) || isCompactionDue(s); compact = migrateSchema(txn, s, listener) || isCompactionDue(s);
} else { } else {
wasDirtyOnInitialisation = false;
createTables(txn); createTables(txn);
initialiseSettings(txn); initialiseSettings(txn);
compact = false; compact = false;
} }
if (LOG.isLoggable(INFO)) {
LOG.info("db dirty? " + wasDirtyOnInitialisation);
}
createIndexes(txn); createIndexes(txn);
setDirty(txn, true);
commitTransaction(txn); commitTransaction(txn);
} catch (DbException e) { } catch (DbException e) {
abortTransaction(txn); abortTransaction(txn);
@@ -425,6 +437,11 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public boolean wasDirtyOnInitialisation() {
return wasDirtyOnInitialisation;
}
/** /**
* Compares the schema version stored in the database with the schema * Compares the schema version stored in the database with the schema
* version used by the current code and applies any suitable migrations to * version used by the current code and applies any suitable migrations to
@@ -500,6 +517,16 @@ abstract class JdbcDatabase implements Database<Connection> {
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE); mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
} }
private boolean isDirty(Settings s) {
return s.getBoolean(DIRTY_KEY, false);
}
protected void setDirty(Connection txn, boolean dirty) throws DbException {
Settings s = new Settings();
s.putBoolean(DIRTY_KEY, dirty);
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
}
private void initialiseSettings(Connection txn) throws DbException { private void initialiseSettings(Connection txn) throws DbException {
Settings s = new Settings(); Settings s = new Settings();
s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION); s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);

View File

@@ -34,12 +34,12 @@ class KeyAgreementTransport {
Logger.getLogger(KeyAgreementTransport.class.getName()); Logger.getLogger(KeyAgreementTransport.class.getName());
// Accept records with current protocol version, known record type // Accept records with current protocol version, known record type
private static Predicate<Record> ACCEPT = r -> private static final Predicate<Record> ACCEPT = r ->
r.getProtocolVersion() == PROTOCOL_VERSION && r.getProtocolVersion() == PROTOCOL_VERSION &&
isKnownRecordType(r.getRecordType()); isKnownRecordType(r.getRecordType());
// Ignore records with current protocol version, unknown record type // Ignore records with current protocol version, unknown record type
private static Predicate<Record> IGNORE = r -> private static final Predicate<Record> IGNORE = r ->
r.getProtocolVersion() == PROTOCOL_VERSION && r.getProtocolVersion() == PROTOCOL_VERSION &&
!isKnownRecordType(r.getRecordType()); !isKnownRecordType(r.getRecordType());

View File

@@ -1,13 +1,12 @@
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1 Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1 Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0 Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0 Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0 Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0 Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0 Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0 Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
Bridge obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0 Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0 Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0 Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0

View File

@@ -41,8 +41,12 @@ import org.junit.Test;
import java.io.File; import java.io.File;
import java.sql.Connection; import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -2346,6 +2350,67 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close(); db.close();
} }
@Test
public void testShutdownGracefully() throws Exception {
Database<Connection> db = open(false);
db.close();
open(true);
assertFalse(db.wasDirtyOnInitialisation());
}
@Test
public void testShutdownDirty() throws Exception {
Database<Connection> db = open(false);
// We want to simulate a dirty shutdown here which would normally be
// caused by an empty battery or by force closing the Android app.
// As there is no obvious way to simulate this, we're artificially
// causing an SqlException during close() here by unloading the JDBC
// drivers.
List<String> unloadedDrivers = unloadDrivers();
try {
db.close();
fail();
} catch (Exception e) {
// continue
e.printStackTrace();
}
// Reloading drivers to continue so that we're able to work with the
// database again.
reloadDrivers(unloadedDrivers);
db = open(true);
assertTrue(db.wasDirtyOnInitialisation());
}
@Test
public void testShutdownDirtyThenGracefully() throws Exception {
Database<Connection> db = open(false);
// Simulating a dirty shutdown here, look at #testShutdownDirty for
// details.
List<String> unloadedDrivers = unloadDrivers();
try {
db.close();
fail();
} catch (Exception e) {
// continue
}
reloadDrivers(unloadedDrivers);
db = open(true);
assertTrue(db.wasDirtyOnInitialisation());
db.close();
db = open(true);
assertFalse(db.wasDirtyOnInitialisation());
}
@Test @Test
public void testCleanupTimer() throws Exception { public void testCleanupTimer() throws Exception {
long duration = 60_000; long duration = 60_000;
@@ -2482,6 +2547,31 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
rootKey, alice); rootKey, alice);
} }
private List<String> unloadDrivers() {
Enumeration<Driver> drivers = DriverManager.getDrivers();
List<String> unloaded = new ArrayList<>();
while (drivers.hasMoreElements()) {
Driver d = drivers.nextElement();
try {
DriverManager.deregisterDriver(d);
unloaded.add(d.getClass().getName());
} catch (SQLException e) {
e.printStackTrace();
fail();
}
}
return unloaded;
}
private void reloadDrivers(List<String> unloadedDrivers)
throws ClassNotFoundException, IllegalAccessException,
InstantiationException, SQLException {
for (String driverName : unloadedDrivers) {
DriverManager.registerDriver(
(Driver) Class.forName(driverName).newInstance());
}
}
@After @After
public void tearDown() { public void tearDown() {
deleteTestDirectory(testDir); deleteTestDirectory(testDir);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,3 +9,19 @@ source_lang = en
type = ANDROID type = ANDROID
minimum_perc = 80 minimum_perc = 80
[briar.google-play-short-description]
# https://support.google.com/googleplay/android-developer/answer/9844778?hl=en#zippy=%2Cview-list-of-available-languages
lang_map = en: en-US, de: de-DE, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
file_filter = fastlane/metadata/android/<lang>/short_description.txt
source_file = fastlane/metadata/android/en-US/short_description.txt
source_lang = en
type = TXT
minimum_perc = 100
[briar.google-play-full-description]
lang_map = en: en-US, de: de-DE, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
file_filter = fastlane/metadata/android/<lang>/full_description.txt
source_file = fastlane/metadata/android/en-US/full_description.txt
source_lang = en
type = TXT
minimum_perc = 100

View File

@@ -19,11 +19,15 @@ android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.2' buildToolsVersion '30.0.2'
packagingOptions {
doNotStrip '**/*.so'
}
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 10216 versionCode 10302
versionName "1.2.16" versionName "1.3.2"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -76,6 +80,7 @@ android {
} }
testOptions { testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests { unitTests {
includeAndroidResources = true includeAndroidResources = true
} }
@@ -144,6 +149,8 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestUtil 'androidx.test:orchestrator:1.3.0'
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.24" androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
androidTestCompileOnly 'javax.annotation:jsr250-api:1.0' androidTestCompileOnly 'javax.annotation:jsr250-api:1.0'
androidTestImplementation 'junit:junit:4.13.1' androidTestImplementation 'junit:junit:4.13.1'

View File

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

View File

@@ -13,6 +13,8 @@
# Uncomment the line if you want fastlane to automatically update itself # Uncomment the line if you want fastlane to automatically update itself
# update_fastlane # update_fastlane
opt_out_usage
default_platform(:android) default_platform(:android)
platform :android do platform :android do
@@ -24,6 +26,21 @@ platform :android do
system './demo-mode-deactivate.sh' system './demo-mode-deactivate.sh'
system './rename_screenshots.py' system './rename_screenshots.py'
end end
desc "Updates Google Play metadata (title, descriptions, video, etc.)"
lane :metadata do
system './update-metadata.sh'
upload_to_play_store(
skip_upload_apk: true,
skip_upload_aab: true,
skip_upload_metadata: false,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
validate_only: false,
)
end
end end

View File

@@ -0,0 +1,5 @@
El Briar és una aplicació de missatgeria dissenyada per a activistes, periodistes i qualsevol persona en general que necessiti una manera segura, fàcil i robsuta de comunicar-se. Al contrari que les aplicacions tradicionals, el Briar no depèn d'un servidor central: els missatges se sincronitzen directament entre els dispositius dels usuaris. Si Internet cau, el Briar pot continuar funcionant mitjançant Bluetooth o Wi-Fi, mantenint el flux fins i tot amb aquesta situació caòtica. Amb un funcionament normal d'Internet, el Briar es pot sincronitzar a través de la xarxa Tor, protegint els usuaris i les seves comunicacions d'ulls indiscrets.
L'aplicació té com a característica principal l'enviament de missatges privats en grups, fòrums o blocs, atès que es basa en la xarxa Tor. Qualsevol interacció al Briar s'emmagatzema només al vostre dispositiu fins que vulgueu compartir-ho amb altres usuaris.
No hi han anuncis ni seguiment El codi font d'aquesta aplicació està disponible per a tothom que vulgui inspeccionar-ho, com ja l'han auditat empreses professionals. Totes les versions del Briar són reproduïbles, la qual cosa permet verificar que el codi font publicat coincideix exactament amb l'aplicació publicada aquí. El desenvolupament és a càrrec d'un petit equip sense ànim lucratiu.

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

@@ -0,0 +1,5 @@
Briar is a messaging app designed for activists, journalists, and anyone else who needs a safe, easy and robust way to communicate. Unlike traditional messaging apps, Briar doesn't rely on a central server - messages are synchronized directly between the users' devices. If the internet's down, Briar can sync via Bluetooth or Wi-Fi, keeping the information flowing in a crisis. If the internet's up, Briar can sync via the Tor network, protecting users and their relationships from surveillance.
The app features private messages, groups and forums as well as blogs. Support for Tor network is built into the app. Everything you do in Briar is only stored on your device unless you decide to share it with other users.
There are no advertisements and no tracking. The source code of the app is completely open for anyone to inspect and has already been professionally audited. All releases of Briar are reproducible, making it possible to verify that the published source code matches exactly the app published here. Development is done by a small non-profit team.

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

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

View File

@@ -0,0 +1,5 @@
Briar é unha app de mensaxería deseñada para activistas, xornalistas e calquera que precise comunicarse de xeito robusto, simple e seguro. Ao contrario que as apps de mensaxería tradicionais, Briar non depende dun servidor central - as mensaxes sincronízanse directamente entre os dispositivos das persoas usuarias. Se caese internet, Briar pode sincronizar a través de Bluetooth ou Wi-Fi, mantendo o fluxo de información en períodos de crise. Se hai internet, Briar pode sincronizar a través da rede Tor, protexendo ás usuarias e ás súas relacións da vixilancia.
A app proporciona mensaxes privadas, grupos e foros así como blogs. O soporte para a rede Tor está incluído na propia app. Todo o que fas en Briar só se garda no teu dispositivo a non ser que decidas compartilo con outras usuarias.
Non hai publicidade nen rastrexo da túa actividade. O código fonte da app está completamente aberto a calquera que queira revisalo e xa foi auditado profesionalmente. Todas as versións de Briar son reproducibles, facendo posible verificar que o código fonte publicado coincide exactamente coa app publicada aquí. O desenvolvemento realízase por un pequeno equipo de voluntarias.

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

@@ -0,0 +1,5 @@
Briar yra žinučių perdavimo programėlė, skirta aktyvistams, žurnalistams ir visiems kitiems, kam reikia saugaus, lengvo ir veiksmingo bendravimo būdo. Kitaip nei kitos tradicinės žinučių perdavimo programėlės, Briar nepasikliauja centriniu serveriu - žinutės yra sinchronizuojamos tiesiogiai tarp naudotojų įrenginių. Jei nėra interneto, Briar gali sinchronizuotis per Bluetooth ar belaidį ryšį (Wi-Fi), kad palaikytų informacijos perdavimą kriziniais atvejais. Jei internetas veikia, Briar gali sinchronizuotis per Tor tinklą, apsaugodama naudotojus ir jų tarpusavio ryšius nuo stebėjimo.
Programėlėje yra galimybė naudotis privačiomis žinutėmis, grupėmis, forumais, o taip pat tinklaraščiais. Tor tinklo palaikymas yra įtaisytas į programėlę. Viskas, ką atliekate Briar programėlėje, yra laikoma jūsų įrenginyje, nebent nuspręsite pasidalinti tuo su kitais naudotojais.
Nėra jokių reklamų ir jokio sekimo. Pradinis programėlės kodas yra visiškai atviras kiekvienam, norinčiam jį ištirti, ir jau buvo profesionaliai patikrintas. Visos Briar laidos yra atgaminamos, todėl galima patikrinti ir įsitikinti, kad paskelbtas pradinis kodas tiksliai atitinka čia paskelbtą programėlę. Programėlės plėtojimą vykdo maža nepelno siekianti komanda.

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

@@ -0,0 +1,5 @@
Briar aktivistler ve gazeteciler başta olmak üzere güvenli, kolay ve sağlam bir iletişim isteyen herkes için tasarlanmış bir ileti sistemidir. Geleneksel ileti sistemlerinin aksine Briar merkezi bir sunucu kullanmaz, iletiler doğrudan kullanıcıların aygıtları arasında eşleştirilir. Briar, eğer İnternet yoksa Bluetooth veya Wi-Fi aracılığıyla da iletileri iletebilir, böylece kriz durumlarında bilgi akışını sürdürür. İnternet varken Briar Tor ağı aracılığıyla iletim sağlar, böylece kullanıcıları ve ilişkilerini gözetimden korumuş olur.
Bu uygulama özel iletiler, gruplar ve forumlarla birlikte blog özelliklerine sahiptir. Tor ağı desteği uygulamada gömülüdür. Briar üzerinde yaptığınız her şey, siz başka kullanıcılarla paylaşmayı seçmediğiniz sürece, sadece kendi aygıtınızda saklanır.
Ne reklam var, ne de sizi izleme. Uygulamanın kaynak kodu, incelemek isteyen herkese tamamen açıktır ve zaten profesyonel olarak da denetlenmiştir. Briar'ın tüm sürümleri yeniden üretilebilir, böylece yayınlanan kaynak kodun, burada yayınlanan uygulamayla tam olarak eşlendiğini doğrulamak mümkündür. Uygulama kâr amacı gütmeyen küçük bir ekip tarafından geliştirilmektedir.

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

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

View File

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

View File

@@ -0,0 +1 @@
Briar

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
tx pull -a -r briar.google-play-full-description,briar.google-play-short-description
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
for LANG_DIR in "$DIR"/metadata/android/*; do
if [[ "$LANG_DIR" == *en-US ]]; then
continue
fi
if [[ -f "$LANG_DIR/full_description.txt" ]] && [[ -f "$LANG_DIR/short_description.txt" ]]; then
# every language uses the same app title
cp "$DIR/metadata/android/en-US/title.txt" "$LANG_DIR/title.txt"
echo "$LANG_DIR"
else
# not complete, remove
rm -r "$LANG_DIR"
fi
done

View File

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

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.IOException;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.FailureHandler;
import androidx.test.espresso.base.DefaultFailureHandler;
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
import androidx.test.runner.screenshot.Screenshot;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
@NotNullByDefault
public class ScreenshotOnFailureRule implements TestRule {
FailureHandler defaultFailureHandler =
new DefaultFailureHandler(getApplicationContext());
@Override
public Statement apply(Statement base, Description description) {
HashSet<ScreenCaptureProcessor> processors = new HashSet<>(1);
processors.add(new BasicScreenCaptureProcessor());
Screenshot.addScreenCaptureProcessors(processors);
return new Statement() {
@Override
public void evaluate() throws Throwable {
AtomicBoolean errorHandled = new AtomicBoolean(false);
Espresso.setFailureHandler((throwable, matcher) -> {
takeScreenshot(description);
errorHandled.set(true);
defaultFailureHandler.handle(throwable, matcher);
});
try {
base.evaluate();
} catch (Throwable t) {
if (!errorHandled.get()) {
takeScreenshot(description);
}
throw t;
}
}
};
}
private void takeScreenshot(Description description) {
String name = description.getTestClass().getSimpleName();
ScreenCapture capture = Screenshot.capture();
capture.setName(name);
try {
capture.process();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -6,19 +6,25 @@ import android.content.Intent;
import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.junit.ClassRule;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.espresso.intent.rule.IntentsTestRule;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public abstract class UiTest { public abstract class UiTest {
@ClassRule
public static final ScreenshotOnFailureRule screenshotOnFailureRule =
new ScreenshotOnFailureRule();
protected final String USERNAME = protected final String USERNAME =
getApplicationContext().getString(R.string.screenshot_alice); getApplicationContext().getString(R.string.screenshot_alice);
protected static final String PASSWORD = "123456"; protected static final String PASSWORD = "123456";
@@ -27,6 +33,8 @@ public abstract class UiTest {
protected AccountManager accountManager; protected AccountManager accountManager;
@Inject @Inject
protected LifecycleManager lifecycleManager; protected LifecycleManager lifecycleManager;
@Inject
protected SettingsManager settingsManager;
public UiTest() { public UiTest() {
BriarTestComponentApplication app = getApplicationContext(); BriarTestComponentApplication app = getApplicationContext();
@@ -35,26 +43,18 @@ public abstract class UiTest {
protected abstract void inject(BriarUiTestComponent component); protected abstract void inject(BriarUiTestComponent component);
protected void startActivity(Class<? extends Activity> clazz) {
Intent i = new Intent(getApplicationContext(), clazz);
i.addFlags(FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(i);
}
@NotNullByDefault @NotNullByDefault
protected class CleanAccountTestRule<A extends Activity> protected class CleanAccountTestRule<A extends Activity>
extends IntentsTestRule<A> { extends IntentsTestRule<A> {
@Nullable
private final Runnable runnable;
public CleanAccountTestRule(Class<A> activityClass) { public CleanAccountTestRule(Class<A> activityClass) {
super(activityClass); super(activityClass);
this.runnable = null;
}
/**
* Use this if you need to run code before launching the activity.
* Note: You need to use {@link #launchActivity(Intent)} yourself
* to start the activity.
*/
public CleanAccountTestRule(Class<A> activityClass, Runnable runnable) {
super(activityClass, false, false);
this.runnable = runnable;
} }
@Override @Override
@@ -62,16 +62,13 @@ public abstract class UiTest {
super.beforeActivityLaunched(); super.beforeActivityLaunched();
accountManager.deleteAccount(); accountManager.deleteAccount();
accountManager.createAccount(USERNAME, PASSWORD); accountManager.createAccount(USERNAME, PASSWORD);
if (runnable != null) { Intent serviceIntent =
Intent serviceIntent = new Intent(getApplicationContext(), BriarService.class);
new Intent(getApplicationContext(), BriarService.class); getApplicationContext().startService(serviceIntent);
getApplicationContext().startService(serviceIntent); try {
try { lifecycleManager.waitForStartup();
lifecycleManager.waitForStartup(); } catch (InterruptedException e) {
} catch (InterruptedException e) { throw new AssertionError(e);
throw new AssertionError(e);
}
runnable.run();
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.app.Activity; import android.app.Activity;
import android.util.Log;
import android.view.View; import android.view.View;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
@@ -14,9 +15,13 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage; import androidx.test.runner.lifecycle.Stage;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.util.HumanReadables.describe; import static androidx.test.espresso.util.HumanReadables.describe;
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal; import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
import static androidx.test.runner.lifecycle.Stage.RESUMED;
import static java.lang.System.currentTimeMillis; import static java.lang.System.currentTimeMillis;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@@ -25,13 +30,26 @@ public class ViewActions {
private final static long TIMEOUT_MS = SECONDS.toMillis(10); private final static long TIMEOUT_MS = SECONDS.toMillis(10);
private final static long WAIT_MS = 50; private final static long WAIT_MS = 50;
public static void waitFor(final Matcher<View> viewMatcher) {
onView(isRoot()).perform(waitUntilMatches(hasDescendant(viewMatcher)));
}
public static void waitFor(final Class<? extends Activity> clazz) {
onView(isRoot()).perform(waitForActivity(clazz, RESUMED, TIMEOUT_MS));
}
public static void waitFor(final Class<? extends Activity> clazz,
long timeout) {
onView(isRoot()).perform(waitForActivity(clazz, RESUMED, timeout));
}
public static ViewAction waitUntilMatches(Matcher<View> viewMatcher) { public static ViewAction waitUntilMatches(Matcher<View> viewMatcher) {
return waitUntilMatches(viewMatcher, TIMEOUT_MS); return waitUntilMatches(viewMatcher, TIMEOUT_MS);
} }
private static ViewAction waitUntilMatches(Matcher<View> viewMatcher, private static ViewAction waitUntilMatches(Matcher<View> viewMatcher,
long timeout) { long timeout) {
return new CustomViewAction() { return new CustomViewAction(timeout) {
@Override @Override
protected boolean exitConditionTrue(View view) { protected boolean exitConditionTrue(View view) {
for (View child : breadthFirstViewTraversal(view)) { for (View child : breadthFirstViewTraversal(view)) {
@@ -48,24 +66,62 @@ public class ViewActions {
}; };
} }
public static ViewAction waitForActivity(Activity activity, Stage stage) { public static ViewAction waitForActivity(Class<? extends Activity> clazz,
return new CustomViewAction() { Stage stage, long timeout) {
return new CustomViewAction(timeout) {
@Override @Override
protected boolean exitConditionTrue(View view) { protected boolean exitConditionTrue(View view) {
boolean found = false;
ActivityLifecycleMonitor lifecycleMonitor = ActivityLifecycleMonitor lifecycleMonitor =
ActivityLifecycleMonitorRegistry.getInstance(); ActivityLifecycleMonitorRegistry.getInstance();
return lifecycleMonitor.getLifecycleStageOf(activity) == stage; log(lifecycleMonitor);
for (Activity a : lifecycleMonitor
.getActivitiesInStage(stage)) {
if (a.getClass().equals(clazz)) found = true;
}
return found;
}
private void log(ActivityLifecycleMonitor lifecycleMonitor) {
log(lifecycleMonitor, Stage.PRE_ON_CREATE);
log(lifecycleMonitor, Stage.CREATED);
log(lifecycleMonitor, Stage.STARTED);
log(lifecycleMonitor, Stage.RESUMED);
log(lifecycleMonitor, Stage.PAUSED);
log(lifecycleMonitor, Stage.STOPPED);
log(lifecycleMonitor, Stage.RESTARTED);
log(lifecycleMonitor, Stage.DESTROYED);
}
private void log(ActivityLifecycleMonitor lifecycleMonitor,
Stage stage) {
for (Activity a : lifecycleMonitor
.getActivitiesInStage(stage)) {
Log.e("TEST", a.getClass().getSimpleName() +
" is in state " + stage);
}
} }
@Override @Override
public String getDescription() { public String getDescription() {
return "Wait for activity " + activity.getClass().getName() + return "Wait for activity " + clazz.getName() + " in stage " +
" to resume within " + TIMEOUT_MS + " milliseconds."; stage.name() + " within " + timeout +
" milliseconds.";
} }
}; };
} }
private static abstract class CustomViewAction implements ViewAction { private static abstract class CustomViewAction implements ViewAction {
private final long timeout;
public CustomViewAction() {
this(TIMEOUT_MS);
}
public CustomViewAction(long timeout) {
this.timeout = timeout;
}
@Override @Override
public Matcher<View> getConstraints() { public Matcher<View> getConstraints() {
return isDisplayed(); return isDisplayed();
@@ -74,7 +130,7 @@ public class ViewActions {
@Override @Override
public void perform(UiController uiController, View view) { public void perform(UiController uiController, View view) {
uiController.loopMainThreadUntilIdle(); uiController.loopMainThreadUntilIdle();
long endTime = currentTimeMillis() + TIMEOUT_MS; long endTime = currentTimeMillis() + timeout;
do { do {
if (exitConditionTrue(view)) return; if (exitConditionTrue(view)) return;
uiController.loopMainThreadForAtLeast(WAIT_MS); uiController.loopMainThreadForAtLeast(WAIT_MS);

View File

@@ -5,6 +5,8 @@ import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.system.ClockModule; import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.account.SignInTestCreateAccount;
import org.briarproject.briar.android.account.SignInTestSignIn;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest; import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
@@ -28,4 +30,8 @@ public interface BriarUiTestComponent extends AndroidComponent {
void inject(NavDrawerActivityTest test); void inject(NavDrawerActivityTest test);
void inject(SignInTestCreateAccount test);
void inject(SignInTestSignIn test);
} }

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android.account;
import android.view.Gravity;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarUiTestComponent;
import org.briarproject.briar.android.UiTest;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.briarproject.briar.android.ViewActions.waitFor;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.hamcrest.Matchers.endsWith;
@RunWith(AndroidJUnit4.class)
public class SignInTestCreateAccount extends UiTest {
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
}
@Test
public void createAccount() throws Exception {
accountManager.deleteAccount();
accountManager.createAccount(USERNAME, PASSWORD);
startActivity(SplashScreenActivity.class);
lifecycleManager.waitForStartup();
waitFor(NavDrawerActivity.class);
// open nav drawer
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START)))
.perform(DrawerActions.open());
// click onboarding away (once shown)
onView(isRoot()).perform(waitUntilMatches(hasDescendant(
withClassName(endsWith("PromptView")))));
onView(withClassName(endsWith("PromptView")))
.perform(click());
// sign-out manually
onView(withText(R.string.sign_out_button))
.check(matches(isDisplayed()))
.perform(click());
lifecycleManager.waitForShutdown();
}
}

View File

@@ -0,0 +1,59 @@
package org.briarproject.briar.android.account;
import android.view.Gravity;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarUiTestComponent;
import org.briarproject.briar.android.UiTest;
import org.briarproject.briar.android.login.StartupActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.briarproject.briar.android.ViewActions.waitFor;
import static org.hamcrest.CoreMatchers.allOf;
/**
* This relies on class sorting to run after {@link SignInTestCreateAccount}.
*/
@RunWith(AndroidJUnit4.class)
public class SignInTestSignIn extends UiTest {
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
}
@Test
public void signIn() throws Exception {
startActivity(SplashScreenActivity.class);
waitFor(StartupActivity.class);
// enter password
onView(withId(R.id.edit_password))
.check(matches(isDisplayed()))
.perform(replaceText(PASSWORD));
onView(withId(R.id.btn_sign_in))
.check(matches(allOf(isDisplayed(), isEnabled())))
.perform(click());
lifecycleManager.waitForStartup();
waitFor(NavDrawerActivity.class);
// ensure nav drawer is visible
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START)));
}
}

View File

@@ -10,12 +10,15 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import androidx.test.filters.LargeTest;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled; import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
@LargeTest
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class PngSuiteImageCompressorTest public class PngSuiteImageCompressorTest
extends AbstractImageCompressorTest { extends AbstractImageCompressorTest {

View File

@@ -12,11 +12,14 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import androidx.test.filters.LargeTest;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled; import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
@LargeTest
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class PngSuiteImageSizeCalculatorTest public class PngSuiteImageSizeCalculatorTest
extends AbstractImageSizeCalculatorTest { extends AbstractImageSizeCalculatorTest {

View File

@@ -1,39 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.Intent;
import org.briarproject.briar.R;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@RunWith(AndroidJUnit4.class)
public class ConversationActivityNotSignedInTest {
@Rule
public ActivityTestRule<ConversationActivity> testRule =
new ActivityTestRule<>(ConversationActivity.class, false, false);
@Test
public void openWithoutSignedIn() {
Context targetContext = getInstrumentation().getTargetContext();
Intent intent = new Intent(targetContext, ConversationActivity.class);
intent.putExtra(CONTACT_ID, 1);
testRule.launchActivity(intent);
onView(withText(R.string.sign_in_button))
.perform(waitUntilMatches(isDisplayed()));
}
}

View File

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

View File

@@ -0,0 +1,41 @@
package org.briarproject.briar.android;
import android.view.View;
import org.hamcrest.Matcher;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import static androidx.test.espresso.action.GeneralLocation.VISIBLE_CENTER;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
public class OverlayTapViewAction implements ViewAction {
public static ViewAction visualClick(OverlayView overlayView) {
return new OverlayTapViewAction(overlayView);
}
private final OverlayView overlayView;
public OverlayTapViewAction(OverlayView overlayView) {
this.overlayView = overlayView;
}
@Override
public Matcher<View> getConstraints() {
return isDisplayingAtLeast(90);
}
@Override
public String getDescription() {
return null;
}
@Override
public void perform(UiController uiController, View view) {
float[] coordinates = VISIBLE_CENTER.calculateCoordinates(view);
overlayView.tap(coordinates);
}
}

View File

@@ -0,0 +1,86 @@
package org.briarproject.briar.android;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.view.View;
import android.view.WindowManager;
import java.util.Random;
import javax.annotation.Nullable;
import static android.content.Context.WINDOW_SERVICE;
import static android.provider.Settings.canDrawOverlays;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
import static org.junit.Assert.assertTrue;
/**
* A full-screen overlay used to make taps visible in instrumentation tests.
*/
public class OverlayView extends View {
public static OverlayView attach(Context ctx) throws Throwable {
assertTrue(canDrawOverlays(ctx));
OverlayView view = new OverlayView(getApplicationContext());
runOnUiThread(() -> attachInternal(ctx, view));
return view;
}
private static void attachInternal(Context ctx, OverlayView view) {
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
WindowManager wm = (WindowManager) ctx.getSystemService(WINDOW_SERVICE);
wm.addView(view, params);
}
private final Random random = new Random();
private final Paint paint;
private final int yOffset;
@Nullable
private float[] coordinates;
public OverlayView(Context ctx) {
super(ctx);
int resourceId = getResources()
.getIdentifier("status_bar_height", "dimen", "android");
yOffset = getResources().getDimensionPixelSize(resourceId);
paint = new Paint();
paint.setAntiAlias(true);
paint.setARGB(175, 255, 0, 0);
setWillNotDraw(false);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
void tap(float[] coordinates) {
this.coordinates = coordinates;
invalidate();
new Handler().postDelayed(this::untap, 750);
}
private void untap() {
this.coordinates = null;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (coordinates == null) return;
float x = coordinates[0] + random.nextInt(42);
float y = coordinates[1] - yOffset + random.nextInt(13);
canvas.drawCircle(x, y, 42, paint);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,10 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"
tools:ignore="ScopedStorage" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
@@ -38,6 +41,7 @@
android:name="org.briarproject.briar.android.BriarApplicationImpl" android:name="org.briarproject.briar.android.BriarApplicationImpl"
android:allowBackup="false" android:allowBackup="false"
android:banner="@mipmap/tv_banner" android:banner="@mipmap/tv_banner"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher_round" android:icon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"
android:logo="@mipmap/ic_launcher_round" android:logo="@mipmap/ic_launcher_round"
@@ -338,7 +342,7 @@
</activity> </activity>
<activity <activity
android:name="org.briarproject.briar.android.keyagreement.ContactExchangeActivity" android:name="org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity"
android:label="@string/add_contact_title" android:label="@string/add_contact_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"> android:theme="@style/BriarTheme.NoActionBar">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
package org.briarproject.briar.android.account; package org.briarproject.briar.android.account;
import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.TextView; import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
@@ -15,12 +13,12 @@ import android.widget.TextView.OnEditorActionListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@@ -31,44 +29,21 @@ import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
abstract class SetupFragment extends BaseFragment implements TextWatcher, public abstract class SetupFragment extends BaseFragment implements TextWatcher,
OnEditorActionListener, OnClickListener { OnEditorActionListener, OnClickListener {
private final static String STATE_KEY_CLICKED = "setupFragmentClicked";
private boolean clicked = false;
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
SetupViewModel viewModel; SetupViewModel viewModel;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { @CallSuper
super.onCreate(savedInstanceState); public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity()) viewModel = new ViewModelProvider(requireActivity())
.get(SetupViewModel.class); .get(SetupViewModel.class);
} }
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
clicked = savedInstanceState.getBoolean(STATE_KEY_CLICKED);
}
if (clicked) {
setNextClicked();
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_KEY_CLICKED, clicked);
}
@CallSuper
void setNextClicked() {
this.clicked = true;
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.help_action, menu); inflater.inflate(R.menu.help_action, menu);
@@ -114,5 +89,4 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
public void afterTextChanged(Editable editable) { public void afterTextChanged(Editable editable) {
// noop // noop
} }
} }

View File

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

View File

@@ -51,6 +51,7 @@ public class UnlockActivity extends BaseActivity {
component.inject(this); component.inject(this);
} }
@Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
overridePendingTransition(0, 0); overridePendingTransition(0, 0);

View File

@@ -4,28 +4,29 @@ import android.app.Activity;
import org.briarproject.briar.android.AndroidComponent; import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.StartupFailureActivity; import org.briarproject.briar.android.StartupFailureActivity;
import org.briarproject.briar.android.account.AuthorNameFragment;
import org.briarproject.briar.android.account.DozeFragment;
import org.briarproject.briar.android.account.SetPasswordFragment;
import org.briarproject.briar.android.account.SetupActivity; import org.briarproject.briar.android.account.SetupActivity;
import org.briarproject.briar.android.account.SetupFragment;
import org.briarproject.briar.android.account.UnlockActivity; import org.briarproject.briar.android.account.UnlockActivity;
import org.briarproject.briar.android.blog.BlogActivity; import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.blog.BlogFragment; import org.briarproject.briar.android.blog.BlogFragment;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.blog.BlogPostFragment; import org.briarproject.briar.android.blog.BlogPostFragment;
import org.briarproject.briar.android.blog.FeedFragment; import org.briarproject.briar.android.blog.FeedFragment;
import org.briarproject.briar.android.blog.FeedPostFragment;
import org.briarproject.briar.android.blog.ReblogActivity; import org.briarproject.briar.android.blog.ReblogActivity;
import org.briarproject.briar.android.blog.ReblogFragment; import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity; import org.briarproject.briar.android.blog.RssFeedImportActivity;
import org.briarproject.briar.android.blog.RssFeedManageActivity; import org.briarproject.briar.android.blog.RssFeedManageActivity;
import org.briarproject.briar.android.blog.WriteBlogPostActivity; import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.contact.ContactListFragment; import org.briarproject.briar.android.contact.ContactListFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactErrorFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactIntroFragment;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity; import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment; import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment;
import org.briarproject.briar.android.contact.add.remote.NicknameFragment; import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity; import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.conversation.AliasDialogFragment; import org.briarproject.briar.android.conversation.AliasDialogFragment;
import org.briarproject.briar.android.conversation.BluetoothConnecterDialogFragment;
import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.conversation.ConversationSettingsDialog; import org.briarproject.briar.android.conversation.ConversationSettingsDialog;
import org.briarproject.briar.android.conversation.ImageActivity; import org.briarproject.briar.android.conversation.ImageActivity;
@@ -37,10 +38,6 @@ import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment; import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
import org.briarproject.briar.android.keyagreement.ContactExchangeErrorFragment;
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
import org.briarproject.briar.android.login.ChangePasswordActivity; import org.briarproject.briar.android.login.ChangePasswordActivity;
import org.briarproject.briar.android.login.OpenDatabaseFragment; import org.briarproject.briar.android.login.OpenDatabaseFragment;
import org.briarproject.briar.android.login.PasswordFragment; import org.briarproject.briar.android.login.PasswordFragment;
@@ -86,7 +83,6 @@ import dagger.Component;
@ActivityScope @ActivityScope
@Component(modules = { @Component(modules = {
ActivityModule.class, ActivityModule.class,
BlogModule.class,
CreateGroupModule.class, CreateGroupModule.class,
GroupInvitationModule.class, GroupInvitationModule.class,
GroupMemberModule.class, GroupMemberModule.class,
@@ -109,9 +105,7 @@ public interface ActivityComponent {
void inject(PanicPreferencesActivity activity); void inject(PanicPreferencesActivity activity);
void inject(ContactExchangeActivity activity); void inject(AddNearbyContactActivity activity);
void inject(KeyAgreementActivity activity);
void inject(ConversationActivity activity); void inject(ConversationActivity activity);
@@ -153,8 +147,6 @@ public interface ActivityComponent {
void inject(BlogPostFragment fragment); void inject(BlogPostFragment fragment);
void inject(FeedPostFragment fragment);
void inject(ReblogFragment fragment); void inject(ReblogFragment fragment);
void inject(ReblogActivity activity); void inject(ReblogActivity activity);
@@ -185,11 +177,7 @@ public interface ActivityComponent {
// Fragments // Fragments
void inject(AuthorNameFragment fragment); void inject(SetupFragment fragment);
void inject(SetPasswordFragment fragment);
void inject(DozeFragment fragment);
void inject(PasswordFragment imageFragment); void inject(PasswordFragment imageFragment);
@@ -209,7 +197,9 @@ public interface ActivityComponent {
void inject(FeedFragment fragment); void inject(FeedFragment fragment);
void inject(KeyAgreementFragment fragment); void inject(AddNearbyContactIntroFragment fragment);
void inject(AddNearbyContactFragment fragment);
void inject(LinkExchangeFragment fragment); void inject(LinkExchangeFragment fragment);
@@ -227,7 +217,7 @@ public interface ActivityComponent {
void inject(ScreenFilterDialogFragment fragment); void inject(ScreenFilterDialogFragment fragment);
void inject(ContactExchangeErrorFragment fragment); void inject(AddNearbyContactErrorFragment fragment);
void inject(AliasDialogFragment aliasDialogFragment); void inject(AliasDialogFragment aliasDialogFragment);
@@ -241,4 +231,6 @@ public interface ActivityComponent {
void inject(ConversationSettingsDialog dialog); void inject(ConversationSettingsDialog dialog);
void inject(
BluetoothConnecterDialogFragment bluetoothConnecterDialogFragment);
} }

View File

@@ -193,6 +193,9 @@ public abstract class BaseActivity extends AppCompatActivity
} }
private boolean showScreenFilterWarning() { private boolean showScreenFilterWarning() {
if (((BriarApplication) getApplication()).isInstrumentationTest()) {
return false;
}
// If the dialog is already visible, filter the tap // If the dialog is already visible, filter the tap
ScreenFilterDialogFragment f = findDialogFragment(); ScreenFilterDialogFragment f = findDialogFragment();
if (f != null && f.isVisible()) return false; if (f != null && f.isVisible()) return false;

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidWakeLockManager; import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Wakeful; import org.briarproject.bramble.api.system.Wakeful;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.account.UnlockActivity; import org.briarproject.briar.android.account.UnlockActivity;
import org.briarproject.briar.android.controller.BriarController; import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController; import org.briarproject.briar.android.controller.DbController;
@@ -236,7 +237,8 @@ public abstract class BriarActivity extends BaseActivity {
if (SDK_INT >= 21) finishAndRemoveTask(); if (SDK_INT >= 21) finishAndRemoveTask();
else supportFinishAfterTransition(); else supportFinishAfterTransition();
LOG.info("Exiting"); LOG.info("Exiting");
System.exit(0); BriarApplication app = (BriarApplication) getApplication();
if (!app.isInstrumentationTest()) System.exit(0);
} }
@Deprecated @Deprecated

View File

@@ -6,12 +6,9 @@ public interface RequestCodes {
int REQUEST_INTRODUCTION = 2; int REQUEST_INTRODUCTION = 2;
int REQUEST_GROUP_INVITE = 3; int REQUEST_GROUP_INVITE = 3;
int REQUEST_SHARE_FORUM = 4; int REQUEST_SHARE_FORUM = 4;
int REQUEST_WRITE_BLOG_POST = 5;
int REQUEST_SHARE_BLOG = 6; int REQUEST_SHARE_BLOG = 6;
int REQUEST_RINGTONE = 7; int REQUEST_RINGTONE = 7;
int REQUEST_PERMISSION_CAMERA_LOCATION = 8;
int REQUEST_DOZE_WHITELISTING = 9; int REQUEST_DOZE_WHITELISTING = 9;
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
int REQUEST_UNLOCK = 11; int REQUEST_UNLOCK = 11;
int REQUEST_KEYGUARD_UNLOCK = 12; int REQUEST_KEYGUARD_UNLOCK = 12;
int REQUEST_ATTACH_IMAGE = 13; int REQUEST_ATTACH_IMAGE = 13;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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