Compare commits

...

115 Commits

Author SHA1 Message Date
akwizgran
7f8e96a654 Bump version numbers for beta release. 2018-03-07 17:10:28 +00:00
akwizgran
84e040605b Don't reuse the same ConnectionChooser every time.
This is a fix for a backporting mistake.
2018-03-07 16:47:08 +00:00
akwizgran
b0aa1517e5 Merge branch '283-key-exchange-connections' into 'maintenance-0.16'
Backport: Refactor key agreement connection choosing

See merge request akwizgran/briar!725
2018-03-07 14:06:09 +00:00
akwizgran
2ac9f567dc Merge branch '1164-store-bluetooth-properties' into 'maintenance-0.16'
Backport: Store Bluetooth address and UUID at first startup

See merge request akwizgran/briar!724
2018-03-07 14:05:08 +00:00
akwizgran
792cfd7d6f Merge branch '790-ask-before-turning-on-bluetooth' into 'maintenance-0.16'
Backport: Ask before turning on Bluetooth to add a contact

See merge request akwizgran/briar!723
2018-03-07 14:04:09 +00:00
Torsten Grote
2112d4fa7d Backport: Update translations 2018-03-07 09:57:42 -03:00
akwizgran
31ca04e070 Merge branch '1001-bluetooth-connects-to-contacts' into 'maintenance-0.16'
Backport: Don't make Bluetooth connections when configured not to

See merge request akwizgran/briar!722
2018-03-07 12:36:17 +00:00
akwizgran
9693a5cb93 Refactor key agreement connection choosing. 2018-03-07 12:27:05 +00:00
akwizgran
82266345ae Merge branch 'bluetooth-refactoring' into 'maintenance-0.16'
Backport: Factor shared Bluetooth code into superclass

See merge request akwizgran/briar!721
2018-03-07 12:24:38 +00:00
akwizgran
0942fe6053 Merge branch 'transport-indicators-no-buttons' into 'maintenance-0.16'
Backport: Prevent transport indicators from looking like buttons

See merge request akwizgran/briar!720
2018-03-07 12:16:16 +00:00
akwizgran
4a1f58705d Address review comments. 2018-03-07 12:10:31 +00:00
akwizgran
cfe0d9a656 Don't set running = true until properties have been loaded. 2018-03-07 12:10:31 +00:00
akwizgran
3cf61e7b3d Store Bluetooth address and UUID at first startup. 2018-03-07 12:10:31 +00:00
akwizgran
7bb7f8ad5b Fix import of wrong Immutable annotation. 2018-03-07 12:09:37 +00:00
akwizgran
fc50bb1c6c Ask before turning on Bluetooth to add a contact. 2018-03-07 12:09:37 +00:00
akwizgran
19be4d6edf Remove unnecessary executor calls. 2018-03-07 12:08:56 +00:00
akwizgran
b2e4de91a4 Don't make Bluetooth connections when configured not to. 2018-03-07 12:08:56 +00:00
akwizgran
9b184fe1d9 Merge branch '1174-link-click-crash' into 'maintenance-0.16'
Backport: Get unwrapped context when clicking links to prevent crash on Android 4

See merge request akwizgran/briar!719
2018-03-07 12:00:27 +00:00
akwizgran
f4ddc01641 Factor shared Bluetooth code into superclass. 2018-03-07 11:56:52 +00:00
akwizgran
08b63201d9 Merge branch 'fix-intro-fragment' into 'maintenance-0.16'
Backport: Fix uncentered intro fragment

See merge request akwizgran/briar!718
2018-03-07 11:52:22 +00:00
Torsten Grote
1c41181f1c Prevent transport indicators from looking like buttons 2018-03-07 11:50:18 +00:00
Torsten Grote
246b330b36 Passing in reference to FragmentManager when clicking links to prevent crash on Android 4 2018-03-07 11:39:24 +00:00
akwizgran
fd3e74cefc Merge branch '1168-startup-status-screen' into 'maintenance-0.16'
Backport: Show status message while opening and migrating DB

See merge request akwizgran/briar!717
2018-03-07 11:31:24 +00:00
goapunk
ef12191ec8 fix uncentered intro fragment
Signed-off-by: goapunk <noobie@goapunks.net>
2018-03-07 11:25:18 +00:00
akwizgran
a9fc310762 Merge branch '1176-startup-failure-crash' into 'maintenance-0.16'
Backport: Inject StartupFailureActivity to prevent NPE

See merge request akwizgran/briar!716
2018-03-07 11:14:49 +00:00
akwizgran
0a70c2d44d Add more lifecycle states, merge lifecycle events. 2018-03-07 11:07:28 +00:00
Torsten Grote
af1fc6f095 Start NavDrawerActivity only after database was opened and services started 2018-03-07 11:07:27 +00:00
Torsten Grote
21956f2627 Show a status screen when opening the database or applying migrations 2018-03-07 11:07:24 +00:00
akwizgran
55db6e524a Merge branch '346-qr-code-optimisations' into 'maintenance-0.16'
Backport: Improve QR code scanning on phones with high res cameras and slow CPUs

See merge request akwizgran/briar!715
2018-03-07 11:06:56 +00:00
Torsten Grote
dac3de24e7 Do not show splash screen when signed in 2018-03-07 11:05:17 +00:00
akwizgran
f93f41893e Inject StartupFailureActivity to prevent NPE. 2018-03-07 11:00:31 +00:00
akwizgran
7dacb43e01 Don't stop camera view when QR code is scanned. 2018-03-07 10:54:27 +00:00
akwizgran
6a962bad24 Use ConstraintLayout for intro fragment. 2018-03-07 10:47:37 +00:00
akwizgran
489c0154e9 Add javadoc links. 2018-03-07 10:47:37 +00:00
akwizgran
85dc99da72 Crop camera preview before looking for QR code. 2018-03-07 10:47:35 +00:00
akwizgran
ec808fd9f7 Add landscape layout for QR code fragment. 2018-03-07 10:45:49 +00:00
Torsten Grote
4c661cd4bb Merge branch '1154-fix-notification-light' into 'maintenance-0.16'
Backport: Fix notification light

See merge request akwizgran/briar!713
2018-03-06 18:17:46 +00:00
Torsten Grote
6324fb72a5 Fix notification light 2018-03-06 15:04:50 -03:00
akwizgran
d3aebc4aba Merge branch '1136-startup-failure-ux' into 'maintenance-0.16'
Backport: Improve UX for startup failures

See merge request akwizgran/briar!707
2018-02-28 10:26:48 +00:00
Torsten Grote
65c0e110c5 Improve UX for startup failures
Show a proper error message when database is too new or too old.
2018-02-26 14:49:01 -03:00
Torsten Grote
67aeb40d34 Backport ErrorFragment 2018-02-26 14:49:00 -03:00
akwizgran
8280b2e3b8 Inject StartupFailureActivity to prevent NPE. 2018-02-26 14:49:00 -03:00
akwizgran
4e0b9145c1 Merge branch '542-retransmission' into 'maintenance-0.16'
Backport: Don't poll for retransmission

See merge request akwizgran/briar!703
2018-02-22 12:45:48 +00:00
akwizgran
0ad4f2f39b Don't poll for retransmission. 2018-02-22 12:36:33 +00:00
akwizgran
812522a900 Bump version numbers for beta release. 2018-02-19 16:40:47 +00:00
akwizgran
98db9da4bc Merge branch '509-tap-viewfinder-to-auto-focus' into 'maintenance-0.16'
Backport: Tap viewfinder to restart auto focus

See merge request akwizgran/briar!701
2018-02-19 16:20:16 +00:00
akwizgran
eda3c964aa Merge branch '1137-stop-polling-disabled-plugins' into 'maintenance-0.16'
Backport: Don't poll disabled transport plugins

See merge request akwizgran/briar!700
2018-02-19 16:03:15 +00:00
akwizgran
68df606146 Tap viewfinder to restart auto focus. 2018-02-19 15:58:20 +00:00
akwizgran
52bd699d2d Don't poll disabled transport plugins. 2018-02-19 15:53:43 +00:00
Torsten Grote
abb8db10db Merge branch 'migration-30-31' into 'maintenance-0.16'
Beta: Migrate DB schema from version 30 to 31

See merge request akwizgran/briar!690
2018-02-18 17:58:48 +00:00
akwizgran
30edb90426 Add migration from schema 30 to 31. 2018-02-02 17:01:49 +00:00
akwizgran
ffc94b2812 Merge branch '545-remove-unnecessary-indexes' into 'maintenance-0.16'
Backport: Remove unnecessary DB indexes

See merge request akwizgran/briar!692
2018-02-02 17:00:00 +00:00
akwizgran
35a7bb4576 Merge branch '594-db-migrations' into 'maintenance-0.16'
Backport: Migrate schema when opening database

See merge request akwizgran/briar!689
2018-02-02 15:46:39 +00:00
akwizgran
2d87e34aa2 Throw meaningful exceptions for schema errors. 2018-02-02 15:34:49 +00:00
akwizgran
088564f22f Add comment. 2018-02-02 15:34:25 +00:00
akwizgran
8c8c1158f4 Apply more than one migration if suitable. 2018-02-02 15:34:09 +00:00
akwizgran
8faa456eb2 Add unit tests for migration logic. 2018-02-02 15:32:20 +00:00
akwizgran
4c61158326 Migrate database schema if a migration is available. 2018-02-02 15:31:58 +00:00
akwizgran
6792abc00a Remove unnecessary DB indexes. 2018-02-01 17:44:22 +00:00
Torsten Grote
63442aea1d Merge branch '1162-redundant-db-tasks' into 'maintenance-0.16'
Backport: Avoid queueing redundant DB tasks during sync

See merge request akwizgran/briar!685
2018-02-01 16:17:11 +00:00
akwizgran
a58443eaa8 Merge branch '1148-wrong-network-interface' into 'maintenance-0.16'
Backport: Prefer LAN addresses with longer prefixes

See merge request akwizgran/briar!684
2018-02-01 15:48:53 +00:00
akwizgran
14a9614c35 Avoid queueing redundant DB tasks during sync. 2018-02-01 15:48:15 +00:00
akwizgran
f1011b97b3 Merge branch '1143-screen-overlay-dialog' into 'maintenance-0.16'
Backport: Don't show screen overlay dialog if all overlay apps have been allowed

See merge request akwizgran/briar!683
2018-02-01 15:41:55 +00:00
akwizgran
1935b1e09a Add tests for link-local addresses. 2018-02-01 15:40:23 +00:00
akwizgran
ac9df9d5d8 Prefer LAN addresses with longer prefixes. 2018-02-01 15:40:23 +00:00
akwizgran
30a800a4d0 Remove unused argument. 2018-02-01 15:34:16 +00:00
akwizgran
69537b67a2 Simplify dialog handling, work around Android bug. 2018-02-01 15:34:16 +00:00
akwizgran
92982f98a8 Update screen overlay warning text. 2018-02-01 15:34:16 +00:00
akwizgran
ea5fa72224 Re-show dialog when activity resumes or is recreated. 2018-02-01 15:34:16 +00:00
akwizgran
5a1651d483 Set layout weight so checkbox is visible. 2018-02-01 15:34:16 +00:00
akwizgran
fcbf6dfb7f Cache the list of overlay apps. 2018-02-01 15:34:15 +00:00
akwizgran
7aebf92a6f Allow filtered taps if all overlay apps are whitelisted. 2018-02-01 15:34:10 +00:00
akwizgran
1b9f8d4f0b Merge branch '1116-samsung-back-crash' into 'maintenance-0.16'
Backport: Workaround for Samsung crash in Android 4.4

See merge request akwizgran/briar!682
2018-02-01 11:00:28 +00:00
Torsten Grote
93db4eb986 Workaround for Samsung crash in Android 4.4
Closes #1116
2018-02-01 10:41:48 +00:00
akwizgran
347c2f22c1 Bump version numbers for beta release. 2018-01-29 16:48:21 +00:00
Torsten Grote
a8ea191ffb Merge branch '1007-samsung-transition-npe-fix' into 'maintenance-0.16'
Backport: Another attempt at fixing an infamous Samsung activity transition NPE

See merge request akwizgran/briar!678
2018-01-29 14:53:46 +00:00
Torsten Grote
2a4c22757b Another attempt at fixing an infamous Samsung activity transition NPE 2018-01-29 12:36:21 -02:00
Torsten Grote
28ebbbc7d1 Backport translation updates
New translations: br, nl, he, sv, cs, ja
2018-01-29 10:45:12 -02:00
akwizgran
5e7d08f05d Merge branch 'change-password-activity' into 'maintenance-0.16'
Backport: ChangePasswordActivity should extend BriarActivity

See merge request akwizgran/briar!673
2018-01-23 17:36:18 +00:00
akwizgran
ea005748dc Merge branch 'tor-plugin-detect-connectivity-loss' into 'maintenance-0.16'
Backport: Tor plugin should detect connectivity loss

See merge request akwizgran/briar!672
2018-01-23 17:29:28 +00:00
akwizgran
b021bfab5e ChangePasswordActivity should extend BriarActivity. 2018-01-23 17:22:43 +00:00
akwizgran
29cd105a1d Use scheduler service to schedule connectivity checks. 2018-01-23 17:16:59 +00:00
akwizgran
be2e68e96c Listen for a wider range of connectivity-related events. 2018-01-23 17:15:53 +00:00
akwizgran
9dd3f81bb7 Use Tor's OR connection events to detect lost connectivity. 2018-01-23 17:15:53 +00:00
akwizgran
5d918591d4 Merge branch '1145-avoid-unnecessary-db-queries' into 'maintenance-0.16'
Backport: Avoid unnecessary DB queries when starting clients

See merge request akwizgran/briar!669
2018-01-16 15:33:14 +00:00
akwizgran
f1c027fa4d Avoid unnecessary DB queries when starting clients. 2018-01-16 15:23:31 +00:00
akwizgran
d2d3ccf68d Merge branch 'prefer-project-modules' into 'maintenance-0.16'
Backport: Prefer project modules over prebuilt dependencies

See merge request akwizgran/briar!668
2018-01-12 17:55:05 +00:00
akwizgran
f4efed54d5 Prefer project modules over prebuilt dependencies. 2018-01-12 17:35:59 +00:00
akwizgran
459538e40c Bump version numbers for beta release. 2017-12-22 14:43:03 +00:00
akwizgran
183f501761 Merge branch '1132-upgrade-tor-0.2.9.14' into 'maintenance-0.16'
Beta: Upgrade Tor to 0.2.9.14, GeoIP to 2017-11-06

See merge request akwizgran/briar!657
2017-12-22 14:10:52 +00:00
akwizgran
65ee5f539b Upgrade Tor to 0.2.9.14, GeoIP to 2017-11-06. 2017-12-22 13:52:45 +00:00
akwizgran
604339326c Merge branch '1129-send-on-ctrl-enter' into 'maintenance-0.16'
Beta: Send message on ctrl + enter

See merge request akwizgran/briar!656
2017-12-22 11:49:55 +00:00
sbkaf
0acec1343f send message on ctrl + enter 2017-12-22 11:32:15 +00:00
akwizgran
0434756bbd Merge branch '1133-extend-expiry-period' into 'maintenance-0.16'
Extend expiry date, show extension notification

See merge request akwizgran/briar!655
2017-12-22 11:23:40 +00:00
akwizgran
e233433140 Extend expiry date, show extension notification. 2017-12-22 10:58:11 +00:00
akwizgran
c63f285f53 Bumped version numbers for beta release. 2017-12-07 14:13:11 +00:00
akwizgran
0800188718 Merge branch '1112-screen-filter-crash' into 'maintenance-0.16'
Beta: Don't show screen filter dialog after onSaveInstanceState().

See merge request !650
2017-12-07 13:29:27 +00:00
akwizgran
6188e48beb Don't show screen filter dialog after onSaveInstanceState(). 2017-12-07 13:07:07 +00:00
akwizgran
5726e29b56 Merge branch '1088-huawei-whitelisting' into 'maintenance-0.16'
Beta: Add button for Huawei's power manager to setup wizard

See merge request !648
2017-12-07 13:05:34 +00:00
Torsten Grote
5d70399de0 Add button for Huawei's power manager to setup wizard 2017-12-05 15:26:14 -02:00
akwizgran
73202dde5e Merge branch '1127-notification-channels' into 'maintenance-0.16'
Beta: Use channels for all notifications

See merge request !647
2017-12-05 17:03:37 +00:00
akwizgran
a98ac8233c Sort order of channel IDs affects UI of Settings app. 2017-12-05 16:49:31 +00:00
akwizgran
bee3e244fc Use channels for all notifications. 2017-12-05 16:49:31 +00:00
akwizgran
da25999a15 Merge branch '1120-crash-removing-shutdown-hook' into 'maintenance-0.16'
Beta: Don't remove shutdown hook when closing DB

See merge request !645
2017-12-05 14:58:56 +00:00
akwizgran
62049df342 Don't remove shutdown hook when closing DB. 2017-12-05 14:46:07 +00:00
akwizgran
024e5aa90f Bumped version numbers for beta release. 2017-12-04 14:43:27 +00:00
akwizgran
6d791481d5 Merge branch '1007-samsung-transition-npe-beta' into 'maintenance-0.16'
Beta: Don't set scene transition for Samsung devices running Android 7.0

See merge request !641
2017-12-04 14:35:39 +00:00
Torsten Grote
0a807d0893 Don't set scene transition for Samsung devices running Android 7.0 2017-12-04 10:58:20 -02:00
akwizgran
23596bbdd4 Merge branch origin/maintenance-0.16 into maintenance-0.16 2017-12-01 17:19:42 +00:00
Torsten Grote
fe79954138 Merge branch 'briar-beta-app-name' into 'maintenance-0.16'
Change app name for beta debug builds

See merge request !636
2017-12-01 16:43:45 +00:00
akwizgran
9902c023ca Bump version number for beta release. 2017-12-01 16:30:18 +00:00
akwizgran
e8baee6734 Specify 7 characters for Git revision.
(cherry picked from commit f0d8532)
2017-12-01 16:29:45 +00:00
akwizgran
a8dc029e56 Change app name for beta debug builds. 2017-12-01 16:21:20 +00:00
akwizgran
74e3fee7aa Merge branch '1124-notification-channel-crash-beta' into 'maintenance-0.16'
Beta: Use NotificationChannel for foreground service to avoid crash on Android 8.1

See merge request !635
2017-12-01 16:00:53 +00:00
Torsten Grote
05aac696b7 Use NotificationChannel for foreground service to avoid crash on Android 8.1
This also seems to address #1075 at least on an emulator
2017-12-01 13:47:02 -02:00
161 changed files with 6562 additions and 2403 deletions

View File

@@ -12,8 +12,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 26 targetSdkVersion 26
versionCode 1612 versionCode 1619
versionName "0.16.12" versionName "0.16.19"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
@@ -54,16 +54,16 @@ dependencyVerification {
} }
ext.torBinaryDir = 'src/main/res/raw' ext.torBinaryDir = 'src/main/res/raw'
ext.torVersion = '0.2.9.12' ext.torVersion = '0.2.9.14'
ext.geoipVersion = '2017-09-06' ext.geoipVersion = '2017-11-06'
ext.torDownloadUrl = 'https://briarproject.org/build/' ext.torDownloadUrl = 'https://briarproject.org/build/'
def torBinaries = [ def torBinaries = [
"tor_arm" : '8ed0b347ffed1d6a4d2fd14495118eb92be83e9cc06e057e15220dc288b31688', "tor_arm" : '1710ea6c47b7f4c1a88bdf4858c7893837635db10e8866854eed8d61629f50e8',
"tor_arm_pie": '64403262511c29f462ca5e7c7621bfc3c944898364d1d5ad35a016bb8a034283', "tor_arm_pie": '974e6949507db8fa2ea45231817c2c3677ed4ccf5488a2252317d744b0be1917',
"tor_x86" : '61e014607a2079bcf1646289c67bff6372b1aded6e1d8d83d7791efda9a4d5ab', "tor_x86" : '3a5e45b3f051fcda9353b098b7086e762ffe7ba9242f7d7c8bf6523faaa8b1e9',
"tor_x86_pie": '18fbc98356697dd0895836ab46d5c9877d1c539193464f7db1e82a65adaaf288', "tor_x86_pie": 'd1d96d8ce1a4b68accf04850185780d10cd5563d3552f7e1f040f8ca32cb4e51',
"geoip" : 'fe49d3adb86d3c512373101422a017dbb86c85a570524663f09dd8ce143a24f3' "geoip" : '8239b98374493529a29096e45fc5877d4d6fdad0146ad8380b291f90d61484ea'
] ]
def downloadBinary(name) { def downloadBinary(name) {

View File

@@ -13,7 +13,8 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reporting.DevReporter; import org.briarproject.bramble.api.reporting.DevReporter;
import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.droidtooth.DroidtoothPluginFactory; import org.briarproject.bramble.api.system.Scheduler;
import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory;
import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory; import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.bramble.plugin.tor.TorPluginFactory; import org.briarproject.bramble.plugin.tor.TorPluginFactory;
@@ -22,6 +23,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.net.SocketFactory; import javax.net.SocketFactory;
@@ -33,16 +35,18 @@ public class AndroidPluginModule {
@Provides @Provides
PluginConfig providePluginConfig(@IoExecutor Executor ioExecutor, PluginConfig providePluginConfig(@IoExecutor Executor ioExecutor,
@Scheduler ScheduledExecutorService scheduler,
AndroidExecutor androidExecutor, SecureRandom random, AndroidExecutor androidExecutor, SecureRandom random,
SocketFactory torSocketFactory, BackoffFactory backoffFactory, SocketFactory torSocketFactory, BackoffFactory backoffFactory,
Application app, LocationUtils locationUtils, DevReporter reporter, Application app, LocationUtils locationUtils, DevReporter reporter,
EventBus eventBus) { EventBus eventBus) {
Context appContext = app.getApplicationContext(); Context appContext = app.getApplicationContext();
DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor, DuplexPluginFactory bluetooth =
androidExecutor, appContext, random, eventBus, backoffFactory); new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor,
DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext, appContext, random, eventBus, backoffFactory);
locationUtils, reporter, eventBus, torSocketFactory, DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, scheduler,
backoffFactory); appContext, locationUtils, reporter, eventBus,
torSocketFactory, backoffFactory);
DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor, DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
backoffFactory, appContext); backoffFactory, appContext);
Collection<DuplexPluginFactory> duplex = Collection<DuplexPluginFactory> duplex =

View File

@@ -0,0 +1,206 @@
package org.briarproject.bramble.plugin.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.AndroidUtils;
import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static java.util.logging.Level.WARNING;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
private static final Logger LOG =
Logger.getLogger(AndroidBluetoothPlugin.class.getName());
private final AndroidExecutor androidExecutor;
private final Context appContext;
private volatile boolean wasEnabledByUs = false;
private volatile BluetoothStateReceiver receiver = null;
// Non-null if the plugin started successfully
private volatile BluetoothAdapter adapter = null;
AndroidBluetoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Backoff backoff,
DuplexPluginCallback callback, int maxLatency) {
super(ioExecutor, secureRandom, backoff, callback, maxLatency);
this.androidExecutor = androidExecutor;
this.appContext = appContext;
}
@Override
public void start() throws PluginException {
super.start();
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
filter.addAction(ACTION_SCAN_MODE_CHANGED);
receiver = new BluetoothStateReceiver();
appContext.registerReceiver(receiver, filter);
}
@Override
public void stop() {
super.stop();
if (receiver != null) appContext.unregisterReceiver(receiver);
}
@Override
void initialiseAdapter() throws IOException {
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
// with a message queue, so submit it to the AndroidExecutor
try {
adapter = androidExecutor.runOnBackgroundThread(
BluetoothAdapter::getDefaultAdapter).get();
} catch (InterruptedException | ExecutionException e) {
throw new IOException(e);
}
if (adapter == null)
throw new IOException("Bluetooth is not supported");
}
@Override
boolean isAdapterEnabled() {
return adapter != null && adapter.isEnabled();
}
@Override
void enableAdapter() {
if (adapter != null && !adapter.isEnabled()) {
if (adapter.enable()) {
LOG.info("Enabling Bluetooth");
wasEnabledByUs = true;
} else {
LOG.info("Could not enable Bluetooth");
}
}
}
@Override
void disableAdapterIfEnabledByUs() {
if (isAdapterEnabled() && wasEnabledByUs) {
if (adapter.disable()) LOG.info("Disabling Bluetooth");
else LOG.info("Could not disable Bluetooth");
wasEnabledByUs = false;
}
}
@Override
void setEnabledByUs() {
wasEnabledByUs = true;
}
@Override
@Nullable
String getBluetoothAddress() {
String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
return address.isEmpty() ? null : address;
}
@Override
BluetoothServerSocket openServerSocket(String uuid) throws IOException {
return adapter.listenUsingInsecureRfcommWithServiceRecord(
"RFCOMM", UUID.fromString(uuid));
}
@Override
void tryToClose(@Nullable BluetoothServerSocket ss) {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
DuplexTransportConnection acceptConnection(BluetoothServerSocket ss)
throws IOException {
return wrapSocket(ss.accept());
}
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
return new AndroidBluetoothTransportConnection(this, s);
}
@Override
boolean isValidAddress(String address) {
return BluetoothAdapter.checkBluetoothAddress(address);
}
@Override
DuplexTransportConnection connectTo(String address, String uuid)
throws IOException {
BluetoothDevice d = adapter.getRemoteDevice(address);
UUID u = UUID.fromString(uuid);
BluetoothSocket s = null;
try {
s = d.createInsecureRfcommSocketToServiceRecord(u);
s.connect();
return wrapSocket(s);
} catch (IOException e) {
tryToClose(s);
throw e;
}
}
private void tryToClose(@Nullable Closeable c) {
try {
if (c != null) c.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if (state == STATE_ON) onAdapterEnabled();
else if (state == STATE_OFF) onAdapterDisabled();
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
if (scanMode == SCAN_MODE_NONE) {
LOG.info("Scan mode: None");
} else if (scanMode == SCAN_MODE_CONNECTABLE) {
LOG.info("Scan mode: Connectable");
} else if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
LOG.info("Scan mode: Discoverable");
}
}
}
}

View File

@@ -1,4 +1,4 @@
package org.briarproject.bramble.plugin.droidtooth; package org.briarproject.bramble.plugin.bluetooth;
import android.content.Context; import android.content.Context;
@@ -21,7 +21,7 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class DroidtoothPluginFactory implements DuplexPluginFactory { public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
@@ -35,7 +35,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
private final EventBus eventBus; private final EventBus eventBus;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
public DroidtoothPluginFactory(Executor ioExecutor, public AndroidBluetoothPluginFactory(Executor ioExecutor,
AndroidExecutor androidExecutor, Context appContext, AndroidExecutor androidExecutor, Context appContext,
SecureRandom secureRandom, EventBus eventBus, SecureRandom secureRandom, EventBus eventBus,
BackoffFactory backoffFactory) { BackoffFactory backoffFactory) {
@@ -61,7 +61,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) { public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
DroidtoothPlugin plugin = new DroidtoothPlugin(ioExecutor, AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin(ioExecutor,
androidExecutor, appContext, secureRandom, backoff, callback, androidExecutor, appContext, secureRandom, backoff, callback,
MAX_LATENCY); MAX_LATENCY);
eventBus.addListener(plugin); eventBus.addListener(plugin);

View File

@@ -1,4 +1,4 @@
package org.briarproject.bramble.plugin.droidtooth; package org.briarproject.bramble.plugin.bluetooth;
import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothSocket;
@@ -11,11 +11,12 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@NotNullByDefault @NotNullByDefault
class DroidtoothTransportConnection extends AbstractDuplexTransportConnection { class AndroidBluetoothTransportConnection
extends AbstractDuplexTransportConnection {
private final BluetoothSocket socket; private final BluetoothSocket socket;
DroidtoothTransportConnection(Plugin plugin, BluetoothSocket socket) { AndroidBluetoothTransportConnection(Plugin plugin, BluetoothSocket socket) {
super(plugin); super(plugin);
this.socket = socket; this.socket = socket;
} }

View File

@@ -1,490 +0,0 @@
package org.briarproject.bramble.plugin.droidtooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.DisableBluetoothEvent;
import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils;
import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class DroidtoothPlugin implements DuplexPlugin, EventListener {
private static final Logger LOG =
Logger.getLogger(DroidtoothPlugin.class.getName());
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final DuplexPluginCallback callback;
private final int maxLatency;
private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false;
private volatile boolean wasEnabledByUs = false;
private volatile BluetoothStateReceiver receiver = null;
private volatile BluetoothServerSocket socket = null;
// Non-null if the plugin started successfully
private volatile BluetoothAdapter adapter = null;
DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Backoff backoff,
DuplexPluginCallback callback, int maxLatency) {
this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor;
this.appContext = appContext;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
// with a message queue, so submit it to the AndroidExecutor
try {
adapter = androidExecutor.runOnBackgroundThread(
BluetoothAdapter::getDefaultAdapter).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warning("Interrupted while getting BluetoothAdapter");
throw new PluginException(e);
} catch (ExecutionException e) {
throw new PluginException(e);
}
if (adapter == null) {
LOG.info("Bluetooth is not supported");
throw new PluginException();
}
running = true;
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
filter.addAction(ACTION_SCAN_MODE_CHANGED);
receiver = new BluetoothStateReceiver();
appContext.registerReceiver(receiver, filter);
// If Bluetooth is enabled, bind a socket
if (adapter.isEnabled()) {
bind();
} else {
// Enable Bluetooth if settings allow
if (callback.getSettings().getBoolean(PREF_BT_ENABLE, false)) {
enableAdapter();
} else {
LOG.info("Not enabling Bluetooth");
}
}
}
private void bind() {
ioExecutor.execute(() -> {
if (!isRunning()) return;
String address = AndroidUtils.getBluetoothAddress(appContext,
adapter);
if (LOG.isLoggable(INFO))
LOG.info("Local address " + scrubMacAddress(address));
if (!StringUtils.isNullOrEmpty(address)) {
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put(PROP_ADDRESS, address);
callback.mergeLocalProperties(p);
}
// Bind a server socket to accept connections from contacts
BluetoothServerSocket ss;
try {
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
"RFCOMM", getUuid());
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return;
}
if (!isRunning()) {
tryToClose(ss);
return;
}
LOG.info("Socket bound");
socket = ss;
backoff.reset();
callback.transportEnabled();
acceptContactConnections();
});
}
private UUID getUuid() {
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return UUID.fromString(uuid);
}
private void tryToClose(@Nullable BluetoothServerSocket ss) {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} finally {
callback.transportDisabled();
}
}
private void acceptContactConnections() {
while (isRunning()) {
BluetoothSocket s;
try {
s = socket.accept();
} catch (IOException e) {
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
return;
}
if (LOG.isLoggable(INFO)) {
String address = s.getRemoteDevice().getAddress();
LOG.info("Connection from " + scrubMacAddress(address));
}
backoff.reset();
callback.incomingConnectionCreated(wrapSocket(s));
}
}
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
return new DroidtoothTransportConnection(this, s);
}
private void enableAdapter() {
if (adapter != null && !adapter.isEnabled()) {
if (adapter.enable()) {
LOG.info("Enabling Bluetooth");
wasEnabledByUs = true;
} else {
LOG.info("Could not enable Bluetooth");
}
}
}
@Override
public void stop() {
running = false;
if (receiver != null) appContext.unregisterReceiver(receiver);
tryToClose(socket);
disableAdapter();
}
private void disableAdapter() {
if (adapter != null && adapter.isEnabled() && wasEnabledByUs) {
if (adapter.disable()) LOG.info("Disabling Bluetooth");
else LOG.info("Could not disable Bluetooth");
}
}
@Override
public boolean isRunning() {
return running && adapter != null && adapter.isEnabled();
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(Collection<ContactId> connected) {
if (!isRunning()) return;
backoff.increment();
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
ContactId c = e.getKey();
if (connected.contains(c)) continue;
String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(() -> {
if (!running) return;
BluetoothSocket s = connect(address, uuid);
if (s != null) {
backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s));
}
});
}
}
@Nullable
private BluetoothSocket connect(String address, String uuid) {
// Validate the address
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
if (LOG.isLoggable(WARNING))
// not scrubbing here to be able to figure out the problem
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
UUID u;
try {
u = UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
// Try to connect
BluetoothDevice d = adapter.getRemoteDevice(address);
BluetoothSocket s = null;
try {
s = d.createInsecureRfcommSocketToServiceRecord(u);
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
s.connect();
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubMacAddress(address));
return s;
} catch (IOException e) {
if (LOG.isLoggable(INFO)) {
LOG.info("Failed to connect to " + scrubMacAddress(address)
+ ": " + e);
}
tryToClose(s);
return null;
}
}
private void tryToClose(@Nullable Closeable c) {
try {
if (c != null) c.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
public DuplexTransportConnection createConnection(ContactId c) {
if (!isRunning()) return null;
TransportProperties p = callback.getRemoteProperties(c);
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
BluetoothSocket s = connect(address, uuid);
if (s == null) return null;
return new DroidtoothTransportConnection(this, s);
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!isRunning()) return null;
// There's no point listening if we can't discover our own address
String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
if (address.isEmpty()) return null;
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(commitment);
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving key agreement connections
BluetoothServerSocket ss;
try {
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
"RFCOMM", uuid);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
BdfList descriptor = new BdfList();
descriptor.add(TRANSPORT_ID_BLUETOOTH);
descriptor.add(StringUtils.macToBytes(address));
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) {
if (!isRunning()) return null;
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(commitment);
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
BluetoothSocket s = connect(address, uuid.toString());
if (s == null) return null;
return new DroidtoothTransportConnection(this, s);
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return StringUtils.macToString(mac);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof EnableBluetoothEvent) {
enableAdapterAsync();
} else if (e instanceof DisableBluetoothEvent) {
disableAdapterAsync();
}
}
private void enableAdapterAsync() {
ioExecutor.execute(this::enableAdapter);
}
private void disableAdapterAsync() {
ioExecutor.execute(this::disableAdapter);
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if (state == STATE_ON) {
LOG.info("Bluetooth enabled");
bind();
} else if (state == STATE_OFF) {
LOG.info("Bluetooth disabled");
tryToClose(socket);
}
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
if (scanMode == SCAN_MODE_NONE) {
LOG.info("Scan mode: None");
} else if (scanMode == SCAN_MODE_CONNECTABLE) {
LOG.info("Scan mode: Connectable");
} else if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
LOG.info("Scan mode: Discoverable");
}
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final BluetoothServerSocket ss;
private BluetoothKeyAgreementListener(BdfList descriptor,
BluetoothServerSocket ss) {
super(descriptor);
this.ss = ss;
}
@Override
public Callable<KeyAgreementConnection> listen() {
return () -> {
BluetoothSocket s = ss.accept();
if (LOG.isLoggable(INFO))
LOG.info(ID.getString() + ": Incoming connection");
return new KeyAgreementConnection(
new DroidtoothTransportConnection(
DroidtoothPlugin.this, s), ID);
};
}
@Override
public void close() {
try {
ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}
}

View File

@@ -59,7 +59,12 @@ import java.util.Map.Entry;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
@@ -70,10 +75,15 @@ import javax.net.SocketFactory;
import static android.content.Context.CONNECTIVITY_SERVICE; import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.MODE_PRIVATE; import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.POWER_SERVICE; import static android.content.Context.POWER_SERVICE;
import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.TYPE_WIFI; import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
import static android.os.PowerManager.PARTIAL_WAKE_LOCK; import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS;
@@ -102,6 +112,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
Logger.getLogger(TorPlugin.class.getName()); Logger.getLogger(TorPlugin.class.getName());
private final Executor ioExecutor; private final Executor ioExecutor;
private final ScheduledExecutorService scheduler;
private final Context appContext; private final Context appContext;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
private final DevReporter reporter; private final DevReporter reporter;
@@ -114,6 +125,9 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final File torDirectory, torFile, geoIpFile, configFile; private final File torDirectory, torFile, geoIpFile, configFile;
private final File doneFile, cookieFile; private final File doneFile, cookieFile;
private final PowerManager.WakeLock wakeLock; private final PowerManager.WakeLock wakeLock;
private final Lock connectionStatusLock;
private final AtomicReference<Future<?>> connectivityCheck =
new AtomicReference<>();
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false; private volatile boolean running = false;
@@ -122,12 +136,13 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private volatile TorControlConnection controlConnection = null; private volatile TorControlConnection controlConnection = null;
private volatile BroadcastReceiver networkStateReceiver = null; private volatile BroadcastReceiver networkStateReceiver = null;
TorPlugin(Executor ioExecutor, Context appContext, TorPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
LocationUtils locationUtils, DevReporter reporter, Context appContext, LocationUtils locationUtils,
SocketFactory torSocketFactory, Backoff backoff, DevReporter reporter, SocketFactory torSocketFactory,
DuplexPluginCallback callback, String architecture, int maxLatency, Backoff backoff, DuplexPluginCallback callback,
int maxIdleTime) { String architecture, int maxLatency, int maxIdleTime) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.appContext = appContext; this.appContext = appContext;
this.locationUtils = locationUtils; this.locationUtils = locationUtils;
this.reporter = reporter; this.reporter = reporter;
@@ -152,6 +167,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
// This tag will prevent Huawei's powermanager from killing us. // This tag will prevent Huawei's powermanager from killing us.
wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService"); wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService");
wakeLock.setReferenceCounted(false); wakeLock.setReferenceCounted(false);
connectionStatusLock = new ReentrantLock();
} }
@Override @Override
@@ -257,7 +273,11 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
// Register to receive network status events // Register to receive network status events
networkStateReceiver = new NetworkStateReceiver(); networkStateReceiver = new NetworkStateReceiver();
IntentFilter filter = new IntentFilter(CONNECTIVITY_ACTION); IntentFilter filter = new IntentFilter();
filter.addAction(CONNECTIVITY_ACTION);
filter.addAction(ACTION_SCREEN_ON);
filter.addAction(ACTION_SCREEN_OFF);
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
appContext.registerReceiver(networkStateReceiver, filter); appContext.registerReceiver(networkStateReceiver, filter);
// Bind a server socket to receive incoming hidden service connections // Bind a server socket to receive incoming hidden service connections
bind(); bind();
@@ -594,7 +614,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override @Override
public DuplexTransportConnection createKeyAgreementConnection( public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) { byte[] commitment, BdfList descriptor) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@@ -618,6 +638,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override @Override
public void orConnStatus(String status, String orName) { public void orConnStatus(String status, String orName) {
if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status); if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
if (status.equals("CLOSED") || status.equals("FAILED"))
updateConnectionStatus(); // Check whether we've lost connectivity
} }
@Override @Override
@@ -657,7 +679,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
@Override @Override
public void onEvent(int event, String path) { public void onEvent(int event, @Nullable String path) {
stopWatching(); stopWatching();
latch.countDown(); latch.countDown();
} }
@@ -677,7 +699,17 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private void updateConnectionStatus() { private void updateConnectionStatus() {
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
if (!running) return; if (!running) return;
try {
connectionStatusLock.lock();
updateConnectionStatusLocked();
} finally {
connectionStatusLock.unlock();
}
});
}
// Locking: connectionStatusLock
private void updateConnectionStatusLocked() {
Object o = appContext.getSystemService(CONNECTIVITY_SERVICE); Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
ConnectivityManager cm = (ConnectivityManager) o; ConnectivityManager cm = (ConnectivityManager) o;
NetworkInfo net = cm.getActiveNetworkInfo(); NetworkInfo net = cm.getActiveNetworkInfo();
@@ -713,7 +745,14 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} catch (IOException e) { } catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} }
}); }
private void scheduleConnectionStatusUpdate() {
Future<?> newConnectivityCheck =
scheduler.schedule(this::updateConnectionStatus, 1, MINUTES);
Future<?> oldConnectivityCheck =
connectivityCheck.getAndSet(newConnectivityCheck);
if (oldConnectivityCheck != null) oldConnectivityCheck.cancel(false);
} }
private class NetworkStateReceiver extends BroadcastReceiver { private class NetworkStateReceiver extends BroadcastReceiver {
@@ -721,9 +760,12 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override @Override
public void onReceive(Context ctx, Intent i) { public void onReceive(Context ctx, Intent i) {
if (!running) return; if (!running) return;
if (CONNECTIVITY_ACTION.equals(i.getAction())) { String action = i.getAction();
LOG.info("Detected connectivity change"); if (LOG.isLoggable(INFO)) LOG.info("Received broadcast " + action);
updateConnectionStatus(); updateConnectionStatus();
if (ACTION_SCREEN_ON.equals(action)
|| ACTION_SCREEN_OFF.equals(action)) {
scheduleConnectionStatusUpdate();
} }
} }
} }

View File

@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.AndroidUtils;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@@ -36,6 +37,7 @@ public class TorPluginFactory implements DuplexPluginFactory {
private static final double BACKOFF_BASE = 1.2; private static final double BACKOFF_BASE = 1.2;
private final Executor ioExecutor; private final Executor ioExecutor;
private final ScheduledExecutorService scheduler;
private final Context appContext; private final Context appContext;
private final LocationUtils locationUtils; private final LocationUtils locationUtils;
private final DevReporter reporter; private final DevReporter reporter;
@@ -43,11 +45,13 @@ public class TorPluginFactory implements DuplexPluginFactory {
private final SocketFactory torSocketFactory; private final SocketFactory torSocketFactory;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
public TorPluginFactory(Executor ioExecutor, Context appContext, public TorPluginFactory(Executor ioExecutor,
ScheduledExecutorService scheduler, Context appContext,
LocationUtils locationUtils, DevReporter reporter, LocationUtils locationUtils, DevReporter reporter,
EventBus eventBus, SocketFactory torSocketFactory, EventBus eventBus, SocketFactory torSocketFactory,
BackoffFactory backoffFactory) { BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.scheduler = scheduler;
this.appContext = appContext; this.appContext = appContext;
this.locationUtils = locationUtils; this.locationUtils = locationUtils;
this.reporter = reporter; this.reporter = reporter;
@@ -89,9 +93,9 @@ public class TorPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
TorPlugin plugin = new TorPlugin(ioExecutor, appContext, locationUtils, TorPlugin plugin = new TorPlugin(ioExecutor, scheduler, appContext,
reporter, torSocketFactory, backoff, callback, architecture, locationUtils, reporter, torSocketFactory, backoff, callback,
MAX_LATENCY, MAX_IDLE_TIME); architecture, MAX_LATENCY, MAX_IDLE_TIME);
eventBus.addListener(plugin); eventBus.addListener(plugin);
return plugin; return plugin;
} }

View File

@@ -0,0 +1,7 @@
package org.briarproject.bramble.api.db;
/**
* Thrown when the database uses a newer schema than the current code.
*/
public class DataTooNewException extends DbException {
}

View File

@@ -0,0 +1,8 @@
package org.briarproject.bramble.api.db;
/**
* Thrown when the database uses an older schema than the current code and
* cannot be migrated.
*/
public class DataTooOldException extends DbException {
}

View File

@@ -37,8 +37,13 @@ public interface DatabaseComponent {
/** /**
* Opens the database and returns true if the database already existed. * Opens the database and returns true if the database already existed.
*
* @throws DataTooNewException if the data uses a newer schema than the
* current code
* @throws DataTooOldException if the data uses an older schema than the
* current code and cannot be migrated
*/ */
boolean open() throws DbException; boolean open(@Nullable MigrationListener listener) throws DbException;
/** /**
* Waits for any open transactions to finish and closes the database. * Waits for any open transactions to finish and closes the database.
@@ -373,6 +378,16 @@ public interface DatabaseComponent {
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m) MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
throws DbException; throws DbException;
/*
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE if
* no messages are scheduled to be sent.
* <p/>
* Read-only.
*/
long getNextSendTime(Transaction txn, ContactId c) throws DbException;
/** /**
* Returns all settings in the given namespace. * Returns all settings in the given namespace.
* <p/> * <p/>

View File

@@ -0,0 +1,11 @@
package org.briarproject.bramble.api.db;
public interface MigrationListener {
/**
* This is called when a migration is started while opening the database.
* It will be called once for each migration being applied.
*/
void onMigrationRun();
}

View File

@@ -2,7 +2,7 @@ package org.briarproject.bramble.api.keyagreement;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import java.util.concurrent.Callable; import java.io.IOException;
/** /**
* An class for managing a particular key agreement listener. * An class for managing a particular key agreement listener.
@@ -24,11 +24,11 @@ public abstract class KeyAgreementListener {
} }
/** /**
* Starts listening for incoming connections, and returns a Callable that * Blocks until an incoming connection is received and returns it.
* will return a KeyAgreementConnection when an incoming connection is *
* received. * @throws IOException if an error occurs or {@link #close()} is called.
*/ */
public abstract Callable<KeyAgreementConnection> listen(); public abstract KeyAgreementConnection accept() throws IOException;
/** /**
* Closes the underlying server socket. * Closes the underlying server socket.

View File

@@ -21,7 +21,25 @@ public interface LifecycleManager {
* The result of calling {@link #startServices(String)}. * The result of calling {@link #startServices(String)}.
*/ */
enum StartResult { enum StartResult {
ALREADY_RUNNING, DB_ERROR, SERVICE_ERROR, SUCCESS ALREADY_RUNNING,
DB_ERROR,
DATA_TOO_OLD_ERROR,
DATA_TOO_NEW_ERROR,
SERVICE_ERROR,
SUCCESS
}
/**
* The state the lifecycle can be in.
* Returned by {@link #getLifecycleState()}
*/
enum LifecycleState {
STARTING, MIGRATING_DATABASE, STARTING_SERVICES, RUNNING, STOPPING;
public boolean isAfter(LifecycleState state) {
return ordinal() > state.ordinal();
}
} }
/** /**
@@ -71,4 +89,10 @@ public interface LifecycleManager {
* the {@link DatabaseComponent} to be closed before returning. * the {@link DatabaseComponent} to be closed before returning.
*/ */
void waitForShutdown() throws InterruptedException; void waitForShutdown() throws InterruptedException;
/**
* Returns the current state of the lifecycle.
*/
LifecycleState getLifecycleState();
} }

View File

@@ -0,0 +1,20 @@
package org.briarproject.bramble.api.lifecycle.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
/**
* An event that is broadcast when the app enters a new lifecycle state.
*/
public class LifecycleEvent extends Event {
private final LifecycleState state;
public LifecycleEvent(LifecycleState state) {
this.state = state;
}
public LifecycleState getLifecycleState() {
return state;
}
}

View File

@@ -1,9 +0,0 @@
package org.briarproject.bramble.api.lifecycle.event;
import org.briarproject.bramble.api.event.Event;
/**
* An event that is broadcast when the app is shutting down.
*/
public class ShutdownEvent extends Event {
}

View File

@@ -36,9 +36,9 @@ public interface DuplexPlugin extends Plugin {
/** /**
* Attempts to connect to the remote peer specified in the given descriptor. * Attempts to connect to the remote peer specified in the given descriptor.
* Returns null if no connection can be established within the given time. * Returns null if no connection can be established.
*/ */
@Nullable @Nullable
DuplexTransportConnection createKeyAgreementConnection( DuplexTransportConnection createKeyAgreementConnection(
byte[] remoteCommitment, BdfList descriptor, long timeout); byte[] remoteCommitment, BdfList descriptor);
} }

View File

@@ -0,0 +1,15 @@
package org.briarproject.bramble.api.plugin.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* An event that informs the Bluetooth plugin that we have enabled the
* Bluetooth adapter.
*/
@Immutable
@NotNullByDefault
public class BluetoothEnabledEvent extends Event {
}

View File

@@ -6,7 +6,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
/** /**
* An event asks the Bluetooth plugin to enable the Bluetooth adapter. * An event that asks the Bluetooth plugin to enable the Bluetooth adapter.
*/ */
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault

View File

@@ -126,6 +126,10 @@ public class StringUtils {
return toUtf8(s).length > maxLength; return toUtf8(s).length > maxLength;
} }
public static boolean isValidMac(String mac) {
return MAC.matcher(mac).matches();
}
public static byte[] macToBytes(String mac) { public static byte[] macToBytes(String mac) {
if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException(); if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException();
return fromHexString(mac.replaceAll(":", "")); return fromHexString(mac.replaceAll(":", ""));

View File

@@ -2,12 +2,27 @@ package org.briarproject.bramble.test;
import org.briarproject.bramble.api.UniqueId; import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.util.IoUtils; import org.briarproject.bramble.util.IoUtils;
import java.io.File; import java.io.File;
import java.util.Random; import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
public class TestUtils { public class TestUtils {
private static final AtomicInteger nextTestDir = private static final AtomicInteger nextTestDir =
@@ -38,4 +53,50 @@ public class TestUtils {
return new SecretKey(getRandomBytes(SecretKey.LENGTH)); return new SecretKey(getRandomBytes(SecretKey.LENGTH));
} }
public static LocalAuthor getLocalAuthor() {
return getLocalAuthor(1 + random.nextInt(MAX_AUTHOR_NAME_LENGTH));
}
public static LocalAuthor getLocalAuthor(int nameLength) {
AuthorId id = new AuthorId(getRandomId());
String name = getRandomString(nameLength);
byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
byte[] privateKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
long created = System.currentTimeMillis();
return new LocalAuthor(id, name, publicKey, privateKey, created);
}
public static Author getAuthor() {
return getAuthor(1 + random.nextInt(MAX_AUTHOR_NAME_LENGTH));
}
public static Author getAuthor(int nameLength) {
AuthorId id = new AuthorId(getRandomId());
String name = getRandomString(nameLength);
byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
return new Author(id, name, publicKey);
}
public static Group getGroup(ClientId clientId) {
int descriptorLength = 1 + random.nextInt(MAX_GROUP_DESCRIPTOR_LENGTH);
return getGroup(clientId, descriptorLength);
}
public static Group getGroup(ClientId clientId, int descriptorLength) {
GroupId groupId = new GroupId(getRandomId());
byte[] descriptor = getRandomBytes(descriptorLength);
return new Group(groupId, clientId, descriptor);
}
public static Message getMessage(GroupId groupId) {
int bodyLength = 1 + random.nextInt(MAX_MESSAGE_BODY_LENGTH);
return getMessage(groupId, MESSAGE_HEADER_LENGTH + bodyLength);
}
public static Message getMessage(GroupId groupId, int rawLength) {
MessageId id = new MessageId(getRandomId());
byte[] raw = getRandomBytes(rawLength);
long timestamp = System.currentTimeMillis();
return new Message(id, groupId, timestamp, raw);
}
} }

View File

@@ -2,8 +2,11 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DataTooNewException;
import org.briarproject.bramble.api.db.DataTooOldException;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
@@ -37,8 +40,13 @@ interface Database<T> {
/** /**
* Opens the database and returns true if the database already existed. * Opens the database and returns true if the database already existed.
*
* @throws DataTooNewException if the data uses a newer schema than the
* current code
* @throws DataTooOldException if the data uses an older schema than the
* current code and cannot be migrated
*/ */
boolean open() throws DbException; boolean open(@Nullable MigrationListener listener) throws DbException;
/** /**
* Prevents new transactions from starting, waits for all current * Prevents new transactions from starting, waits for all current
@@ -449,6 +457,16 @@ interface Database<T> {
Collection<MessageId> getMessagesToShare(T txn, ClientId c) Collection<MessageId> getMessagesToShare(T txn, ClientId c)
throws DbException; throws DbException;
/**
* Returns the next time (in milliseconds since the Unix epoch) when a
* message is due to be sent to the given contact. The returned value may
* be zero if a message is due to be sent immediately, or Long.MAX_VALUE
* if no messages are scheduled to be sent.
* <p/>
* Read-only.
*/
long getNextSendTime(T txn, ContactId c) throws DbException;
/** /**
* Returns the message with the given ID, in serialised form, or null if * Returns the message with the given ID, in serialised form, or null if
* the message has been deleted. * the message has been deleted.

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.db.ContactExistsException;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.NoSuchGroupException; import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.db.NoSuchLocalAuthorException; import org.briarproject.bramble.api.db.NoSuchLocalAuthorException;
@@ -90,8 +91,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
private final ReentrantReadWriteLock lock = private final ReentrantReadWriteLock lock =
new ReentrantReadWriteLock(true); new ReentrantReadWriteLock(true);
private volatile int shutdownHandle = -1;
@Inject @Inject
DatabaseComponentImpl(Database<T> db, Class<T> txnClass, EventBus eventBus, DatabaseComponentImpl(Database<T> db, Class<T> txnClass, EventBus eventBus,
ShutdownManager shutdown) { ShutdownManager shutdown) {
@@ -102,23 +101,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
@Override @Override
public boolean open() throws DbException { public boolean open(@Nullable MigrationListener listener)
Runnable shutdownHook = () -> { throws DbException {
boolean reopened = db.open(listener);
shutdown.addShutdownHook(() -> {
try { try {
close(); close();
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} }
}; });
boolean reopened = db.open();
shutdownHandle = shutdown.addShutdownHook(shutdownHook);
return reopened; return reopened;
} }
@Override @Override
public void close() throws DbException { public void close() throws DbException {
if (closed.getAndSet(true)) return; if (closed.getAndSet(true)) return;
shutdown.removeShutdownHook(shutdownHandle);
db.close(); db.close();
} }
@@ -583,6 +581,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessageDependents(txn, m); return db.getMessageDependents(txn, m);
} }
@Override
public long getNextSendTime(Transaction transaction, ContactId c)
throws DbException {
T txn = unbox(transaction);
return db.getNextSendTime(txn, c);
}
@Override @Override
public Settings getSettings(Transaction transaction, String namespace) public Settings getSettings(Transaction transaction, String namespace)
throws DbException { throws DbException {

View File

@@ -23,10 +23,4 @@ interface DatabaseConstants {
*/ */
String SCHEMA_VERSION_KEY = "schemaVersion"; String SCHEMA_VERSION_KEY = "schemaVersion";
/**
* The {@link Settings} key under which the minimum supported database
* schema version is stored.
*/
String MIN_SCHEMA_VERSION_KEY = "minSchemaVersion";
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
@@ -13,6 +14,7 @@ import java.sql.DriverManager;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Properties; import java.util.Properties;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
@@ -40,10 +42,11 @@ class H2Database extends JdbcDatabase {
} }
@Override @Override
public boolean open() throws DbException { public boolean open(@Nullable MigrationListener listener)
throws DbException {
boolean reopen = config.databaseExists(); boolean reopen = config.databaseExists();
if (!reopen) config.getDatabaseDirectory().mkdirs(); if (!reopen) config.getDatabaseDirectory().mkdirs();
super.open("org.h2.Driver", reopen); super.open("org.h2.Driver", reopen, listener);
return reopen; return reopen;
} }

View File

@@ -3,9 +3,12 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DataTooNewException;
import org.briarproject.bramble.api.db.DataTooOldException;
import org.briarproject.bramble.api.db.DbClosedException; import org.briarproject.bramble.api.db.DbClosedException;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
@@ -47,6 +50,7 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.db.Metadata.REMOVE; import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
@@ -57,7 +61,6 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING; import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; import static org.briarproject.bramble.api.sync.ValidationManager.State.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.MIN_SCHEMA_VERSION_KEY;
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY; import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry; import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
@@ -68,8 +71,8 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
@NotNullByDefault @NotNullByDefault
abstract class JdbcDatabase implements Database<Connection> { abstract class JdbcDatabase implements Database<Connection> {
private static final int SCHEMA_VERSION = 30; // Package access for testing
private static final int MIN_SCHEMA_VERSION = 30; static final int CODE_SCHEMA_VERSION = 31;
private static final String CREATE_SETTINGS = private static final String CREATE_SETTINGS =
"CREATE TABLE settings" "CREATE TABLE settings"
@@ -148,11 +151,16 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_MESSAGE_METADATA = private static final String CREATE_MESSAGE_METADATA =
"CREATE TABLE messageMetadata" "CREATE TABLE messageMetadata"
+ " (messageId HASH NOT NULL," + " (messageId HASH NOT NULL,"
+ " groupId HASH NOT NULL," // Denormalised
+ " state INT NOT NULL," // Denormalised
+ " key VARCHAR NOT NULL," + " key VARCHAR NOT NULL,"
+ " value BINARY NOT NULL," + " value BINARY NOT NULL,"
+ " PRIMARY KEY (messageId, key)," + " PRIMARY KEY (messageId, key),"
+ " FOREIGN KEY (messageId)" + " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)" + " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE,"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String CREATE_MESSAGE_DEPENDENCIES = private static final String CREATE_MESSAGE_DEPENDENCIES =
@@ -236,25 +244,13 @@ abstract class JdbcDatabase implements Database<Connection> {
"CREATE INDEX IF NOT EXISTS contactsByAuthorId" "CREATE INDEX IF NOT EXISTS contactsByAuthorId"
+ " ON contacts (authorId)"; + " ON contacts (authorId)";
private static final String INDEX_MESSAGES_BY_GROUP_ID =
"CREATE INDEX IF NOT EXISTS messagesByGroupId"
+ " ON messages (groupId)";
private static final String INDEX_OFFERS_BY_CONTACT_ID =
"CREATE INDEX IF NOT EXISTS offersByContactId"
+ " ON offers (contactId)";
private static final String INDEX_GROUPS_BY_CLIENT_ID = private static final String INDEX_GROUPS_BY_CLIENT_ID =
"CREATE INDEX IF NOT EXISTS groupsByClientId" "CREATE INDEX IF NOT EXISTS groupsByClientId"
+ " ON groups (clientId)"; + " ON groups (clientId)";
private static final String INDEX_MESSAGE_METADATA_BY_MESSAGE_ID = private static final String INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE =
"CREATE INDEX IF NOT EXISTS messageMetadataByMessageId" "CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
+ " ON messageMetadata (messageId)"; + " ON messageMetadata (groupId, state)";
private static final String INDEX_GROUP_METADATA_BY_GROUP_ID =
"CREATE INDEX IF NOT EXISTS groupMetadataByGroupId"
+ " ON groupMetadata (groupId)";
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(JdbcDatabase.class.getName()); Logger.getLogger(JdbcDatabase.class.getName());
@@ -284,7 +280,8 @@ abstract class JdbcDatabase implements Database<Connection> {
this.clock = clock; this.clock = clock;
} }
protected void open(String driverClass, boolean reopen) throws DbException { protected void open(String driverClass, boolean reopen,
@Nullable MigrationListener listener) throws DbException {
// Load the JDBC driver // Load the JDBC driver
try { try {
Class.forName(driverClass); Class.forName(driverClass);
@@ -295,10 +292,10 @@ abstract class JdbcDatabase implements Database<Connection> {
Connection txn = startTransaction(); Connection txn = startTransaction();
try { try {
if (reopen) { if (reopen) {
if (!checkSchemaVersion(txn)) throw new DbException(); checkSchemaVersion(txn, listener);
} else { } else {
createTables(txn); createTables(txn);
storeSchemaVersion(txn); storeSchemaVersion(txn, CODE_SCHEMA_VERSION);
} }
createIndexes(txn); createIndexes(txn);
commitTransaction(txn); commitTransaction(txn);
@@ -308,19 +305,51 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
private boolean checkSchemaVersion(Connection txn) throws DbException { /**
* Compares the schema version stored in the database with the schema
* version used by the current code and applies any suitable migrations to
* the data if necessary.
*
* @throws DataTooNewException if the data uses a newer schema than the
* current code
* @throws DataTooOldException if the data uses an older schema than the
* current code and cannot be migrated
*/
private void checkSchemaVersion(Connection txn,
@Nullable MigrationListener listener) throws DbException {
Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE); Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
int schemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1); int dataSchemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1);
if (schemaVersion == SCHEMA_VERSION) return true; if (dataSchemaVersion == -1) throw new DbException();
if (schemaVersion < MIN_SCHEMA_VERSION) return false; if (dataSchemaVersion == CODE_SCHEMA_VERSION) return;
int minSchemaVersion = s.getInt(MIN_SCHEMA_VERSION_KEY, -1); if (CODE_SCHEMA_VERSION < dataSchemaVersion)
return SCHEMA_VERSION >= minSchemaVersion; throw new DataTooNewException();
// Apply any suitable migrations in order
for (Migration<Connection> m : getMigrations()) {
int start = m.getStartVersion(), end = m.getEndVersion();
if (start == dataSchemaVersion) {
if (LOG.isLoggable(INFO))
LOG.info("Migrating from schema " + start + " to " + end);
if (listener != null) listener.onMigrationRun();
// Apply the migration
m.migrate(txn);
// Store the new schema version
storeSchemaVersion(txn, end);
dataSchemaVersion = end;
}
}
if (dataSchemaVersion != CODE_SCHEMA_VERSION)
throw new DataTooOldException();
} }
private void storeSchemaVersion(Connection txn) throws DbException { // Package access for testing
List<Migration<Connection>> getMigrations() {
return Collections.singletonList(new Migration30_31());
}
private void storeSchemaVersion(Connection txn, int version)
throws DbException {
Settings s = new Settings(); Settings s = new Settings();
s.putInt(SCHEMA_VERSION_KEY, SCHEMA_VERSION); s.putInt(SCHEMA_VERSION_KEY, version);
s.putInt(MIN_SCHEMA_VERSION_KEY, MIN_SCHEMA_VERSION);
mergeSettings(txn, s, DB_SETTINGS_NAMESPACE); mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
} }
@@ -370,11 +399,8 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
s = txn.createStatement(); s = txn.createStatement();
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID); s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
s.executeUpdate(INDEX_MESSAGES_BY_GROUP_ID);
s.executeUpdate(INDEX_OFFERS_BY_CONTACT_ID);
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID); s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_MESSAGE_ID); s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
s.executeUpdate(INDEX_GROUP_METADATA_BY_GROUP_ID);
s.close(); s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s); tryToClose(s);
@@ -500,7 +526,8 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
// Create a contact row // Create a contact row
String sql = "INSERT INTO contacts" String sql = "INSERT INTO contacts"
+ " (authorId, name, publicKey, localAuthorId, verified, active)" + " (authorId, name, publicKey, localAuthorId,"
+ " verified, active)"
+ " VALUES (?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, remote.getId().getBytes()); ps.setBytes(1, remote.getId().getBytes());
@@ -1330,16 +1357,13 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
// Retrieve the message IDs for each query term and intersect // Retrieve the message IDs for each query term and intersect
Set<MessageId> intersection = null; Set<MessageId> intersection = null;
String sql = "SELECT m.messageId" String sql = "SELECT messageId FROM messageMetadata"
+ " FROM messages AS m" + " WHERE groupId = ? AND state = ?"
+ " JOIN messageMetadata AS md"
+ " ON m.messageId = md.messageId"
+ " WHERE state = ? AND groupId = ?"
+ " AND key = ? AND value = ?"; + " AND key = ? AND value = ?";
for (Entry<String, byte[]> e : query.entrySet()) { for (Entry<String, byte[]> e : query.entrySet()) {
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, DELIVERED.getValue()); ps.setBytes(1, g.getBytes());
ps.setBytes(2, g.getBytes()); ps.setInt(2, DELIVERED.getValue());
ps.setString(3, e.getKey()); ps.setString(3, e.getKey());
ps.setBytes(4, e.getValue()); ps.setBytes(4, e.getValue());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -1367,25 +1391,20 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT m.messageId, key, value" String sql = "SELECT messageId, key, value"
+ " FROM messages AS m" + " FROM messageMetadata"
+ " JOIN messageMetadata AS md" + " WHERE groupId = ? AND state = ?";
+ " ON m.messageId = md.messageId"
+ " WHERE state = ? AND groupId = ?"
+ " ORDER BY m.messageId";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, DELIVERED.getValue()); ps.setBytes(1, g.getBytes());
ps.setBytes(2, g.getBytes()); ps.setInt(2, DELIVERED.getValue());
rs = ps.executeQuery(); rs = ps.executeQuery();
Map<MessageId, Metadata> all = new HashMap<>(); Map<MessageId, Metadata> all = new HashMap<>();
Metadata metadata = null;
MessageId lastMessageId = null;
while (rs.next()) { while (rs.next()) {
MessageId messageId = new MessageId(rs.getBytes(1)); MessageId messageId = new MessageId(rs.getBytes(1));
if (lastMessageId == null || !messageId.equals(lastMessageId)) { Metadata metadata = all.get(messageId);
if (metadata == null) {
metadata = new Metadata(); metadata = new Metadata();
all.put(messageId, metadata); all.put(messageId, metadata);
lastMessageId = messageId;
} }
metadata.put(rs.getString(2), rs.getBytes(3)); metadata.put(rs.getString(2), rs.getBytes(3));
} }
@@ -1440,10 +1459,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT key, value FROM messageMetadata AS md" String sql = "SELECT key, value FROM messageMetadata"
+ " JOIN messages AS m" + " WHERE state = ? AND messageId = ?";
+ " ON m.messageId = md.messageId"
+ " WHERE m.state = ? AND md.messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, DELIVERED.getValue()); ps.setInt(1, DELIVERED.getValue());
ps.setBytes(2, m.getBytes()); ps.setBytes(2, m.getBytes());
@@ -1466,11 +1483,9 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT key, value FROM messageMetadata AS md" String sql = "SELECT key, value FROM messageMetadata"
+ " JOIN messages AS m" + " WHERE (state = ? OR state = ?)"
+ " ON m.messageId = md.messageId" + " AND messageId = ?";
+ " WHERE (m.state = ? OR m.state = ?)"
+ " AND md.messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, DELIVERED.getValue()); ps.setInt(1, DELIVERED.getValue());
ps.setInt(2, PENDING.getValue()); ps.setInt(2, PENDING.getValue());
@@ -1833,6 +1848,41 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public long getNextSendTime(Connection txn, ContactId c)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT expiry FROM messages AS m"
+ " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId"
+ " JOIN statuses AS s"
+ " ON m.messageId = s.messageId"
+ " AND gv.contactId = s.contactId"
+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
+ " AND seen = FALSE"
+ " ORDER BY expiry LIMIT 1";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
rs = ps.executeQuery();
long nextSendTime = Long.MAX_VALUE;
if (rs.next()) {
nextSendTime = rs.getLong(1);
if (rs.next()) throw new AssertionError();
}
rs.close();
ps.close();
return nextSendTime;
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
@Override @Override
@Nullable @Nullable
public byte[] getRawMessage(Connection txn, MessageId m) public byte[] getRawMessage(Connection txn, MessageId m)
@@ -2058,25 +2108,92 @@ abstract class JdbcDatabase implements Database<Connection> {
@Override @Override
public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta) public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
throws DbException { throws DbException {
mergeMetadata(txn, g.getBytes(), meta, "groupMetadata", "groupId"); PreparedStatement ps = null;
try {
Map<String, byte[]> added = removeOrUpdateMetadata(txn,
g.getBytes(), meta, "groupMetadata", "groupId");
if (added.isEmpty()) return;
// Insert any keys that don't already exist
String sql = "INSERT INTO groupMetadata (groupId, key, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
for (Entry<String, byte[]> e : added.entrySet()) {
ps.setString(2, e.getKey());
ps.setBytes(3, e.getValue());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != added.size())
throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
} }
@Override @Override
public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta) public void mergeMessageMetadata(Connection txn, MessageId m,
throws DbException { Metadata meta) throws DbException {
mergeMetadata(txn, m.getBytes(), meta, "messageMetadata", "messageId"); PreparedStatement ps = null;
ResultSet rs = null;
try {
Map<String, byte[]> added = removeOrUpdateMetadata(txn,
m.getBytes(), meta, "messageMetadata", "messageId");
if (added.isEmpty()) return;
// Get the group ID and message state for the denormalised columns
String sql = "SELECT groupId, state FROM messages"
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
GroupId g = new GroupId(rs.getBytes(1));
State state = State.fromValue(rs.getInt(2));
rs.close();
ps.close();
// Insert any keys that don't already exist
sql = "INSERT INTO messageMetadata"
+ " (messageId, groupId, state, key, value)"
+ " VALUES (?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setBytes(2, g.getBytes());
ps.setInt(3, state.getValue());
for (Entry<String, byte[]> e : added.entrySet()) {
ps.setString(4, e.getKey());
ps.setBytes(5, e.getValue());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != added.size())
throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
} }
private void mergeMetadata(Connection txn, byte[] id, Metadata meta, // Removes or updates any existing entries, returns any entries that
String tableName, String columnName) throws DbException { // need to be added
private Map<String, byte[]> removeOrUpdateMetadata(Connection txn,
byte[] id, Metadata meta, String tableName, String columnName)
throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
// Determine which keys are being removed // Determine which keys are being removed
List<String> removed = new ArrayList<>(); List<String> removed = new ArrayList<>();
Map<String, byte[]> retained = new HashMap<>(); Map<String, byte[]> notRemoved = new HashMap<>();
for (Entry<String, byte[]> e : meta.entrySet()) { for (Entry<String, byte[]> e : meta.entrySet()) {
if (e.getValue() == REMOVE) removed.add(e.getKey()); if (e.getValue() == REMOVE) removed.add(e.getKey());
else retained.put(e.getKey(), e.getValue()); else notRemoved.put(e.getKey(), e.getValue());
} }
// Delete any keys that are being removed // Delete any keys that are being removed
if (!removed.isEmpty()) { if (!removed.isEmpty()) {
@@ -2097,45 +2214,33 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
ps.close(); ps.close();
} }
if (retained.isEmpty()) return; if (notRemoved.isEmpty()) return Collections.emptyMap();
// Update any keys that already exist // Update any keys that already exist
String sql = "UPDATE " + tableName + " SET value = ?" String sql = "UPDATE " + tableName + " SET value = ?"
+ " WHERE " + columnName + " = ? AND key = ?"; + " WHERE " + columnName + " = ? AND key = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(2, id); ps.setBytes(2, id);
for (Entry<String, byte[]> e : retained.entrySet()) { for (Entry<String, byte[]> e : notRemoved.entrySet()) {
ps.setBytes(1, e.getValue()); ps.setBytes(1, e.getValue());
ps.setString(3, e.getKey()); ps.setString(3, e.getKey());
ps.addBatch(); ps.addBatch();
} }
int[] batchAffected = ps.executeBatch(); int[] batchAffected = ps.executeBatch();
if (batchAffected.length != retained.size()) if (batchAffected.length != notRemoved.size())
throw new DbStateException(); throw new DbStateException();
for (int rows : batchAffected) { for (int rows : batchAffected) {
if (rows < 0) throw new DbStateException(); if (rows < 0) throw new DbStateException();
if (rows > 1) throw new DbStateException(); if (rows > 1) throw new DbStateException();
} }
// Insert any keys that don't already exist
sql = "INSERT INTO " + tableName
+ " (" + columnName + ", key, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, id);
int updateIndex = 0, inserted = 0;
for (Entry<String, byte[]> e : retained.entrySet()) {
if (batchAffected[updateIndex] == 0) {
ps.setString(2, e.getKey());
ps.setBytes(3, e.getValue());
ps.addBatch();
inserted++;
}
updateIndex++;
}
batchAffected = ps.executeBatch();
if (batchAffected.length != inserted) throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close(); ps.close();
// Are there any keys that don't already exist?
Map<String, byte[]> added = new HashMap<>();
int updateIndex = 0;
for (Entry<String, byte[]> e : notRemoved.entrySet()) {
if (batchAffected[updateIndex++] == 0)
added.put(e.getKey(), e.getValue());
}
return added;
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(ps); tryToClose(ps);
throw new DbException(e); throw new DbException(e);
@@ -2488,7 +2593,8 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public void setMessageShared(Connection txn, MessageId m) throws DbException { public void setMessageShared(Connection txn, MessageId m)
throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE messages SET shared = TRUE" String sql = "UPDATE messages SET shared = TRUE"
@@ -2516,6 +2622,14 @@ abstract class JdbcDatabase implements Database<Connection> {
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException(); if (affected < 0 || affected > 1) throw new DbStateException();
ps.close(); ps.close();
// Update denormalised column in messageMetadata
sql = "UPDATE messageMetadata SET state = ? WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue());
ps.setBytes(2, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(ps); tryToClose(ps);
throw new DbException(e); throw new DbException(e);

View File

@@ -0,0 +1,18 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
interface Migration<T> {
/**
* Returns the schema version from which this migration starts.
*/
int getStartVersion();
/**
* Returns the schema version at which this migration ends.
*/
int getEndVersion();
void migrate(T txn) throws DbException;
}

View File

@@ -0,0 +1,75 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.WARNING;
class Migration30_31 implements Migration<Connection> {
private static final Logger LOG =
Logger.getLogger(Migration30_31.class.getName());
@Override
public int getStartVersion() {
return 30;
}
@Override
public int getEndVersion() {
return 31;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
// Add groupId column
s.execute("ALTER TABLE messageMetadata"
+ " ADD COLUMN groupId BINARY(32) AFTER messageId");
// Populate groupId column
s.execute("UPDATE messageMetadata AS mm SET groupId ="
+ " (SELECT groupId FROM messages AS m"
+ " WHERE mm.messageId = m.messageId)");
// Add not null constraint now column has been populated
s.execute("ALTER TABLE messageMetadata"
+ " ALTER COLUMN groupId"
+ " SET NOT NULL");
// Add foreign key constraint
s.execute("ALTER TABLE messageMetadata"
+ " ADD CONSTRAINT groupIdForeignKey"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE");
// Add state column
s.execute("ALTER TABLE messageMetadata"
+ " ADD COLUMN state INT AFTER groupId");
// Populate state column
s.execute("UPDATE messageMetadata AS mm SET state ="
+ " (SELECT state FROM messages AS m"
+ " WHERE mm.messageId = m.messageId)");
// Add not null constraint now column has been populated
s.execute("ALTER TABLE messageMetadata"
+ " ALTER COLUMN state"
+ " SET NOT NULL");
} catch (SQLException e) {
tryToClose(s);
throw new DbException(e);
}
}
private void tryToClose(@Nullable Statement s) {
try {
if (s != null) s.close();
} catch (SQLException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}

View File

@@ -0,0 +1,36 @@
package org.briarproject.bramble.keyagreement;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import java.util.concurrent.Callable;
import javax.annotation.Nullable;
interface ConnectionChooser {
/**
* Submits a connection task to the chooser.
*/
void submit(Callable<KeyAgreementConnection> task);
/**
* Returns a connection returned by any of the tasks submitted to the
* chooser, waiting up to the given amount of time for a connection if
* necessary. Returns null if the time elapses without a connection
* becoming available.
*
* @param timeout the timeout in milliseconds
* @throws InterruptedException if the thread is interrupted while waiting
* for a connection to become available
*/
@Nullable
KeyAgreementConnection poll(long timeout) throws InterruptedException;
/**
* Stops the chooser. Any connections already returned to the chooser are
* closed unless they have been removed from the chooser by calling
* {@link #poll(long)}. Any connections subsequently returned to the
* chooser will also be closed.
*/
void stop();
}

View File

@@ -0,0 +1,112 @@
package org.briarproject.bramble.keyagreement;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
@NotNullByDefault
@ThreadSafe
class ConnectionChooserImpl implements ConnectionChooser {
private static final Logger LOG =
Logger.getLogger(ConnectionChooserImpl.class.getName());
private final Clock clock;
private final Executor ioExecutor;
private final Object lock = new Object();
// The following are locking: lock
private boolean stopped = false;
private final Queue<KeyAgreementConnection> results = new LinkedList<>();
@Inject
ConnectionChooserImpl(Clock clock, @IoExecutor Executor ioExecutor) {
this.clock = clock;
this.ioExecutor = ioExecutor;
}
@Override
public void submit(Callable<KeyAgreementConnection> task) {
ioExecutor.execute(() -> {
try {
KeyAgreementConnection c = task.call();
if (c != null) addResult(c);
} catch (Exception e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
});
}
@Nullable
@Override
public KeyAgreementConnection poll(long timeout)
throws InterruptedException {
long now = clock.currentTimeMillis();
long end = now + timeout;
synchronized (lock) {
while (!stopped && results.isEmpty() && now < end) {
lock.wait(end - now);
now = clock.currentTimeMillis();
}
return results.poll();
}
}
@Override
public void stop() {
List<KeyAgreementConnection> unused;
synchronized (lock) {
unused = new ArrayList<>(results);
results.clear();
stopped = true;
lock.notifyAll();
}
if (LOG.isLoggable(INFO))
LOG.info("Closing " + unused.size() + " unused connections");
for (KeyAgreementConnection c : unused) tryToClose(c.getConnection());
}
private void addResult(KeyAgreementConnection c) {
if (LOG.isLoggable(INFO))
LOG.info("Got connection for " + c.getTransportId());
boolean close = false;
synchronized (lock) {
if (stopped) {
close = true;
} else {
results.add(c);
lock.notifyAll();
}
}
if (close) {
LOG.info("Already stopped");
tryToClose(c.getConnection());
}
}
private void tryToClose(DuplexTransportConnection conn) {
try {
conn.getReader().dispose(false, true);
conn.getWriter().dispose(false);
} catch (IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}

View File

@@ -13,23 +13,19 @@ import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT;
@@ -45,29 +41,27 @@ class KeyAgreementConnector {
Logger.getLogger(KeyAgreementConnector.class.getName()); Logger.getLogger(KeyAgreementConnector.class.getName());
private final Callbacks callbacks; private final Callbacks callbacks;
private final Clock clock;
private final CryptoComponent crypto; private final CryptoComponent crypto;
private final PluginManager pluginManager; private final PluginManager pluginManager;
private final CompletionService<KeyAgreementConnection> connect; private final ConnectionChooser connectionChooser;
private final List<KeyAgreementListener> listeners = new ArrayList<>(); private final List<KeyAgreementListener> listeners =
private final List<Future<KeyAgreementConnection>> pending = new CopyOnWriteArrayList<>();
new ArrayList<>(); private final CountDownLatch aliceLatch = new CountDownLatch(1);
private final AtomicBoolean waitingSent = new AtomicBoolean(false);
private volatile boolean connecting = false; private volatile boolean alice = false, stopped = false;
private volatile boolean alice = false;
KeyAgreementConnector(Callbacks callbacks, Clock clock, KeyAgreementConnector(Callbacks callbacks,
CryptoComponent crypto, PluginManager pluginManager, CryptoComponent crypto, PluginManager pluginManager,
Executor ioExecutor) { ConnectionChooser connectionChooser) {
this.callbacks = callbacks; this.callbacks = callbacks;
this.clock = clock;
this.crypto = crypto; this.crypto = crypto;
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
connect = new ExecutorCompletionService<>(ioExecutor); this.connectionChooser = connectionChooser;
} }
public Payload listen(KeyPair localKeyPair) { Payload listen(KeyPair localKeyPair) {
LOG.info("Starting BQP listeners"); LOG.info("Starting BQP listeners");
// Derive commitment // Derive commitment
byte[] commitment = crypto.deriveKeyCommitment( byte[] commitment = crypto.deriveKeyCommitment(
@@ -80,8 +74,9 @@ class KeyAgreementConnector {
if (l != null) { if (l != null) {
TransportId id = plugin.getId(); TransportId id = plugin.getId();
descriptors.add(new TransportDescriptor(id, l.getDescriptor())); descriptors.add(new TransportDescriptor(id, l.getDescriptor()));
pending.add(connect.submit(new ReadableTask(l.listen()))); if (LOG.isLoggable(INFO)) LOG.info("Listening via " + id);
listeners.add(l); listeners.add(l);
connectionChooser.submit(new ReadableTask(l::accept));
} }
} }
return new Payload(commitment, descriptors); return new Payload(commitment, descriptors);
@@ -89,125 +84,92 @@ class KeyAgreementConnector {
void stopListening() { void stopListening() {
LOG.info("Stopping BQP listeners"); LOG.info("Stopping BQP listeners");
for (KeyAgreementListener l : listeners) { stopped = true;
l.close(); aliceLatch.countDown();
} for (KeyAgreementListener l : listeners) l.close();
listeners.clear(); connectionChooser.stop();
} }
@Nullable @Nullable
public KeyAgreementTransport connect(Payload remotePayload, public KeyAgreementTransport connect(Payload remotePayload, boolean alice) {
boolean alice) { // Let the ReadableTasks know if we are Alice
// Let the listeners know if we are Alice
this.connecting = true;
this.alice = alice; this.alice = alice;
long end = clock.currentTimeMillis() + CONNECTION_TIMEOUT; aliceLatch.countDown();
// Start connecting over supported transports // Start connecting over supported transports
LOG.info("Starting outgoing BQP connections"); if (LOG.isLoggable(INFO)) {
LOG.info("Starting outgoing BQP connections as "
+ (alice ? "Alice" : "Bob"));
}
for (TransportDescriptor d : remotePayload.getTransportDescriptors()) { for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
Plugin p = pluginManager.getPlugin(d.getId()); Plugin p = pluginManager.getPlugin(d.getId());
if (p instanceof DuplexPlugin) { if (p instanceof DuplexPlugin) {
if (LOG.isLoggable(INFO))
LOG.info("Connecting via " + d.getId());
DuplexPlugin plugin = (DuplexPlugin) p; DuplexPlugin plugin = (DuplexPlugin) p;
pending.add(connect.submit(new ReadableTask( byte[] commitment = remotePayload.getCommitment();
new ConnectorTask(plugin, remotePayload.getCommitment(), BdfList descriptor = d.getDescriptor();
d.getDescriptor(), end)))); connectionChooser.submit(new ReadableTask(
new ConnectorTask(plugin, commitment, descriptor)));
} }
} }
// Get chosen connection // Get chosen connection
KeyAgreementConnection chosen = null;
try { try {
long now = clock.currentTimeMillis(); KeyAgreementConnection chosen =
Future<KeyAgreementConnection> f = connectionChooser.poll(CONNECTION_TIMEOUT);
connect.poll(end - now, MILLISECONDS); if (chosen == null) return null;
if (f == null)
return null; // No task completed within the timeout.
chosen = f.get();
return new KeyAgreementTransport(chosen); return new KeyAgreementTransport(chosen);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.info("Interrupted while waiting for connection"); LOG.info("Interrupted while waiting for connection");
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return null; return null;
} catch (ExecutionException | IOException e) { } catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null; return null;
} finally { } finally {
stopListening(); stopListening();
// Close all other connections
closePending(chosen);
} }
} }
private void closePending(@Nullable KeyAgreementConnection chosen) { private void waitingForAlice() {
for (Future<KeyAgreementConnection> f : pending) { if (!waitingSent.getAndSet(true)) callbacks.connectionWaiting();
try {
if (f.cancel(true)) {
LOG.info("Cancelled task");
} else if (!f.isCancelled()) {
KeyAgreementConnection c = f.get();
if (c != null && c != chosen)
tryToClose(c.getConnection(), false);
}
} catch (InterruptedException e) {
LOG.info("Interrupted while closing sockets");
Thread.currentThread().interrupt();
return;
} catch (ExecutionException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
try {
if (LOG.isLoggable(INFO))
LOG.info("Closing connection, exception: " + exception);
conn.getReader().dispose(exception, true);
conn.getWriter().dispose(exception);
} catch (IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
} }
private class ConnectorTask implements Callable<KeyAgreementConnection> { private class ConnectorTask implements Callable<KeyAgreementConnection> {
private final byte[] commitment; private final byte[] commitment;
private final BdfList descriptor; private final BdfList descriptor;
private final long end;
private final DuplexPlugin plugin; private final DuplexPlugin plugin;
private ConnectorTask(DuplexPlugin plugin, byte[] commitment, private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
BdfList descriptor, long end) { BdfList descriptor) {
this.plugin = plugin; this.plugin = plugin;
this.commitment = commitment; this.commitment = commitment;
this.descriptor = descriptor; this.descriptor = descriptor;
this.end = end;
} }
@Nullable
@Override @Override
public KeyAgreementConnection call() throws Exception { public KeyAgreementConnection call() throws Exception {
// Repeat attempts until we connect, get interrupted, or time out // Repeat attempts until we connect, get stopped, or get interrupted
while (true) { while (!stopped) {
long now = clock.currentTimeMillis();
if (now > end) throw new IOException();
DuplexTransportConnection conn = DuplexTransportConnection conn =
plugin.createKeyAgreementConnection(commitment, plugin.createKeyAgreementConnection(commitment,
descriptor, end - now); descriptor);
if (conn != null) { if (conn != null) {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info(plugin.getId().getString() + LOG.info(plugin.getId() + ": Outgoing connection");
": Outgoing connection");
return new KeyAgreementConnection(conn, plugin.getId()); return new KeyAgreementConnection(conn, plugin.getId());
} }
// Wait 2s before retry (to circumvent transient failures) // Wait 2s before retry (to circumvent transient failures)
Thread.sleep(2000); Thread.sleep(2000);
} }
return null;
} }
} }
private class ReadableTask private class ReadableTask implements Callable<KeyAgreementConnection> {
implements Callable<KeyAgreementConnection> {
private final Callable<KeyAgreementConnection> connectionTask; private final Callable<KeyAgreementConnection> connectionTask;
@@ -215,24 +177,23 @@ class KeyAgreementConnector {
this.connectionTask = connectionTask; this.connectionTask = connectionTask;
} }
@Nullable
@Override @Override
public KeyAgreementConnection call() throws Exception { public KeyAgreementConnection call() throws Exception {
KeyAgreementConnection c = connectionTask.call(); KeyAgreementConnection c = connectionTask.call();
if (c == null) return null;
aliceLatch.await();
if (alice || stopped) return c;
// Bob waits here for Alice to scan his QR code, determine her
// role, and send her key
InputStream in = c.getConnection().getReader().getInputStream(); InputStream in = c.getConnection().getReader().getInputStream();
boolean waitingSent = false; while (!stopped && in.available() == 0) {
while (!alice && in.available() == 0) { if (LOG.isLoggable(INFO))
if (!waitingSent && connecting && !alice) { LOG.info(c.getTransportId() + ": Waiting for data");
// Bob waits here until Alice obtains his payload. waitingForAlice();
callbacks.connectionWaiting(); Thread.sleep(500);
waitingSent = true;
} }
if (LOG.isLoggable(INFO)) { if (!stopped && LOG.isLoggable(INFO))
LOG.info(c.getTransportId().getString() +
": Waiting for connection");
}
Thread.sleep(1000);
}
if (!alice && LOG.isLoggable(INFO))
LOG.info(c.getTransportId().getString() + ": Data available"); LOG.info(c.getTransportId().getString() + ": Data available");
return c; return c;
} }

View File

@@ -1,19 +1,10 @@
package org.briarproject.bramble.keyagreement; package org.briarproject.bramble.keyagreement;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.data.BdfReaderFactory; import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.BdfWriterFactory; import org.briarproject.bramble.api.data.BdfWriterFactory;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory; import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
import org.briarproject.bramble.api.keyagreement.PayloadEncoder; import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
import org.briarproject.bramble.api.keyagreement.PayloadParser; import org.briarproject.bramble.api.keyagreement.PayloadParser;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.system.Clock;
import java.util.concurrent.Executor;
import javax.inject.Singleton;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@@ -22,13 +13,9 @@ import dagger.Provides;
public class KeyAgreementModule { public class KeyAgreementModule {
@Provides @Provides
@Singleton KeyAgreementTaskFactory provideKeyAgreementTaskFactory(
KeyAgreementTaskFactory provideKeyAgreementTaskFactory(Clock clock, KeyAgreementTaskFactoryImpl keyAgreementTaskFactory) {
CryptoComponent crypto, EventBus eventBus, return keyAgreementTaskFactory;
@IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder,
PluginManager pluginManager) {
return new KeyAgreementTaskFactoryImpl(clock, crypto, eventBus,
ioExecutor, payloadEncoder, pluginManager);
} }
@Provides @Provides
@@ -40,4 +27,10 @@ public class KeyAgreementModule {
PayloadParser providePayloadParser(BdfReaderFactory bdfReaderFactory) { PayloadParser providePayloadParser(BdfReaderFactory bdfReaderFactory) {
return new PayloadParserImpl(bdfReaderFactory); return new PayloadParserImpl(bdfReaderFactory);
} }
@Provides
ConnectionChooser provideConnectionChooser(
ConnectionChooserImpl connectionChooser) {
return connectionChooser;
}
} }

View File

@@ -89,7 +89,8 @@ class KeyAgreementProtocol {
byte[] theirPublicKey; byte[] theirPublicKey;
if (alice) { if (alice) {
sendKey(); sendKey();
// Alice waits here until Bob obtains her payload. // Alice waits here for Bob to scan her QR code, determine his
// role, receive her key and respond with his key
callbacks.connectionWaiting(); callbacks.connectionWaiting();
theirPublicKey = receiveKey(); theirPublicKey = receiveKey();
} else { } else {

View File

@@ -5,42 +5,37 @@ import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.keyagreement.KeyAgreementTask; import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory; import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
import org.briarproject.bramble.api.keyagreement.PayloadEncoder; import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager; import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.system.Clock;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory { class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory {
private final Clock clock;
private final CryptoComponent crypto; private final CryptoComponent crypto;
private final EventBus eventBus; private final EventBus eventBus;
private final Executor ioExecutor;
private final PayloadEncoder payloadEncoder; private final PayloadEncoder payloadEncoder;
private final PluginManager pluginManager; private final PluginManager pluginManager;
private final Provider<ConnectionChooser> connectionChooserProvider;
@Inject @Inject
KeyAgreementTaskFactoryImpl(Clock clock, CryptoComponent crypto, KeyAgreementTaskFactoryImpl(CryptoComponent crypto, EventBus eventBus,
EventBus eventBus, @IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder, PluginManager pluginManager,
PayloadEncoder payloadEncoder, PluginManager pluginManager) { Provider<ConnectionChooser> connectionChooserProvider) {
this.clock = clock;
this.crypto = crypto; this.crypto = crypto;
this.eventBus = eventBus; this.eventBus = eventBus;
this.ioExecutor = ioExecutor;
this.payloadEncoder = payloadEncoder; this.payloadEncoder = payloadEncoder;
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.connectionChooserProvider = connectionChooserProvider;
} }
@Override @Override
public KeyAgreementTask createTask() { public KeyAgreementTask createTask() {
return new KeyAgreementTaskImpl(clock, crypto, eventBus, payloadEncoder, return new KeyAgreementTaskImpl(crypto, eventBus, payloadEncoder,
pluginManager, ioExecutor); pluginManager, connectionChooserProvider.get());
} }
} }

View File

@@ -17,19 +17,16 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
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.plugin.PluginManager; import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class KeyAgreementTaskImpl extends Thread implements class KeyAgreementTaskImpl extends Thread implements KeyAgreementTask,
KeyAgreementTask, KeyAgreementConnector.Callbacks, KeyAgreementProtocol.Callbacks, KeyAgreementConnector.Callbacks {
KeyAgreementProtocol.Callbacks {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(KeyAgreementTaskImpl.class.getName()); Logger.getLogger(KeyAgreementTaskImpl.class.getName());
@@ -43,15 +40,15 @@ class KeyAgreementTaskImpl extends Thread implements
private Payload localPayload; private Payload localPayload;
private Payload remotePayload; private Payload remotePayload;
KeyAgreementTaskImpl(Clock clock, CryptoComponent crypto, KeyAgreementTaskImpl(CryptoComponent crypto, EventBus eventBus,
EventBus eventBus, PayloadEncoder payloadEncoder, PayloadEncoder payloadEncoder, PluginManager pluginManager,
PluginManager pluginManager, Executor ioExecutor) { ConnectionChooser connectionChooser) {
this.crypto = crypto; this.crypto = crypto;
this.eventBus = eventBus; this.eventBus = eventBus;
this.payloadEncoder = payloadEncoder; this.payloadEncoder = payloadEncoder;
localKeyPair = crypto.generateAgreementKeyPair(); localKeyPair = crypto.generateAgreementKeyPair();
connector = new KeyAgreementConnector(this, clock, crypto, connector = new KeyAgreementConnector(this, crypto, pluginManager,
pluginManager, ioExecutor); connectionChooser);
} }
@Override @Override
@@ -65,10 +62,8 @@ class KeyAgreementTaskImpl extends Thread implements
@Override @Override
public synchronized void stopListening() { public synchronized void stopListening() {
if (localPayload != null) { if (localPayload != null) {
if (remotePayload == null) if (remotePayload == null) connector.stopListening();
connector.stopListening(); else interrupt();
else
interrupt();
} }
} }

View File

@@ -2,8 +2,11 @@ package org.briarproject.bramble.lifecycle;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyPair; import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.db.DataTooNewException;
import org.briarproject.bramble.api.db.DataTooOldException;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.AuthorFactory; import org.briarproject.bramble.api.identity.AuthorFactory;
@@ -12,7 +15,7 @@ import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.lifecycle.Service; import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Client; import org.briarproject.bramble.api.sync.Client;
@@ -29,14 +32,21 @@ import javax.inject.Inject;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_NEW_ERROR;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_ERROR;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DB_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DB_ERROR;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS;
@ThreadSafe @ThreadSafe
@NotNullByDefault @NotNullByDefault
class LifecycleManagerImpl implements LifecycleManager { class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(LifecycleManagerImpl.class.getName()); Logger.getLogger(LifecycleManagerImpl.class.getName());
@@ -54,6 +64,8 @@ class LifecycleManagerImpl implements LifecycleManager {
private final CountDownLatch startupLatch = new CountDownLatch(1); private final CountDownLatch startupLatch = new CountDownLatch(1);
private final CountDownLatch shutdownLatch = new CountDownLatch(1); private final CountDownLatch shutdownLatch = new CountDownLatch(1);
private volatile LifecycleState state = STARTING;
@Inject @Inject
LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus, LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus,
CryptoComponent crypto, AuthorFactory authorFactory, CryptoComponent crypto, AuthorFactory authorFactory,
@@ -119,7 +131,7 @@ class LifecycleManagerImpl implements LifecycleManager {
LOG.info("Starting services"); LOG.info("Starting services");
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
boolean reopened = db.open(); boolean reopened = db.open(this);
long duration = System.currentTimeMillis() - start; long duration = System.currentTimeMillis() - start;
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
if (reopened) if (reopened)
@@ -131,7 +143,10 @@ class LifecycleManagerImpl implements LifecycleManager {
registerLocalAuthor(createLocalAuthor(nickname)); registerLocalAuthor(createLocalAuthor(nickname));
} }
state = STARTING_SERVICES;
dbLatch.countDown(); dbLatch.countDown();
eventBus.broadcast(new LifecycleEvent(STARTING_SERVICES));
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
for (Client c : clients) { for (Client c : clients) {
@@ -157,8 +172,17 @@ class LifecycleManagerImpl implements LifecycleManager {
+ " took " + duration + " ms"); + " took " + duration + " ms");
} }
} }
state = RUNNING;
startupLatch.countDown(); startupLatch.countDown();
eventBus.broadcast(new LifecycleEvent(RUNNING));
return SUCCESS; return SUCCESS;
} catch (DataTooOldException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return DATA_TOO_OLD_ERROR;
} catch (DataTooNewException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return DATA_TOO_NEW_ERROR;
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return DB_ERROR; return DB_ERROR;
@@ -170,6 +194,12 @@ class LifecycleManagerImpl implements LifecycleManager {
} }
} }
@Override
public void onMigrationRun() {
state = MIGRATING_DATABASE;
eventBus.broadcast(new LifecycleEvent(MIGRATING_DATABASE));
}
@Override @Override
public void stopServices() { public void stopServices() {
try { try {
@@ -180,7 +210,8 @@ class LifecycleManagerImpl implements LifecycleManager {
} }
try { try {
LOG.info("Stopping services"); LOG.info("Stopping services");
eventBus.broadcast(new ShutdownEvent()); state = STOPPING;
eventBus.broadcast(new LifecycleEvent(STOPPING));
for (Service s : services) { for (Service s : services) {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
s.stopService(); s.stopService();
@@ -225,4 +256,8 @@ class LifecycleManagerImpl implements LifecycleManager {
shutdownLatch.await(); shutdownLatch.await();
} }
@Override
public LifecycleState getLifecycleState() {
return state;
}
} }

View File

@@ -16,6 +16,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent; import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent; import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
@@ -25,6 +26,7 @@ import java.security.SecureRandom;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -50,7 +52,7 @@ class Poller implements EventListener {
private final SecureRandom random; private final SecureRandom random;
private final Clock clock; private final Clock clock;
private final Lock lock; private final Lock lock;
private final Map<TransportId, PollTask> tasks; // Locking: lock private final Map<TransportId, ScheduledPollTask> tasks; // Locking: lock
@Inject @Inject
Poller(@IoExecutor Executor ioExecutor, Poller(@IoExecutor Executor ioExecutor,
@@ -93,6 +95,10 @@ class Poller implements EventListener {
TransportEnabledEvent t = (TransportEnabledEvent) e; TransportEnabledEvent t = (TransportEnabledEvent) e;
// Poll the newly enabled transport // Poll the newly enabled transport
pollNow(t.getTransportId()); pollNow(t.getTransportId());
} else if (e instanceof TransportDisabledEvent) {
TransportDisabledEvent t = (TransportDisabledEvent) e;
// Cancel polling for the disabled transport
cancel(t.getTransportId());
} }
} }
@@ -151,18 +157,31 @@ class Poller implements EventListener {
TransportId t = p.getId(); TransportId t = p.getId();
lock.lock(); lock.lock();
try { try {
PollTask scheduled = tasks.get(t); ScheduledPollTask scheduled = tasks.get(t);
if (scheduled == null || due < scheduled.due) { if (scheduled == null || due < scheduled.task.due) {
// If a later task exists, cancel it. If it's already started
// it will abort safely when it finds it's been replaced
if (scheduled != null) scheduled.future.cancel(false);
PollTask task = new PollTask(p, due, randomiseNext); PollTask task = new PollTask(p, due, randomiseNext);
tasks.put(t, task); Future future = scheduler.schedule(
scheduler.schedule(
() -> ioExecutor.execute(task), delay, MILLISECONDS); () -> ioExecutor.execute(task), delay, MILLISECONDS);
tasks.put(t, new ScheduledPollTask(task, future));
} }
} finally { } finally {
lock.unlock(); lock.unlock();
} }
} }
private void cancel(TransportId t) {
lock.lock();
try {
ScheduledPollTask scheduled = tasks.remove(t);
if (scheduled != null) scheduled.future.cancel(false);
} finally {
lock.unlock();
}
}
@IoExecutor @IoExecutor
private void poll(Plugin p) { private void poll(Plugin p) {
TransportId t = p.getId(); TransportId t = p.getId();
@@ -170,6 +189,17 @@ class Poller implements EventListener {
p.poll(connectionRegistry.getConnectedContacts(t)); p.poll(connectionRegistry.getConnectedContacts(t));
} }
private class ScheduledPollTask {
private final PollTask task;
private final Future future;
private ScheduledPollTask(PollTask task, Future future) {
this.task = task;
this.future = future;
}
}
private class PollTask implements Runnable { private class PollTask implements Runnable {
private final Plugin plugin; private final Plugin plugin;
@@ -188,7 +218,9 @@ class Poller implements EventListener {
lock.lock(); lock.lock();
try { try {
TransportId t = plugin.getId(); TransportId t = plugin.getId();
if (tasks.get(t) != this) return; // Replaced by another task ScheduledPollTask scheduled = tasks.get(t);
if (scheduled != null && scheduled.task != this)
return; // Replaced by another task
tasks.remove(t); tasks.remove(t);
} finally { } finally {
lock.unlock(); lock.unlock();

View File

@@ -3,6 +3,8 @@ package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection; import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener; import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -13,8 +15,11 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent;
import org.briarproject.bramble.api.plugin.event.DisableBluetoothEvent;
import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.util.OsUtils; import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import java.io.IOException; import java.io.IOException;
@@ -23,30 +28,25 @@ import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static javax.bluetooth.DiscoveryAgent.GIAC;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID; import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS; import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID; import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES; import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class BluetoothPlugin implements DuplexPlugin { abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(BluetoothPlugin.class.getName()); Logger.getLogger(BluetoothPlugin.class.getName());
@@ -58,9 +58,38 @@ class BluetoothPlugin implements DuplexPlugin {
private final int maxLatency; private final int maxLatency;
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false; private volatile boolean running = false, contactConnections = false;
private volatile StreamConnectionNotifier socket = null; private volatile String contactConnectionsUuid = null;
private volatile LocalDevice localDevice = null; private volatile SS socket = null;
abstract void initialiseAdapter() throws IOException;
abstract boolean isAdapterEnabled();
abstract void enableAdapter();
abstract void disableAdapterIfEnabledByUs();
abstract void setEnabledByUs();
/**
* Returns the local Bluetooth address, or null if no valid address can
* be found.
*/
@Nullable
abstract String getBluetoothAddress();
abstract SS openServerSocket(String uuid) throws IOException;
abstract void tryToClose(@Nullable SS ss);
abstract DuplexTransportConnection acceptConnection(SS ss)
throws IOException;
abstract boolean isValidAddress(String address);
abstract DuplexTransportConnection connectTo(String address, String uuid)
throws IOException;
BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom, BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom,
Backoff backoff, DuplexPluginCallback callback, int maxLatency) { Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
@@ -71,6 +100,19 @@ class BluetoothPlugin implements DuplexPlugin {
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
} }
void onAdapterEnabled() {
LOG.info("Bluetooth enabled");
// We may not have been able to get the local address before
ioExecutor.execute(this::updateProperties);
if (shouldAllowContactConnections()) bind();
}
void onAdapterDisabled() {
LOG.info("Bluetooth disabled");
tryToClose(socket);
callback.transportDisabled();
}
@Override @Override
public TransportId getId() { public TransportId getId() {
return ID; return ID;
@@ -90,107 +132,103 @@ class BluetoothPlugin implements DuplexPlugin {
@Override @Override
public void start() throws PluginException { public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException(); if (used.getAndSet(true)) throw new IllegalStateException();
// Initialise the Bluetooth stack
try { try {
localDevice = LocalDevice.getLocalDevice(); initialiseAdapter();
} catch (UnsatisfiedLinkError e) { } catch (IOException e) {
// On Linux the user may need to install libbluetooth-dev
if (OsUtils.isLinux())
callback.showMessage("BLUETOOTH_INSTALL_LIBS");
throw new PluginException(e);
} catch (BluetoothStateException e) {
throw new PluginException(e); throw new PluginException(e);
} }
if (LOG.isLoggable(INFO)) updateProperties();
LOG.info("Local address " + localDevice.getBluetoothAddress());
running = true; running = true;
bind(); loadSettings();
if (shouldAllowContactConnections()) {
if (isAdapterEnabled()) bind();
else enableAdapter();
}
}
private void loadSettings() {
contactConnections =
callback.getSettings().getBoolean(PREF_BT_ENABLE, false);
}
private boolean shouldAllowContactConnections() {
return contactConnections;
} }
private void bind() { private void bind() {
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
if (!running) return; if (!isRunning() || !shouldAllowContactConnections()) return;
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put(PROP_ADDRESS, localDevice.getBluetoothAddress());
callback.mergeLocalProperties(p);
// Bind a server socket to accept connections from contacts // Bind a server socket to accept connections from contacts
String url = makeUrl("localhost", getUuid()); SS ss;
StreamConnectionNotifier ss;
try { try {
ss = (StreamConnectionNotifier) Connector.open(url); ss = openServerSocket(contactConnectionsUuid);
} catch (IOException e) { } catch (IOException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
LOG.log(WARNING, e.toString(), e);
return; return;
} }
if (!running) { if (!isRunning() || !shouldAllowContactConnections()) {
tryToClose(ss); tryToClose(ss);
return; return;
} }
socket = ss; socket = ss;
backoff.reset(); backoff.reset();
callback.transportEnabled(); callback.transportEnabled();
acceptContactConnections(ss); acceptContactConnections();
}); });
} }
private String makeUrl(String address, String uuid) { private void updateProperties() {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM"; TransportProperties p = callback.getLocalProperties();
String address = p.get(PROP_ADDRESS);
String uuid = p.get(PROP_UUID);
boolean changed = false;
if (address == null) {
address = getBluetoothAddress();
if (LOG.isLoggable(INFO))
LOG.info("Local address " + scrubMacAddress(address));
if (!StringUtils.isNullOrEmpty(address)) {
p.put(PROP_ADDRESS, address);
changed = true;
}
} }
private String getUuid() {
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) { if (uuid == null) {
byte[] random = new byte[UUID_BYTES]; byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random); secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString(); uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put(PROP_UUID, uuid); p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p); changed = true;
} }
return uuid; contactConnectionsUuid = uuid;
if (changed) callback.mergeLocalProperties(p);
} }
private void tryToClose(@Nullable StreamConnectionNotifier ss) { private void acceptContactConnections() {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} finally {
callback.transportDisabled();
}
}
private void acceptContactConnections(StreamConnectionNotifier ss) {
while (true) { while (true) {
StreamConnection s; DuplexTransportConnection conn;
try { try {
s = ss.acceptAndOpen(); conn = acceptConnection(socket);
} catch (IOException e) { } catch (IOException e) {
// This is expected when the socket is closed // This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString()); if (LOG.isLoggable(INFO)) LOG.info(e.toString());
return; return;
} }
backoff.reset(); backoff.reset();
callback.incomingConnectionCreated(wrapSocket(s)); callback.incomingConnectionCreated(conn);
if (!running) return; if (!running) return;
} }
} }
private DuplexTransportConnection wrapSocket(StreamConnection s) {
return new BluetoothTransportConnection(this, s);
}
@Override @Override
public void stop() { public void stop() {
running = false; running = false;
tryToClose(socket); tryToClose(socket);
callback.transportDisabled();
disableAdapterIfEnabledByUs();
} }
@Override @Override
public boolean isRunning() { public boolean isRunning() {
return running; return running && isAdapterEnabled();
} }
@Override @Override
@@ -205,7 +243,7 @@ class BluetoothPlugin implements DuplexPlugin {
@Override @Override
public void poll(Collection<ContactId> connected) { public void poll(Collection<ContactId> connected) {
if (!running) return; if (!isRunning() || !shouldAllowContactConnections()) return;
backoff.increment(); backoff.increment();
// Try to connect to known devices in parallel // Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote = Map<ContactId, TransportProperties> remote =
@@ -218,41 +256,56 @@ class BluetoothPlugin implements DuplexPlugin {
String uuid = e.getValue().get(PROP_UUID); String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue; if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(() -> { ioExecutor.execute(() -> {
if (!running) return; if (!isRunning() || !shouldAllowContactConnections()) return;
StreamConnection s = connect(makeUrl(address, uuid)); DuplexTransportConnection conn = connect(address, uuid);
if (s != null) { if (conn != null) {
backoff.reset(); backoff.reset();
callback.outgoingConnectionCreated(c, wrapSocket(s)); callback.outgoingConnectionCreated(c, conn);
} }
}); });
} }
} }
@Nullable @Nullable
private StreamConnection connect(String url) { private DuplexTransportConnection connect(String address, String uuid) {
if (LOG.isLoggable(INFO)) LOG.info("Connecting to " + url); // Validate the address
if (!isValidAddress(address)) {
if (LOG.isLoggable(WARNING))
// Not scrubbing here to be able to figure out the problem
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
try { try {
StreamConnection s = (StreamConnection) Connector.open(url); //noinspection ResultOfMethodCallIgnored
if (LOG.isLoggable(INFO)) LOG.info("Connected to " + url); UUID.fromString(uuid);
return s; } catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
try {
DuplexTransportConnection conn = connectTo(address, uuid);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubMacAddress(address));
return conn;
} catch (IOException e) { } catch (IOException e) {
if (LOG.isLoggable(INFO)) LOG.info("Could not connect to " + url); if (LOG.isLoggable(INFO))
LOG.info("Could not connect to " + scrubMacAddress(address));
return null; return null;
} }
} }
@Override @Override
public DuplexTransportConnection createConnection(ContactId c) { public DuplexTransportConnection createConnection(ContactId c) {
if (!running) return null; if (!isRunning() || !shouldAllowContactConnections()) return null;
TransportProperties p = callback.getRemoteProperties(c); TransportProperties p = callback.getRemoteProperties(c);
String address = p.get(PROP_ADDRESS); String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null; if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID); String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null; if (StringUtils.isNullOrEmpty(uuid)) return null;
String url = makeUrl(address, uuid); return connect(address, uuid);
StreamConnection s = connect(url);
if (s == null) return null;
return new BluetoothTransportConnection(this, s);
} }
@Override @Override
@@ -262,35 +315,34 @@ class BluetoothPlugin implements DuplexPlugin {
@Override @Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) { public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (!running) return null; if (!isRunning()) return null;
// There's no point listening if we can't discover our own address
String address = getBluetoothAddress();
if (address == null) return null;
// No truncation necessary because COMMIT_LENGTH = 16 // No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString(); String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid); if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
String url = makeUrl("localhost", uuid);
// Make the device discoverable if possible
makeDeviceDiscoverable();
// Bind a server socket for receiving key agreement connections // Bind a server socket for receiving key agreement connections
StreamConnectionNotifier ss; SS ss;
try { try {
ss = (StreamConnectionNotifier) Connector.open(url); ss = openServerSocket(uuid);
} catch (IOException e) { } catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null; return null;
} }
if (!running) { if (!isRunning()) {
tryToClose(ss); tryToClose(ss);
return null; return null;
} }
BdfList descriptor = new BdfList(); BdfList descriptor = new BdfList();
descriptor.add(TRANSPORT_ID_BLUETOOTH); descriptor.add(TRANSPORT_ID_BLUETOOTH);
String address = localDevice.getBluetoothAddress();
descriptor.add(StringUtils.macToBytes(address)); descriptor.add(StringUtils.macToBytes(address));
return new BluetoothKeyAgreementListener(descriptor, ss); return new BluetoothKeyAgreementListener(descriptor, ss);
} }
@Override @Override
public DuplexTransportConnection createKeyAgreementConnection( public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) { byte[] commitment, BdfList descriptor) {
if (!isRunning()) return null; if (!isRunning()) return null;
String address; String address;
try { try {
@@ -303,10 +355,7 @@ class BluetoothPlugin implements DuplexPlugin {
String uuid = UUID.nameUUIDFromBytes(commitment).toString(); String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid); LOG.info("Connecting to key agreement UUID " + uuid);
String url = makeUrl(address, uuid); return connect(address, uuid);
StreamConnection s = connect(url);
if (s == null) return null;
return new BluetoothTransportConnection(this, s);
} }
private String parseAddress(BdfList descriptor) throws FormatException { private String parseAddress(BdfList descriptor) throws FormatException {
@@ -315,44 +364,56 @@ class BluetoothPlugin implements DuplexPlugin {
return StringUtils.macToString(mac); return StringUtils.macToString(mac);
} }
private void makeDeviceDiscoverable() { @Override
// Try to make the device discoverable (requires root on Linux) public void eventOccurred(Event e) {
try { if (e instanceof EnableBluetoothEvent) {
localDevice.setDiscoverable(GIAC); ioExecutor.execute(this::enableAdapter);
} catch (BluetoothStateException e) { } else if (e instanceof DisableBluetoothEvent) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); ioExecutor.execute(this::disableAdapterIfEnabledByUs);
} else if (e instanceof BluetoothEnabledEvent) {
setEnabledByUs();
} else if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(ID.getString()))
ioExecutor.execute(this::onSettingsUpdated);
}
}
private void onSettingsUpdated() {
boolean wasAllowed = shouldAllowContactConnections();
loadSettings();
boolean isAllowed = shouldAllowContactConnections();
if (wasAllowed && !isAllowed) {
LOG.info("Contact connections disabled");
tryToClose(socket);
callback.transportDisabled();
disableAdapterIfEnabledByUs();
} else if (!wasAllowed && isAllowed) {
LOG.info("Contact connections enabled");
if (isAdapterEnabled()) bind();
else enableAdapter();
} }
} }
private class BluetoothKeyAgreementListener extends KeyAgreementListener { private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final StreamConnectionNotifier ss; private final SS ss;
private BluetoothKeyAgreementListener(BdfList descriptor, private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
StreamConnectionNotifier ss) {
super(descriptor); super(descriptor);
this.ss = ss; this.ss = ss;
} }
@Override @Override
public Callable<KeyAgreementConnection> listen() { public KeyAgreementConnection accept() throws IOException {
return () -> { DuplexTransportConnection conn = acceptConnection(ss);
StreamConnection s = ss.acceptAndOpen(); if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
if (LOG.isLoggable(INFO)) return new KeyAgreementConnection(conn, ID);
LOG.info(ID.getString() + ": Incoming connection");
return new KeyAgreementConnection(
new BluetoothTransportConnection(
BluetoothPlugin.this, s), ID);
};
} }
@Override @Override
public void close() { public void close() {
try { tryToClose(ss);
ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
} }
} }
} }

View File

@@ -23,9 +23,8 @@ import java.net.SocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -44,6 +43,9 @@ class LanTcpPlugin extends TcpPlugin {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(LanTcpPlugin.class.getName()); Logger.getLogger(LanTcpPlugin.class.getName());
private static final LanAddressComparator ADDRESS_COMPARATOR =
new LanAddressComparator();
private static final int MAX_ADDRESSES = 4; private static final int MAX_ADDRESSES = 4;
private static final String SEPARATOR = ","; private static final String SEPARATOR = ",";
@@ -63,19 +65,18 @@ class LanTcpPlugin extends TcpPlugin {
TransportProperties p = callback.getLocalProperties(); TransportProperties p = callback.getLocalProperties();
String oldIpPorts = p.get(PROP_IP_PORTS); String oldIpPorts = p.get(PROP_IP_PORTS);
List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts); List<InetSocketAddress> olds = parseSocketAddresses(oldIpPorts);
List<InetSocketAddress> locals = new LinkedList<>(); List<InetSocketAddress> locals = new ArrayList<>();
for (InetAddress local : getLocalIpAddresses()) { for (InetAddress local : getLocalIpAddresses()) {
if (isAcceptableAddress(local)) { if (isAcceptableAddress(local)) {
// If this is the old address, try to use the same port // If this is the old address, try to use the same port
for (InetSocketAddress old : olds) { for (InetSocketAddress old : olds) {
if (old.getAddress().equals(local)) { if (old.getAddress().equals(local))
int port = old.getPort(); locals.add(new InetSocketAddress(local, old.getPort()));
locals.add(0, new InetSocketAddress(local, port));
}
} }
locals.add(new InetSocketAddress(local, 0)); locals.add(new InetSocketAddress(local, 0));
} }
} }
Collections.sort(locals, ADDRESS_COMPARATOR);
return locals; return locals;
} }
@@ -153,17 +154,39 @@ class LanTcpPlugin extends TcpPlugin {
// Package access for testing // Package access for testing
boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) { boolean addressesAreOnSameLan(byte[] localIp, byte[] remoteIp) {
// 10.0.0.0/8 // 10.0.0.0/8
if (localIp[0] == 10) return remoteIp[0] == 10; if (isPrefix10(localIp)) return isPrefix10(remoteIp);
// 172.16.0.0/12 // 172.16.0.0/12
if (localIp[0] == (byte) 172 && (localIp[1] & 0xF0) == 16) if (isPrefix172(localIp)) return isPrefix172(remoteIp);
return remoteIp[0] == (byte) 172 && (remoteIp[1] & 0xF0) == 16;
// 192.168.0.0/16 // 192.168.0.0/16
if (localIp[0] == (byte) 192 && localIp[1] == (byte) 168) if (isPrefix192(localIp)) return isPrefix192(remoteIp);
return remoteIp[0] == (byte) 192 && remoteIp[1] == (byte) 168;
// Unrecognised prefix - may be compatible // Unrecognised prefix - may be compatible
return true; return true;
} }
private static boolean isPrefix10(byte[] ipv4) {
return ipv4[0] == 10;
}
private static boolean isPrefix172(byte[] ipv4) {
return ipv4[0] == (byte) 172 && (ipv4[1] & 0xF0) == 16;
}
private static boolean isPrefix192(byte[] ipv4) {
return ipv4[0] == (byte) 192 && ipv4[1] == (byte) 168;
}
// Returns the prefix length for an RFC 1918 address, or 0 for any other
// address
private static int getRfc1918PrefixLength(InetAddress addr) {
if (!(addr instanceof Inet4Address)) return 0;
if (!addr.isSiteLocalAddress()) return 0;
byte[] ipv4 = addr.getAddress();
if (isPrefix10(ipv4)) return 8;
if (isPrefix172(ipv4)) return 12;
if (isPrefix192(ipv4)) return 16;
return 0;
}
@Override @Override
public boolean supportsKeyAgreement() { public boolean supportsKeyAgreement() {
return true; return true;
@@ -200,7 +223,7 @@ class LanTcpPlugin extends TcpPlugin {
@Override @Override
public DuplexTransportConnection createKeyAgreementConnection( public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) { byte[] commitment, BdfList descriptor) {
if (!isRunning()) return null; if (!isRunning()) return null;
InetSocketAddress remote; InetSocketAddress remote;
try { try {
@@ -259,14 +282,11 @@ class LanTcpPlugin extends TcpPlugin {
} }
@Override @Override
public Callable<KeyAgreementConnection> listen() { public KeyAgreementConnection accept() throws IOException {
return () -> {
Socket s = ss.accept(); Socket s = ss.accept();
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
LOG.info(ID.getString() + ": Incoming connection"); return new KeyAgreementConnection(new TcpTransportConnection(
return new KeyAgreementConnection( LanTcpPlugin.this, s), ID);
new TcpTransportConnection(LanTcpPlugin.this, s), ID);
};
} }
@Override @Override
@@ -278,4 +298,19 @@ class LanTcpPlugin extends TcpPlugin {
} }
} }
} }
static class LanAddressComparator implements Comparator<InetSocketAddress> {
@Override
public int compare(InetSocketAddress a, InetSocketAddress b) {
// Prefer addresses with non-zero ports
int aPort = a.getPort(), bPort = b.getPort();
if (aPort > 0 && bPort == 0) return -1;
if (aPort == 0 && bPort > 0) return 1;
// Prefer addresses with longer RFC 1918 prefixes
int aPrefix = getRfc1918PrefixLength(a.getAddress());
int bPrefix = getRfc1918PrefixLength(b.getAddress());
return bPrefix - aPrefix;
}
}
} }

View File

@@ -297,7 +297,7 @@ abstract class TcpPlugin implements DuplexPlugin {
@Override @Override
public DuplexTransportConnection createKeyAgreementConnection( public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) { byte[] commitment, BdfList descriptor) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@@ -63,6 +63,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
@Override @Override
public void createLocalState(Transaction txn) throws DbException { public void createLocalState(Transaction txn) throws DbException {
if (db.containsGroup(txn, localGroup.getId())) return;
db.addGroup(txn, localGroup); db.addGroup(txn, localGroup);
// Ensure we've set things up for any pre-existing contacts // Ensure we've set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) addingContact(txn, c); for (Contact c : db.getContacts(txn)) addingContact(txn, c);

View File

@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Offer; import org.briarproject.bramble.api.sync.Offer;
@@ -29,6 +29,8 @@ import java.util.Collection;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
@@ -36,6 +38,7 @@ import javax.annotation.concurrent.ThreadSafe;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
@@ -49,12 +52,14 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD
@NotNullByDefault @NotNullByDefault
class DuplexOutgoingSession implements SyncSession, EventListener { class DuplexOutgoingSession implements SyncSession, EventListener {
// Check for retransmittable records once every 60 seconds
private static final int RETX_QUERY_INTERVAL = 60 * 1000;
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(DuplexOutgoingSession.class.getName()); Logger.getLogger(DuplexOutgoingSession.class.getName());
private static final ThrowingRunnable<IOException> CLOSE = () -> {}; private static final ThrowingRunnable<IOException> CLOSE = () -> {
};
private static final ThrowingRunnable<IOException>
NEXT_SEND_TIME_DECREASED = () -> {
};
private final DatabaseComponent db; private final DatabaseComponent db;
private final Executor dbExecutor; private final Executor dbExecutor;
@@ -65,6 +70,13 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private final RecordWriter recordWriter; private final RecordWriter recordWriter;
private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks; private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
private final AtomicBoolean generateAckQueued = new AtomicBoolean(false);
private final AtomicBoolean generateBatchQueued = new AtomicBoolean(false);
private final AtomicBoolean generateOfferQueued = new AtomicBoolean(false);
private final AtomicBoolean generateRequestQueued =
new AtomicBoolean(false);
private final AtomicLong nextSendTime = new AtomicLong(Long.MAX_VALUE);
private volatile boolean interrupted = false; private volatile boolean interrupted = false;
DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor, DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
@@ -87,21 +99,21 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
eventBus.addListener(this); eventBus.addListener(this);
try { try {
// Start a query for each type of record // Start a query for each type of record
dbExecutor.execute(new GenerateAck()); generateAck();
dbExecutor.execute(new GenerateBatch()); generateBatch();
dbExecutor.execute(new GenerateOffer()); generateOffer();
dbExecutor.execute(new GenerateRequest()); generateRequest();
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
long nextKeepalive = now + maxIdleTime; long nextKeepalive = now + maxIdleTime;
long nextRetxQuery = now + RETX_QUERY_INTERVAL;
boolean dataToFlush = true; boolean dataToFlush = true;
// Write records until interrupted // Write records until interrupted
try { try {
while (!interrupted) { while (!interrupted) {
// Work out how long we should wait for a record // Work out how long we should wait for a record
now = clock.currentTimeMillis(); now = clock.currentTimeMillis();
long wait = Math.min(nextKeepalive, nextRetxQuery) - now; long keepaliveWait = Math.max(0, nextKeepalive - now);
if (wait < 0) wait = 0; long sendWait = Math.max(0, nextSendTime.get() - now);
long wait = Math.min(keepaliveWait, sendWait);
// Flush any unflushed data if we're going to wait // Flush any unflushed data if we're going to wait
if (wait > 0 && dataToFlush && writerTasks.isEmpty()) { if (wait > 0 && dataToFlush && writerTasks.isEmpty()) {
recordWriter.flush(); recordWriter.flush();
@@ -113,20 +125,25 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
MILLISECONDS); MILLISECONDS);
if (task == null) { if (task == null) {
now = clock.currentTimeMillis(); now = clock.currentTimeMillis();
if (now >= nextRetxQuery) { if (now >= nextSendTime.get()) {
// Check for retransmittable records // Check for retransmittable messages
dbExecutor.execute(new GenerateBatch()); LOG.info("Checking for retransmittable messages");
dbExecutor.execute(new GenerateOffer()); setNextSendTime(Long.MAX_VALUE);
nextRetxQuery = now + RETX_QUERY_INTERVAL; generateBatch();
generateOffer();
} }
if (now >= nextKeepalive) { if (now >= nextKeepalive) {
// Flush the stream to keep it alive // Flush the stream to keep it alive
LOG.info("Sending keepalive");
recordWriter.flush(); recordWriter.flush();
dataToFlush = false; dataToFlush = false;
nextKeepalive = now + maxIdleTime; nextKeepalive = now + maxIdleTime;
} }
} else if (task == CLOSE) { } else if (task == CLOSE) {
LOG.info("Closed");
break; break;
} else if (task == NEXT_SEND_TIME_DECREASED) {
LOG.info("Next send time decreased");
} else { } else {
task.run(); task.run();
dataToFlush = true; dataToFlush = true;
@@ -142,6 +159,31 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
} }
} }
private void generateAck() {
if (generateAckQueued.compareAndSet(false, true))
dbExecutor.execute(new GenerateAck());
}
private void generateBatch() {
if (generateBatchQueued.compareAndSet(false, true))
dbExecutor.execute(new GenerateBatch());
}
private void generateOffer() {
if (generateOfferQueued.compareAndSet(false, true))
dbExecutor.execute(new GenerateOffer());
}
private void generateRequest() {
if (generateRequestQueued.compareAndSet(false, true))
dbExecutor.execute(new GenerateRequest());
}
private void setNextSendTime(long time) {
long old = nextSendTime.getAndSet(time);
if (time < old) writerTasks.add(NEXT_SEND_TIME_DECREASED);
}
@Override @Override
public void interrupt() { public void interrupt() {
interrupted = true; interrupted = true;
@@ -154,22 +196,23 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) interrupt(); if (c.getContactId().equals(contactId)) interrupt();
} else if (e instanceof MessageSharedEvent) { } else if (e instanceof MessageSharedEvent) {
dbExecutor.execute(new GenerateOffer()); generateOffer();
} else if (e instanceof GroupVisibilityUpdatedEvent) { } else if (e instanceof GroupVisibilityUpdatedEvent) {
GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e; GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
if (g.getAffectedContacts().contains(contactId)) if (g.getAffectedContacts().contains(contactId))
dbExecutor.execute(new GenerateOffer()); generateOffer();
} else if (e instanceof MessageRequestedEvent) { } else if (e instanceof MessageRequestedEvent) {
if (((MessageRequestedEvent) e).getContactId().equals(contactId)) if (((MessageRequestedEvent) e).getContactId().equals(contactId))
dbExecutor.execute(new GenerateBatch()); generateBatch();
} else if (e instanceof MessageToAckEvent) { } else if (e instanceof MessageToAckEvent) {
if (((MessageToAckEvent) e).getContactId().equals(contactId)) if (((MessageToAckEvent) e).getContactId().equals(contactId))
dbExecutor.execute(new GenerateAck()); generateAck();
} else if (e instanceof MessageToRequestEvent) { } else if (e instanceof MessageToRequestEvent) {
if (((MessageToRequestEvent) e).getContactId().equals(contactId)) if (((MessageToRequestEvent) e).getContactId().equals(contactId))
dbExecutor.execute(new GenerateRequest()); generateRequest();
} else if (e instanceof ShutdownEvent) { } else if (e instanceof LifecycleEvent) {
interrupt(); LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt();
} }
} }
@@ -179,6 +222,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override @Override
public void run() { public void run() {
if (interrupted) return; if (interrupted) return;
if (!generateAckQueued.getAndSet(false)) throw new AssertionError();
try { try {
Ack a; Ack a;
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
@@ -212,7 +256,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
if (interrupted) return; if (interrupted) return;
recordWriter.writeAck(ack); recordWriter.writeAck(ack);
LOG.info("Sent ack"); LOG.info("Sent ack");
dbExecutor.execute(new GenerateAck()); generateAck();
} }
} }
@@ -222,12 +266,15 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override @Override
public void run() { public void run() {
if (interrupted) return; if (interrupted) return;
if (!generateBatchQueued.getAndSet(false))
throw new AssertionError();
try { try {
Collection<byte[]> b; Collection<byte[]> b;
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
b = db.generateRequestedBatch(txn, contactId, b = db.generateRequestedBatch(txn, contactId,
MAX_RECORD_PAYLOAD_LENGTH, maxLatency); MAX_RECORD_PAYLOAD_LENGTH, maxLatency);
setNextSendTime(db.getNextSendTime(txn, contactId));
db.commitTransaction(txn); db.commitTransaction(txn);
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
@@ -256,7 +303,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
if (interrupted) return; if (interrupted) return;
for (byte[] raw : batch) recordWriter.writeMessage(raw); for (byte[] raw : batch) recordWriter.writeMessage(raw);
LOG.info("Sent batch"); LOG.info("Sent batch");
dbExecutor.execute(new GenerateBatch()); generateBatch();
} }
} }
@@ -266,12 +313,15 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override @Override
public void run() { public void run() {
if (interrupted) return; if (interrupted) return;
if (!generateOfferQueued.getAndSet(false))
throw new AssertionError();
try { try {
Offer o; Offer o;
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
o = db.generateOffer(txn, contactId, MAX_MESSAGE_IDS, o = db.generateOffer(txn, contactId, MAX_MESSAGE_IDS,
maxLatency); maxLatency);
setNextSendTime(db.getNextSendTime(txn, contactId));
db.commitTransaction(txn); db.commitTransaction(txn);
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
@@ -300,7 +350,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
if (interrupted) return; if (interrupted) return;
recordWriter.writeOffer(offer); recordWriter.writeOffer(offer);
LOG.info("Sent offer"); LOG.info("Sent offer");
dbExecutor.execute(new GenerateOffer()); generateOffer();
} }
} }
@@ -310,6 +360,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
@Override @Override
public void run() { public void run() {
if (interrupted) return; if (interrupted) return;
if (!generateRequestQueued.getAndSet(false))
throw new AssertionError();
try { try {
Request r; Request r;
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
@@ -343,7 +395,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
if (interrupted) return; if (interrupted) return;
recordWriter.writeRequest(request); recordWriter.writeRequest(request);
LOG.info("Sent request"); LOG.info("Sent request");
dbExecutor.execute(new GenerateRequest()); generateRequest();
} }
} }
} }

View File

@@ -11,7 +11,7 @@ import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
@@ -27,6 +27,7 @@ import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
/** /**
* An incoming {@link SyncSession}. * An incoming {@link SyncSession}.
@@ -96,8 +97,9 @@ class IncomingSession implements SyncSession, EventListener {
if (e instanceof ContactRemovedEvent) { if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) interrupt(); if (c.getContactId().equals(contactId)) interrupt();
} else if (e instanceof ShutdownEvent) { } else if (e instanceof LifecycleEvent) {
interrupt(); LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt();
} }
} }

View File

@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.event.ShutdownEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.RecordWriter; import org.briarproject.bramble.api.sync.RecordWriter;
@@ -28,6 +28,7 @@ import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
@@ -109,8 +110,9 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
if (e instanceof ContactRemovedEvent) { if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) interrupt(); if (c.getContactId().equals(contactId)) interrupt();
} else if (e instanceof ShutdownEvent) { } else if (e instanceof LifecycleEvent) {
interrupt(); LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt();
} }
} }

View File

@@ -47,11 +47,10 @@ import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.transport.IncomingKeys; import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.OutgoingKeys; import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestUtils; import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.Mockery;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
@@ -74,7 +73,13 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public class DatabaseComponentImplTest extends BrambleTestCase { public class DatabaseComponentImplTest extends BrambleMockTestCase {
@SuppressWarnings("unchecked")
private final Database<Object> database = context.mock(Database.class);
private final ShutdownManager shutdown =
context.mock(ShutdownManager.class);
private final EventBus eventBus = context.mock(EventBus.class);
private final Object txn = new Object(); private final Object txn = new Object();
private final ClientId clientId; private final ClientId clientId;
@@ -125,16 +130,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} }
@Test @Test
@SuppressWarnings("unchecked")
public void testSimpleCalls() throws Exception { public void testSimpleCalls() throws Exception {
int shutdownHandle = 12345; int shutdownHandle = 12345;
Mockery context = new Mockery();
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// open() // open()
oneOf(database).open(); oneOf(database).open(null);
will(returnValue(false)); will(returnValue(false));
oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
will(returnValue(shutdownHandle)); will(returnValue(shutdownHandle));
@@ -194,13 +194,12 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
// endTransaction() // endTransaction()
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// close() // close()
oneOf(shutdown).removeShutdownHook(shutdownHandle);
oneOf(database).close(); oneOf(database).close();
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown); shutdown);
assertFalse(db.open()); assertFalse(db.open(null));
Transaction transaction = db.startTransaction(false); Transaction transaction = db.startTransaction(false);
try { try {
db.addLocalAuthor(transaction, localAuthor); db.addLocalAuthor(transaction, localAuthor);
@@ -221,18 +220,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
db.close(); db.close();
context.assertIsSatisfied();
} }
@Test @Test
public void testLocalMessagesAreNotStoredUnlessGroupExists() public void testLocalMessagesAreNotStoredUnlessGroupExists()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -252,17 +244,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testAddLocalMessage() throws Exception { public void testAddLocalMessage() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -294,18 +279,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testVariousMethodsThrowExceptionIfContactIsMissing() public void testVariousMethodsThrowExceptionIfContactIsMissing()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not) // Check whether the contact is in the DB (which it's not)
exactly(18).of(database).startTransaction(); exactly(18).of(database).startTransaction();
@@ -500,18 +478,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testVariousMethodsThrowExceptionIfLocalAuthorIsMissing() public void testVariousMethodsThrowExceptionIfLocalAuthorIsMissing()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the pseudonym is in the DB (which it's not) // Check whether the pseudonym is in the DB (which it's not)
exactly(3).of(database).startTransaction(); exactly(3).of(database).startTransaction();
@@ -552,18 +523,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testVariousMethodsThrowExceptionIfGroupIsMissing() public void testVariousMethodsThrowExceptionIfGroupIsMissing()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the group is in the DB (which it's not) // Check whether the group is in the DB (which it's not)
exactly(8).of(database).startTransaction(); exactly(8).of(database).startTransaction();
@@ -657,18 +621,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testVariousMethodsThrowExceptionIfMessageIsMissing() public void testVariousMethodsThrowExceptionIfMessageIsMissing()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the message is in the DB (which it's not) // Check whether the message is in the DB (which it's not)
exactly(11).of(database).startTransaction(); exactly(11).of(database).startTransaction();
@@ -792,18 +749,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testVariousMethodsThrowExceptionIfTransportIsMissing() public void testVariousMethodsThrowExceptionIfTransportIsMissing()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// startTransaction() // startTransaction()
oneOf(database).startTransaction(); oneOf(database).startTransaction();
@@ -890,19 +840,12 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testGenerateAck() throws Exception { public void testGenerateAck() throws Exception {
Collection<MessageId> messagesToAck = Arrays.asList(messageId, Collection<MessageId> messagesToAck = Arrays.asList(messageId,
messageId1); messageId1);
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -925,8 +868,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
@@ -934,11 +875,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
byte[] raw1 = new byte[size]; byte[] raw1 = new byte[size];
Collection<MessageId> ids = Arrays.asList(messageId, messageId1); Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
Collection<byte[]> messages = Arrays.asList(raw, raw1); Collection<byte[]> messages = Arrays.asList(raw, raw1);
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -969,19 +905,12 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testGenerateOffer() throws Exception { public void testGenerateOffer() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Collection<MessageId> ids = Arrays.asList(messageId, messageId1); Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1007,19 +936,12 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testGenerateRequest() throws Exception { public void testGenerateRequest() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Collection<MessageId> ids = Arrays.asList(messageId, messageId1); Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1042,8 +964,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
@@ -1051,11 +971,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
byte[] raw1 = new byte[size]; byte[] raw1 = new byte[size];
Collection<MessageId> ids = Arrays.asList(messageId, messageId1); Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
Collection<byte[]> messages = Arrays.asList(raw, raw1); Collection<byte[]> messages = Arrays.asList(raw, raw1);
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1087,17 +1002,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testReceiveAck() throws Exception { public void testReceiveAck() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1120,17 +1028,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testReceiveMessage() throws Exception { public void testReceiveMessage() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1175,17 +1076,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testReceiveDuplicateMessage() throws Exception { public void testReceiveDuplicateMessage() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1212,17 +1106,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testReceiveMessageWithoutVisibleGroup() throws Exception { public void testReceiveMessageWithoutVisibleGroup() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1242,8 +1129,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
@@ -1251,11 +1136,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId messageId2 = new MessageId(TestUtils.getRandomId()); MessageId messageId2 = new MessageId(TestUtils.getRandomId());
MessageId messageId3 = new MessageId(TestUtils.getRandomId()); MessageId messageId3 = new MessageId(TestUtils.getRandomId());
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1296,17 +1176,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testReceiveRequest() throws Exception { public void testReceiveRequest() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1330,17 +1203,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testChangingVisibilityCallsListeners() throws Exception { public void testChangingVisibilityCallsListeners() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1370,18 +1236,11 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testNotChangingVisibilityDoesNotCallListeners() public void testNotChangingVisibilityDoesNotCallListeners()
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1403,8 +1262,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
@@ -1412,11 +1269,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
TransportKeys transportKeys = createTransportKeys(); TransportKeys transportKeys = createTransportKeys();
Map<ContactId, TransportKeys> keys = Collections.singletonMap( Map<ContactId, TransportKeys> keys = Collections.singletonMap(
contactId, transportKeys); contactId, transportKeys);
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// startTransaction() // startTransaction()
oneOf(database).startTransaction(); oneOf(database).startTransaction();
@@ -1446,8 +1298,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
private TransportKeys createTransportKeys() { private TransportKeys createTransportKeys() {
@@ -1480,11 +1330,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
Settings merged = new Settings(); Settings merged = new Settings();
merged.put("foo", "bar"); merged.put("foo", "bar");
merged.put("baz", "qux"); merged.put("baz", "qux");
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// startTransaction() // startTransaction()
oneOf(database).startTransaction(); oneOf(database).startTransaction();
@@ -1514,8 +1359,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
@@ -1545,12 +1388,6 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
private void testCannotStartTransactionDuringTransaction( private void testCannotStartTransactionDuringTransaction(
boolean firstTxnReadOnly, boolean secondTxnReadOnly) boolean firstTxnReadOnly, boolean secondTxnReadOnly)
throws Exception { throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1560,22 +1397,12 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
shutdown); shutdown);
assertNotNull(db.startTransaction(firstTxnReadOnly)); assertNotNull(db.startTransaction(firstTxnReadOnly));
try {
db.startTransaction(secondTxnReadOnly); db.startTransaction(secondTxnReadOnly);
fail(); fail();
} finally {
context.assertIsSatisfied();
}
} }
@Test @Test
public void testCannotAddLocalIdentityAsContact() throws Exception { public void testCannotAddLocalIdentityAsContact() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1599,18 +1426,10 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
public void testCannotAddDuplicateContact() throws Exception { public void testCannotAddDuplicateContact() throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
@@ -1636,22 +1455,16 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
} finally { } finally {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
context.assertIsSatisfied();
} }
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testMessageDependencies() throws Exception { public void testMessageDependencies() throws Exception {
int shutdownHandle = 12345; int shutdownHandle = 12345;
Mockery context = new Mockery();
Database<Object> database = context.mock(Database.class);
ShutdownManager shutdown = context.mock(ShutdownManager.class);
EventBus eventBus = context.mock(EventBus.class);
MessageId messageId2 = new MessageId(TestUtils.getRandomId()); MessageId messageId2 = new MessageId(TestUtils.getRandomId());
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// open() // open()
oneOf(database).open(); oneOf(database).open(null);
will(returnValue(false)); will(returnValue(false));
oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
will(returnValue(shutdownHandle)); will(returnValue(shutdownHandle));
@@ -1693,13 +1506,12 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
// endTransaction() // endTransaction()
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// close() // close()
oneOf(shutdown).removeShutdownHook(shutdownHandle);
oneOf(database).close(); oneOf(database).close();
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
shutdown); shutdown);
assertFalse(db.open()); assertFalse(db.open(null));
Transaction transaction = db.startTransaction(false); Transaction transaction = db.startTransaction(false);
try { try {
db.addLocalMessage(transaction, message, metadata, true); db.addLocalMessage(transaction, message, metadata, true);
@@ -1714,7 +1526,5 @@ public class DatabaseComponentImplTest extends BrambleTestCase {
db.endTransaction(transaction); db.endTransaction(transaction);
} }
db.close(); db.close();
context.assertIsSatisfied();
} }
} }

View File

@@ -0,0 +1,233 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DataTooNewException;
import org.briarproject.bramble.api.db.DataTooOldException;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestUtils;
import org.jmock.Expectations;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.sql.Connection;
import java.util.List;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
import static org.briarproject.bramble.db.JdbcDatabase.CODE_SCHEMA_VERSION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@NotNullByDefault
public abstract class DatabaseMigrationTest extends BrambleMockTestCase {
private final File testDir = TestUtils.getTestDirectory();
@SuppressWarnings("unchecked")
private final Migration<Connection> migration =
context.mock(Migration.class, "migration");
@SuppressWarnings("unchecked")
private final Migration<Connection> migration1 =
context.mock(Migration.class, "migration1");
protected final DatabaseConfig config =
new TestDatabaseConfig(testDir, 1024 * 1024);
protected final Clock clock = new SystemClock();
abstract Database<Connection> createDatabase(
List<Migration<Connection>> migrations) throws Exception;
@Before
public void setUp() {
assertTrue(testDir.mkdirs());
}
@After
public void tearDown() {
TestUtils.deleteTestDirectory(testDir);
}
@Test
public void testDoesNotRunMigrationsWhenCreatingDatabase()
throws Exception {
Database<Connection> db = createDatabase(singletonList(migration));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
db.close();
}
@Test(expected = DbException.class)
public void testThrowsExceptionIfDataSchemaVersionIsMissing()
throws Exception {
// Open the DB for the first time
Database<Connection> db = createDatabase(asList(migration, migration1));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
// Override the data schema version
setDataSchemaVersion(db, -1);
db.close();
// Reopen the DB - an exception should be thrown
db = createDatabase(asList(migration, migration1));
db.open(null);
}
@Test
public void testDoesNotRunMigrationsIfSchemaVersionsMatch()
throws Exception {
// Open the DB for the first time
Database<Connection> db = createDatabase(asList(migration, migration1));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
db.close();
// Reopen the DB - migrations should not be run
db = createDatabase(asList(migration, migration1));
assertTrue(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
db.close();
}
@Test(expected = DataTooNewException.class)
public void testThrowsExceptionIfDataIsNewerThanCode() throws Exception {
// Open the DB for the first time
Database<Connection> db = createDatabase(asList(migration, migration1));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
// Override the data schema version
setDataSchemaVersion(db, CODE_SCHEMA_VERSION + 1);
db.close();
// Reopen the DB - an exception should be thrown
db = createDatabase(asList(migration, migration1));
db.open(null);
}
@Test(expected = DataTooOldException.class)
public void testThrowsExceptionIfCodeIsNewerThanDataAndNoMigrations()
throws Exception {
// Open the DB for the first time
Database<Connection> db = createDatabase(emptyList());
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 1);
db.close();
// Reopen the DB - an exception should be thrown
db = createDatabase(emptyList());
db.open(null);
}
@Test(expected = DataTooOldException.class)
public void testThrowsExceptionIfCodeIsNewerThanDataAndNoSuitableMigration()
throws Exception {
context.checking(new Expectations() {{
oneOf(migration).getStartVersion();
will(returnValue(CODE_SCHEMA_VERSION - 2));
oneOf(migration).getEndVersion();
will(returnValue(CODE_SCHEMA_VERSION - 1));
oneOf(migration1).getStartVersion();
will(returnValue(CODE_SCHEMA_VERSION - 1));
oneOf(migration1).getEndVersion();
will(returnValue(CODE_SCHEMA_VERSION));
}});
// Open the DB for the first time
Database<Connection> db = createDatabase(asList(migration, migration1));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
// Override the data schema version
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 3);
db.close();
// Reopen the DB - an exception should be thrown
db = createDatabase(asList(migration, migration1));
db.open(null);
}
@Test
public void testRunsMigrationIfCodeIsNewerThanDataAndSuitableMigration()
throws Exception {
context.checking(new Expectations() {{
// First migration should be run, increasing schema version by 2
oneOf(migration).getStartVersion();
will(returnValue(CODE_SCHEMA_VERSION - 2));
oneOf(migration).getEndVersion();
will(returnValue(CODE_SCHEMA_VERSION));
oneOf(migration).migrate(with(any(Connection.class)));
// Second migration is not suitable and should be skipped
oneOf(migration1).getStartVersion();
will(returnValue(CODE_SCHEMA_VERSION - 1));
oneOf(migration1).getEndVersion();
will(returnValue(CODE_SCHEMA_VERSION));
}});
// Open the DB for the first time
Database<Connection> db = createDatabase(asList(migration, migration1));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
// Override the data schema version
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
db.close();
// Reopen the DB - the first migration should be run
db = createDatabase(asList(migration, migration1));
assertTrue(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
db.close();
}
@Test
public void testRunsMigrationsIfCodeIsNewerThanDataAndSuitableMigrations()
throws Exception {
context.checking(new Expectations() {{
// First migration should be run, incrementing schema version
oneOf(migration).getStartVersion();
will(returnValue(CODE_SCHEMA_VERSION - 2));
oneOf(migration).getEndVersion();
will(returnValue(CODE_SCHEMA_VERSION - 1));
oneOf(migration).migrate(with(any(Connection.class)));
// Second migration should be run, incrementing schema version again
oneOf(migration1).getStartVersion();
will(returnValue(CODE_SCHEMA_VERSION - 1));
oneOf(migration1).getEndVersion();
will(returnValue(CODE_SCHEMA_VERSION));
oneOf(migration1).migrate(with(any(Connection.class)));
}});
// Open the DB for the first time
Database<Connection> db = createDatabase(asList(migration, migration1));
assertFalse(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
// Override the data schema version
setDataSchemaVersion(db, CODE_SCHEMA_VERSION - 2);
db.close();
// Reopen the DB - both migrations should be run
db = createDatabase(asList(migration, migration1));
assertTrue(db.open(null));
assertEquals(CODE_SCHEMA_VERSION, getDataSchemaVersion(db));
db.close();
}
private int getDataSchemaVersion(Database<Connection> db)
throws Exception {
Connection txn = db.startTransaction();
Settings s = db.getSettings(txn, DB_SETTINGS_NAMESPACE);
db.commitTransaction(txn);
return s.getInt(SCHEMA_VERSION_KEY, -1);
}
private void setDataSchemaVersion(Database<Connection> db, int version)
throws Exception {
Settings s = new Settings();
s.putInt(SCHEMA_VERSION_KEY, version);
Connection txn = db.startTransaction();
db.mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
db.commitTransaction(txn);
}
}

View File

@@ -17,6 +17,7 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.ValidationManager.State; import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.IncomingKeys; import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.OutgoingKeys; import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.api.transport.TransportKeys;
@@ -1635,6 +1636,53 @@ public class H2DatabaseTest extends BrambleTestCase {
db.close(); db.close();
} }
@Test
public void testGetNextSendTime() throws Exception {
long now = System.currentTimeMillis();
Database<Connection> db = open(false, new StoppedClock(now));
Connection txn = db.startTransaction();
// Add a contact, a group and a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
true, true));
db.addGroup(txn, group);
db.addMessage(txn, message, UNKNOWN, false);
// There should be no messages to send
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the group with the contact - still no messages to send
db.addGroupVisibility(txn, contactId, groupId, true);
db.addStatus(txn, contactId, messageId, false, false);
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Set the message's state to DELIVERED - still no messages to send
db.setMessageState(txn, messageId, DELIVERED);
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
// Share the message - now it should be sendable immediately
db.setMessageShared(txn, messageId);
assertEquals(0, db.getNextSendTime(txn, contactId));
// Update the message's expiry time as though we sent it - now the
// message should be sendable after one round-trip
db.updateExpiryTime(txn, contactId, messageId, 1000);
assertEquals(now + 2000, db.getNextSendTime(txn, contactId));
// Update the message's expiry time again - now it should be sendable
// after two round-trips
db.updateExpiryTime(txn, contactId, messageId, 1000);
assertEquals(now + 4000, db.getNextSendTime(txn, contactId));
// Delete the message - there should be no messages to send
db.deleteMessage(txn, messageId);
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
db.commitTransaction(txn);
db.close();
}
@Test @Test
public void testExceptionHandling() throws Exception { public void testExceptionHandling() throws Exception {
Database<Connection> db = open(false); Database<Connection> db = open(false);
@@ -1652,10 +1700,15 @@ public class H2DatabaseTest extends BrambleTestCase {
} }
private Database<Connection> open(boolean resume) throws Exception { private Database<Connection> open(boolean resume) throws Exception {
return open(resume, new SystemClock());
}
private Database<Connection> open(boolean resume, Clock clock)
throws Exception {
Database<Connection> db = new H2Database(new TestDatabaseConfig(testDir, Database<Connection> db = new H2Database(new TestDatabaseConfig(testDir,
MAX_SIZE), new SystemClock()); MAX_SIZE), clock);
if (!resume) TestUtils.deleteTestDirectory(testDir); if (!resume) TestUtils.deleteTestDirectory(testDir);
db.open(); db.open(null);
return db; return db;
} }
@@ -1683,4 +1736,23 @@ public class H2DatabaseTest extends BrambleTestCase {
public void tearDown() { public void tearDown() {
TestUtils.deleteTestDirectory(testDir); TestUtils.deleteTestDirectory(testDir);
} }
private static class StoppedClock implements Clock {
private final long time;
private StoppedClock(long time) {
this.time = time;
}
@Override
public long currentTimeMillis() {
return time;
}
@Override
public void sleep(long milliseconds) throws InterruptedException {
Thread.sleep(milliseconds);
}
}
} }

View File

@@ -0,0 +1,21 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.sql.Connection;
import java.util.List;
@NotNullByDefault
public class H2MigrationTest extends DatabaseMigrationTest {
@Override
Database<Connection> createDatabase(List<Migration<Connection>> migrations)
throws Exception {
return new H2Database(config, clock) {
@Override
List<Migration<Connection>> getMigrations() {
return migrations;
}
};
}
}

View File

@@ -0,0 +1,280 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.ValidationManager.State;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map.Entry;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class Migration30_31Test extends BrambleTestCase {
private static final String CREATE_GROUPS_STUB =
"CREATE TABLE groups"
+ " (groupID BINARY(32) NOT NULL,"
+ " PRIMARY KEY (groupId))";
private static final String CREATE_MESSAGES =
"CREATE TABLE messages"
+ " (messageId BINARY(32) NOT NULL,"
+ " groupId BINARY(32) NOT NULL,"
+ " timestamp BIGINT NOT NULL,"
+ " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL,"
+ " length INT NOT NULL,"
+ " raw BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId),"
+ " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE)";
private static final String CREATE_MESSAGE_METADATA_30 =
"CREATE TABLE messageMetadata"
+ " (messageId BINARY(32) NOT NULL,"
+ " key VARCHAR NOT NULL,"
+ " value BINARY NOT NULL,"
+ " PRIMARY KEY (messageId, key),"
+ " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE)";
private final File testDir = TestUtils.getTestDirectory();
private final File db = new File(testDir, "db");
private final String url = "jdbc:h2:" + db.getAbsolutePath();
private final GroupId groupId = new GroupId(getRandomId());
private final GroupId groupId1 = new GroupId(getRandomId());
private final Message message = TestUtils.getMessage(groupId);
private final Message message1 = TestUtils.getMessage(groupId1);
private final Metadata meta = new Metadata(), meta1 = new Metadata();
private Connection connection = null;
public Migration30_31Test() {
for (int i = 0; i < 10; i++) {
meta.put(getRandomString(123 + i), getRandomBytes(123 + i));
meta1.put(getRandomString(123 + i), getRandomBytes(123 + i));
}
}
@Before
public void setUp() throws Exception {
assertTrue(testDir.mkdirs());
Class.forName("org.h2.Driver");
connection = DriverManager.getConnection(url);
}
@After
public void tearDown() throws Exception {
if (connection != null) connection.close();
TestUtils.deleteTestDirectory(testDir);
}
@Test
public void testMigration() throws Exception {
try {
Statement s = connection.createStatement();
s.execute(CREATE_GROUPS_STUB);
s.execute(CREATE_MESSAGES);
s.execute(CREATE_MESSAGE_METADATA_30);
s.close();
addGroup(groupId);
addMessage(message, DELIVERED, true);
addMessageMetadata30(message, meta);
assertMetadataEquals(meta, getMessageMetadata(message.getId()));
addGroup(groupId1);
addMessage(message1, UNKNOWN, false);
addMessageMetadata30(message1, meta1);
assertMetadataEquals(meta1, getMessageMetadata(message1.getId()));
new Migration30_31().migrate(connection);
assertMetadataEquals(meta, getMessageMetadata(message.getId()));
for (String key : meta.keySet()) {
GroupId g = getMessageMetadataGroupId31(message.getId(), key);
assertEquals(groupId, g);
State state = getMessageMetadataState31(message.getId(), key);
assertEquals(DELIVERED, state);
}
assertMetadataEquals(meta1, getMessageMetadata(message1.getId()));
for (String key : meta1.keySet()) {
GroupId g = getMessageMetadataGroupId31(message1.getId(), key);
assertEquals(groupId1, g);
State state = getMessageMetadataState31(message1.getId(), key);
assertEquals(UNKNOWN, state);
}
} catch (SQLException e) {
connection.close();
throw e;
}
}
private void addGroup(GroupId g) throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO groups (groupId) VALUES (?)";
ps = connection.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private void addMessage(Message m, State state, boolean shared)
throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
+ " state, shared, length, raw)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes());
ps.setBytes(2, m.getGroupId().getBytes());
ps.setLong(3, m.getTimestamp());
ps.setInt(4, state.getValue());
ps.setBoolean(5, shared);
byte[] raw = m.getRaw();
ps.setInt(6, raw.length);
ps.setBytes(7, raw);
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private void addMessageMetadata30(Message m, Metadata meta)
throws SQLException {
PreparedStatement ps = null;
try {
String sql = "INSERT INTO messageMetadata"
+ " (messageId, key, value)"
+ " VALUES (?, ?, ?)";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes());
for (Entry<String, byte[]> e : meta.entrySet()) {
ps.setString(2, e.getKey());
ps.setBytes(3, e.getValue());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != meta.size())
throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
if (ps != null) ps.close();
throw e;
}
}
private Metadata getMessageMetadata(MessageId m) throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT key, value FROM messageMetadata"
+ " WHERE messageId = ?";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
Metadata meta = new Metadata();
while (rs.next()) meta.put(rs.getString(1), rs.getBytes(2));
rs.close();
ps.close();
return meta;
} catch (SQLException e) {
if (rs != null) rs.close();
if (ps != null) ps.close();
throw e;
}
}
private GroupId getMessageMetadataGroupId31(MessageId m, String key)
throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT groupId FROM messageMetadata"
+ " WHERE messageId = ? AND key = ?";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setString(2, key);
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
GroupId g = new GroupId(rs.getBytes(1));
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return g;
} catch (SQLException e) {
if (rs != null) rs.close();
if (ps != null) ps.close();
throw e;
}
}
private State getMessageMetadataState31(MessageId m, String key)
throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT state FROM messageMetadata"
+ " WHERE messageId = ? AND key = ?";
ps = connection.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setString(2, key);
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
State state = State.fromValue(rs.getInt(1));
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return state;
} catch (SQLException e) {
if (rs != null) rs.close();
if (ps != null) ps.close();
throw e;
}
}
private void assertMetadataEquals(Metadata expected, Metadata actual) {
assertEquals(expected.size(), actual.size());
for (Entry<String, byte[]> e : expected.entrySet()) {
byte[] value = actual.get(e.getKey());
assertNotNull(value);
assertArrayEquals(e.getValue(), value);
}
}
}

View File

@@ -12,14 +12,14 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent; import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent; import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent;
import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor; import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.bramble.test.RunAction; import org.briarproject.bramble.test.RunAction;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.legacy.ClassImposteriser; import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.Test; import org.junit.Test;
@@ -29,30 +29,37 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
public class PollerTest extends BrambleTestCase { public class PollerTest extends BrambleMockTestCase {
private final ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
private final ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
private final ConnectionRegistry connectionRegistry =
context.mock(ConnectionRegistry.class);
private final PluginManager pluginManager =
context.mock(PluginManager.class);
private final Clock clock = context.mock(Clock.class);
private final ScheduledFuture future = context.mock(ScheduledFuture.class);
private final SecureRandom random;
private final Executor ioExecutor = new ImmediateExecutor();
private final TransportId transportId = new TransportId("id");
private final ContactId contactId = new ContactId(234); private final ContactId contactId = new ContactId(234);
private final int pollingInterval = 60 * 1000; private final int pollingInterval = 60 * 1000;
private final long now = System.currentTimeMillis(); private final long now = System.currentTimeMillis();
public PollerTest() {
context.setImposteriser(ClassImposteriser.INSTANCE);
random = context.mock(SecureRandom.class);
}
@Test @Test
public void testConnectOnContactStatusChanged() throws Exception { public void testConnectOnContactStatusChanged() throws Exception {
Mockery context = new Mockery();
context.setImposteriser(ClassImposteriser.INSTANCE);
Executor ioExecutor = new ImmediateExecutor();
ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
ConnectionRegistry connectionRegistry =
context.mock(ConnectionRegistry.class);
PluginManager pluginManager = context.mock(PluginManager.class);
SecureRandom random = context.mock(SecureRandom.class);
Clock clock = context.mock(Clock.class);
// Two simplex plugins: one supports polling, the other doesn't // Two simplex plugins: one supports polling, the other doesn't
SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class); SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
SimplexPlugin simplexPlugin1 = SimplexPlugin simplexPlugin1 =
@@ -120,28 +127,12 @@ public class PollerTest extends BrambleTestCase {
connectionRegistry, pluginManager, random, clock); connectionRegistry, pluginManager, random, clock);
p.eventOccurred(new ContactStatusChangedEvent(contactId, true)); p.eventOccurred(new ContactStatusChangedEvent(contactId, true));
context.assertIsSatisfied();
} }
@Test @Test
public void testRescheduleAndReconnectOnConnectionClosed() public void testRescheduleAndReconnectOnConnectionClosed()
throws Exception { throws Exception {
Mockery context = new Mockery();
context.setImposteriser(ClassImposteriser.INSTANCE);
Executor ioExecutor = new ImmediateExecutor();
ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
ConnectionRegistry connectionRegistry =
context.mock(ConnectionRegistry.class);
PluginManager pluginManager = context.mock(PluginManager.class);
SecureRandom random = context.mock(SecureRandom.class);
Clock clock = context.mock(Clock.class);
DuplexPlugin plugin = context.mock(DuplexPlugin.class); DuplexPlugin plugin = context.mock(DuplexPlugin.class);
TransportId transportId = new TransportId("id");
DuplexTransportConnection duplexConnection = DuplexTransportConnection duplexConnection =
context.mock(DuplexTransportConnection.class); context.mock(DuplexTransportConnection.class);
@@ -168,6 +159,7 @@ public class PollerTest extends BrambleTestCase {
will(returnValue(now)); will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) pollingInterval), with(MILLISECONDS)); with((long) pollingInterval), with(MILLISECONDS));
will(returnValue(future));
// connectToContact() // connectToContact()
// Check whether the contact is already connected // Check whether the contact is already connected
oneOf(connectionRegistry).isConnected(contactId, transportId); oneOf(connectionRegistry).isConnected(contactId, transportId);
@@ -185,28 +177,12 @@ public class PollerTest extends BrambleTestCase {
p.eventOccurred(new ConnectionClosedEvent(contactId, transportId, p.eventOccurred(new ConnectionClosedEvent(contactId, transportId,
false)); false));
context.assertIsSatisfied();
} }
@Test @Test
public void testRescheduleOnConnectionOpened() throws Exception { public void testRescheduleOnConnectionOpened() throws Exception {
Mockery context = new Mockery(); Plugin plugin = context.mock(Plugin.class);
context.setImposteriser(ClassImposteriser.INSTANCE);
Executor ioExecutor = new ImmediateExecutor();
ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
ConnectionRegistry connectionRegistry =
context.mock(ConnectionRegistry.class);
PluginManager pluginManager = context.mock(PluginManager.class);
SecureRandom random = context.mock(SecureRandom.class);
Clock clock = context.mock(Clock.class);
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
TransportId transportId = new TransportId("id");
context.checking(new Expectations() {{ context.checking(new Expectations() {{
allowing(plugin).getId(); allowing(plugin).getId();
@@ -224,6 +200,7 @@ public class PollerTest extends BrambleTestCase {
will(returnValue(now)); will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) pollingInterval), with(MILLISECONDS)); with((long) pollingInterval), with(MILLISECONDS));
will(returnValue(future));
}}); }});
Poller p = new Poller(ioExecutor, scheduler, connectionManager, Poller p = new Poller(ioExecutor, scheduler, connectionManager,
@@ -231,27 +208,11 @@ public class PollerTest extends BrambleTestCase {
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId, p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false)); false));
context.assertIsSatisfied();
} }
@Test @Test
public void testRescheduleDoesNotReplaceEarlierTask() throws Exception { public void testRescheduleDoesNotReplaceEarlierTask() throws Exception {
Mockery context = new Mockery(); Plugin plugin = context.mock(Plugin.class);
context.setImposteriser(ClassImposteriser.INSTANCE);
Executor ioExecutor = new ImmediateExecutor();
ScheduledExecutorService scheduler =
context.mock(ScheduledExecutorService.class);
ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
ConnectionRegistry connectionRegistry =
context.mock(ConnectionRegistry.class);
PluginManager pluginManager = context.mock(PluginManager.class);
SecureRandom random = context.mock(SecureRandom.class);
Clock clock = context.mock(Clock.class);
DuplexPlugin plugin = context.mock(DuplexPlugin.class);
TransportId transportId = new TransportId("id");
context.checking(new Expectations() {{ context.checking(new Expectations() {{
allowing(plugin).getId(); allowing(plugin).getId();
@@ -270,6 +231,7 @@ public class PollerTest extends BrambleTestCase {
will(returnValue(now)); will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) pollingInterval), with(MILLISECONDS)); with((long) pollingInterval), with(MILLISECONDS));
will(returnValue(future));
// Second event // Second event
// Get the plugin // Get the plugin
oneOf(pluginManager).getPlugin(transportId); oneOf(pluginManager).getPlugin(transportId);
@@ -291,27 +253,59 @@ public class PollerTest extends BrambleTestCase {
false)); false));
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId, p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false)); false));
context.assertIsSatisfied();
} }
@Test @Test
public void testPollOnTransportEnabled() throws Exception { public void testRescheduleReplacesLaterTask() throws Exception {
Mockery context = new Mockery(); Plugin plugin = context.mock(Plugin.class);
context.setImposteriser(ClassImposteriser.INSTANCE);
Executor ioExecutor = new ImmediateExecutor(); context.checking(new Expectations() {{
ScheduledExecutorService scheduler = allowing(plugin).getId();
context.mock(ScheduledExecutorService.class); will(returnValue(transportId));
ConnectionManager connectionManager = // First event
context.mock(ConnectionManager.class); // Get the plugin
ConnectionRegistry connectionRegistry = oneOf(pluginManager).getPlugin(transportId);
context.mock(ConnectionRegistry.class); will(returnValue(plugin));
PluginManager pluginManager = context.mock(PluginManager.class); // The plugin supports polling
SecureRandom random = context.mock(SecureRandom.class); oneOf(plugin).shouldPoll();
Clock clock = context.mock(Clock.class); will(returnValue(true));
// Schedule the next poll
oneOf(plugin).getPollingInterval();
will(returnValue(pollingInterval));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) pollingInterval), with(MILLISECONDS));
will(returnValue(future));
// Second event
// Get the plugin
oneOf(pluginManager).getPlugin(transportId);
will(returnValue(plugin));
// The plugin supports polling
oneOf(plugin).shouldPoll();
will(returnValue(true));
// Replace the previously scheduled task, due later
oneOf(plugin).getPollingInterval();
will(returnValue(pollingInterval - 2));
oneOf(clock).currentTimeMillis();
will(returnValue(now + 1));
oneOf(future).cancel(false);
oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) pollingInterval - 2), with(MILLISECONDS));
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false));
p.eventOccurred(new ConnectionOpenedEvent(contactId, transportId,
false));
}
@Test
public void testPollsOnTransportEnabled() throws Exception {
Plugin plugin = context.mock(Plugin.class); Plugin plugin = context.mock(Plugin.class);
TransportId transportId = new TransportId("id");
List<ContactId> connected = Collections.singletonList(contactId); List<ContactId> connected = Collections.singletonList(contactId);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
@@ -328,6 +322,7 @@ public class PollerTest extends BrambleTestCase {
will(returnValue(now)); will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L), oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L),
with(MILLISECONDS)); with(MILLISECONDS));
will(returnValue(future));
will(new RunAction()); will(new RunAction());
// Running the polling task schedules the next polling task // Running the polling task schedules the next polling task
oneOf(plugin).getPollingInterval(); oneOf(plugin).getPollingInterval();
@@ -338,6 +333,7 @@ public class PollerTest extends BrambleTestCase {
will(returnValue(now)); will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), oneOf(scheduler).schedule(with(any(Runnable.class)),
with((long) (pollingInterval * 0.5)), with(MILLISECONDS)); with((long) (pollingInterval * 0.5)), with(MILLISECONDS));
will(returnValue(future));
// Poll the plugin // Poll the plugin
oneOf(connectionRegistry).getConnectedContacts(transportId); oneOf(connectionRegistry).getConnectedContacts(transportId);
will(returnValue(connected)); will(returnValue(connected));
@@ -348,7 +344,36 @@ public class PollerTest extends BrambleTestCase {
connectionRegistry, pluginManager, random, clock); connectionRegistry, pluginManager, random, clock);
p.eventOccurred(new TransportEnabledEvent(transportId)); p.eventOccurred(new TransportEnabledEvent(transportId));
}
context.assertIsSatisfied(); @Test
public void testCancelsPollingOnTransportDisabled() throws Exception {
Plugin plugin = context.mock(Plugin.class);
List<ContactId> connected = Collections.singletonList(contactId);
context.checking(new Expectations() {{
allowing(plugin).getId();
will(returnValue(transportId));
// Get the plugin
oneOf(pluginManager).getPlugin(transportId);
will(returnValue(plugin));
// The plugin supports polling
oneOf(plugin).shouldPoll();
will(returnValue(true));
// Schedule a polling task immediately
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L),
with(MILLISECONDS));
will(returnValue(future));
// The plugin is disabled before the task runs - cancel the task
oneOf(future).cancel(false);
}});
Poller p = new Poller(ioExecutor, scheduler, connectionManager,
connectionRegistry, pluginManager, random, clock);
p.eventOccurred(new TransportEnabledEvent(transportId));
p.eventOccurred(new TransportDisabledEvent(transportId));
} }
} }

View File

@@ -2,7 +2,6 @@ package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener; import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
@@ -11,6 +10,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.plugin.tcp.LanTcpPlugin.LanAddressComparator;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test; import org.junit.Test;
@@ -22,13 +22,12 @@ import java.net.NetworkInterface;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@@ -56,6 +55,9 @@ public class LanTcpPluginTest extends BrambleTestCase {
// Local and remote in 192.168.0.0/16 should return true // Local and remote in 192.168.0.0/16 should return true
assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0),
makeAddress(192, 168, 255, 255))); makeAddress(192, 168, 255, 255)));
// Local and remote in 169.254.0.0/16 (link-local) should return true
assertTrue(plugin.addressesAreOnSameLan(makeAddress(169, 254, 0, 0),
makeAddress(169, 254, 255, 255)));
// Local and remote in different recognised prefixes should return false // Local and remote in different recognised prefixes should return false
assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0),
makeAddress(172, 31, 255, 255))); makeAddress(172, 31, 255, 255)));
@@ -193,9 +195,16 @@ public class LanTcpPluginTest extends BrambleTestCase {
KeyAgreementListener kal = KeyAgreementListener kal =
plugin.createKeyAgreementListener(new byte[COMMIT_LENGTH]); plugin.createKeyAgreementListener(new byte[COMMIT_LENGTH]);
assertNotNull(kal); assertNotNull(kal);
Callable<KeyAgreementConnection> c = kal.listen(); CountDownLatch latch = new CountDownLatch(1);
FutureTask<KeyAgreementConnection> f = new FutureTask<>(c); AtomicBoolean error = new AtomicBoolean(false);
new Thread(f).start(); new Thread(() -> {
try {
kal.accept();
latch.countDown();
} catch (IOException e) {
error.set(true);
}
}).start();
// The plugin should have bound a socket and stored the port number // The plugin should have bound a socket and stored the port number
BdfList descriptor = kal.getDescriptor(); BdfList descriptor = kal.getDescriptor();
assertEquals(3, descriptor.size()); assertEquals(3, descriptor.size());
@@ -211,10 +220,12 @@ public class LanTcpPluginTest extends BrambleTestCase {
InetSocketAddress socketAddr = new InetSocketAddress(addr, port); InetSocketAddress socketAddr = new InetSocketAddress(addr, port);
Socket s = new Socket(); Socket s = new Socket();
s.connect(socketAddr, 100); s.connect(socketAddr, 100);
assertNotNull(f.get(5, SECONDS)); // Check that the connection was accepted
assertTrue(latch.await(5, SECONDS));
assertFalse(error.get());
// Clean up
s.close(); s.close();
kal.close(); kal.close();
// Stop the plugin
plugin.stop(); plugin.stop();
} }
@@ -263,7 +274,7 @@ public class LanTcpPluginTest extends BrambleTestCase {
descriptor.add(local.getPort()); descriptor.add(local.getPort());
// Connect to the port // Connect to the port
DuplexTransportConnection d = plugin.createKeyAgreementConnection( DuplexTransportConnection d = plugin.createKeyAgreementConnection(
new byte[COMMIT_LENGTH], descriptor, 5000); new byte[COMMIT_LENGTH], descriptor);
assertNotNull(d); assertNotNull(d);
// Check that the connection was accepted // Check that the connection was accepted
assertTrue(latch.await(5, SECONDS)); assertTrue(latch.await(5, SECONDS));
@@ -275,6 +286,57 @@ public class LanTcpPluginTest extends BrambleTestCase {
plugin.stop(); plugin.stop();
} }
@Test
public void testComparatorPrefersNonZeroPorts() throws Exception {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress nonZero = new InetSocketAddress("1.2.3.4", 1234);
InetSocketAddress zero = new InetSocketAddress("1.2.3.4", 0);
assertEquals(0, comparator.compare(nonZero, nonZero));
assertTrue(comparator.compare(nonZero, zero) < 0);
assertTrue(comparator.compare(zero, nonZero) > 0);
assertEquals(0, comparator.compare(zero, zero));
}
@Test
public void testComparatorPrefersLongerPrefixes() throws Exception {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
assertEquals(0, comparator.compare(prefix192, prefix192));
assertTrue(comparator.compare(prefix192, prefix172) < 0);
assertTrue(comparator.compare(prefix192, prefix10) < 0);
assertTrue(comparator.compare(prefix172, prefix192) > 0);
assertEquals(0, comparator.compare(prefix172, prefix172));
assertTrue(comparator.compare(prefix172, prefix10) < 0);
assertTrue(comparator.compare(prefix10, prefix192) > 0);
assertTrue(comparator.compare(prefix10, prefix172) > 0);
assertEquals(0, comparator.compare(prefix10, prefix10));
}
@Test
public void testComparatorPrefersSiteLocalToLinkLocal() throws Exception {
Comparator<InetSocketAddress> comparator = new LanAddressComparator();
InetSocketAddress prefix192 = new InetSocketAddress("192.168.0.1", 0);
InetSocketAddress prefix172 = new InetSocketAddress("172.16.0.1", 0);
InetSocketAddress prefix10 = new InetSocketAddress("10.0.0.1", 0);
InetSocketAddress linkLocal = new InetSocketAddress("169.254.0.1", 0);
assertTrue(comparator.compare(prefix192, linkLocal) < 0);
assertTrue(comparator.compare(prefix172, linkLocal) < 0);
assertTrue(comparator.compare(prefix10, linkLocal) < 0);
assertTrue(comparator.compare(linkLocal, prefix192) > 0);
assertTrue(comparator.compare(linkLocal, prefix172) > 0);
assertTrue(comparator.compare(linkLocal, prefix10) > 0);
assertEquals(0, comparator.compare(linkLocal, linkLocal));
}
private boolean systemHasLocalIpv4Address() throws Exception { private boolean systemHasLocalIpv4Address() throws Exception {
for (NetworkInterface i : Collections.list( for (NetworkInterface i : Collections.list(
NetworkInterface.getNetworkInterfaces())) { NetworkInterface.getNetworkInterfaces())) {

View File

@@ -94,6 +94,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Group contactGroup1 = getGroup(), contactGroup2 = getGroup(); Group contactGroup1 = getGroup(), contactGroup2 = getGroup();
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(false));
oneOf(db).addGroup(txn, localGroup); oneOf(db).addGroup(txn, localGroup);
oneOf(db).getContacts(txn); oneOf(db).getContacts(txn);
will(returnValue(contacts)); will(returnValue(contacts));
@@ -123,7 +125,21 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
} }
@Test @Test
public void testCreatesGroupWhenAddingContact() throws Exception { public void testDoesNotCreateGroupsAtStartupIfAlreadyCreated()
throws Exception {
Transaction txn = new Transaction(null, false);
context.checking(new Expectations() {{
oneOf(db).containsGroup(txn, localGroup.getId());
will(returnValue(true));
}});
TransportPropertyManagerImpl t = createInstance();
t.createLocalState(txn);
}
@Test
public void testCreatesContactGroupWhenAddingContact() throws Exception {
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
Contact contact = getContact(true); Contact contact = getContact(true);
Group contactGroup = getGroup(); Group contactGroup = getGroup();

View File

@@ -17,6 +17,8 @@ import javax.inject.Singleton;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
@Module @Module
public class TestLifecycleModule { public class TestLifecycleModule {
@@ -57,6 +59,11 @@ public class TestLifecycleModule {
@Override @Override
public void waitForShutdown() throws InterruptedException { public void waitForShutdown() throws InterruptedException {
} }
@Override
public LifecycleState getLifecycleState() {
return RUNNING;
}
}; };
return lifecycleManager; return lifecycleManager;
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin; package org.briarproject.bramble.plugin;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.ShutdownManager; import org.briarproject.bramble.api.lifecycle.ShutdownManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -8,7 +9,7 @@ import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reliability.ReliabilityLayerFactory; import org.briarproject.bramble.api.reliability.ReliabilityLayerFactory;
import org.briarproject.bramble.plugin.bluetooth.BluetoothPluginFactory; import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory;
import org.briarproject.bramble.plugin.file.RemovableDrivePluginFactory; import org.briarproject.bramble.plugin.file.RemovableDrivePluginFactory;
import org.briarproject.bramble.plugin.modem.ModemPluginFactory; import org.briarproject.bramble.plugin.modem.ModemPluginFactory;
import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory; import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory;
@@ -30,9 +31,10 @@ public class DesktopPluginModule extends PluginModule {
PluginConfig getPluginConfig(@IoExecutor Executor ioExecutor, PluginConfig getPluginConfig(@IoExecutor Executor ioExecutor,
SecureRandom random, BackoffFactory backoffFactory, SecureRandom random, BackoffFactory backoffFactory,
ReliabilityLayerFactory reliabilityFactory, ReliabilityLayerFactory reliabilityFactory,
ShutdownManager shutdownManager) { ShutdownManager shutdownManager, EventBus eventBus) {
DuplexPluginFactory bluetooth = new BluetoothPluginFactory(ioExecutor, DuplexPluginFactory bluetooth =
random, backoffFactory); new JavaBluetoothPluginFactory(ioExecutor, random, eventBus,
backoffFactory);
DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor, DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor,
reliabilityFactory); reliabilityFactory);
DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor, DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor,

View File

@@ -0,0 +1,115 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.StringUtils.isValidMac;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class JavaBluetoothPlugin extends BluetoothPlugin<StreamConnectionNotifier> {
private static final Logger LOG =
Logger.getLogger(JavaBluetoothPlugin.class.getName());
// Non-null if the plugin started successfully
private volatile LocalDevice localDevice = null;
JavaBluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom,
Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
super(ioExecutor, secureRandom, backoff, callback, maxLatency);
}
@Override
void initialiseAdapter() throws IOException {
try {
localDevice = LocalDevice.getLocalDevice();
} catch (UnsatisfiedLinkError | BluetoothStateException e) {
throw new IOException(e);
}
}
@Override
boolean isAdapterEnabled() {
return localDevice != null && LocalDevice.isPowerOn();
}
@Override
void enableAdapter() {
// Nothing we can do on this platform
LOG.info("Could not enable Bluetooth");
}
@Override
void disableAdapterIfEnabledByUs() {
// We didn't enable it so we don't need to disable it
}
@Override
void setEnabledByUs() {
// Irrelevant on this platform
}
@Nullable
@Override
String getBluetoothAddress() {
return localDevice.getBluetoothAddress();
}
@Override
StreamConnectionNotifier openServerSocket(String uuid) throws IOException {
String url = makeUrl("localhost", uuid);
return (StreamConnectionNotifier) Connector.open(url);
}
@Override
void tryToClose(@Nullable StreamConnectionNotifier ss) {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
DuplexTransportConnection acceptConnection(StreamConnectionNotifier ss)
throws IOException {
return wrapSocket(ss.acceptAndOpen());
}
@Override
boolean isValidAddress(String address) {
return isValidMac(address);
}
@Override
DuplexTransportConnection connectTo(String address, String uuid)
throws IOException {
String url = makeUrl(address, uuid);
return wrapSocket((StreamConnection) Connector.open(url));
}
private String makeUrl(String address, String uuid) {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
}
private DuplexTransportConnection wrapSocket(StreamConnection s) {
return new JavaBluetoothTransportConnection(this, s);
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.bramble.plugin.bluetooth; package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.BackoffFactory; import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -17,7 +18,7 @@ import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class BluetoothPluginFactory implements DuplexPluginFactory { public class JavaBluetoothPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute private static final int MIN_POLLING_INTERVAL = 60 * 1000; // 1 minute
@@ -27,12 +28,15 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
private final Executor ioExecutor; private final Executor ioExecutor;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
private final EventBus eventBus;
public BluetoothPluginFactory(Executor ioExecutor, public JavaBluetoothPluginFactory(Executor ioExecutor,
SecureRandom secureRandom, BackoffFactory backoffFactory) { SecureRandom secureRandom, EventBus eventBus,
BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.backoffFactory = backoffFactory; this.backoffFactory = backoffFactory;
this.eventBus = eventBus;
} }
@Override @Override
@@ -49,7 +53,9 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) { public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new BluetoothPlugin(ioExecutor, secureRandom, backoff, callback, JavaBluetoothPlugin plugin = new JavaBluetoothPlugin(ioExecutor,
MAX_LATENCY); secureRandom, backoff, callback, MAX_LATENCY);
eventBus.addListener(plugin);
return plugin;
} }
} }

View File

@@ -11,11 +11,12 @@ import java.io.OutputStream;
import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnection;
@NotNullByDefault @NotNullByDefault
class BluetoothTransportConnection extends AbstractDuplexTransportConnection { class JavaBluetoothTransportConnection
extends AbstractDuplexTransportConnection {
private final StreamConnection stream; private final StreamConnection stream;
BluetoothTransportConnection(Plugin plugin, StreamConnection stream) { JavaBluetoothTransportConnection(Plugin plugin, StreamConnection stream) {
super(plugin); super(plugin);
this.stream = stream; this.stream = stream;
} }

View File

@@ -177,7 +177,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
@Override @Override
public DuplexTransportConnection createKeyAgreementConnection( public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor, long timeout) { byte[] commitment, BdfList descriptor) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
lang_map = pt_BR: pt-rBR, fr_FR: fr, nb_NO: nb, zh-Hans: zh-rCN lang_map = pt_BR: pt-rBR, nb_NO: nb, zh-Hans: zh-rCN
[briar.stringsxml-5] [briar.stringsxml-5]
file_filter = src/main/res/values-<lang>/strings.xml file_filter = src/main/res/values-<lang>/strings.xml

View File

@@ -1,6 +1,10 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'witness' apply plugin: 'witness'
configurations.all {
resolutionStrategy.preferProjectModules()
}
dependencies { dependencies {
implementation project(path: ':briar-core', configuration: 'default') implementation project(path: ':briar-core', configuration: 'default')
implementation project(path: ':bramble-core', configuration: 'default') implementation project(path: ':bramble-core', configuration: 'default')
@@ -20,7 +24,7 @@ dependencies {
} }
implementation "com.android.support:cardview-v7:$supportVersion" implementation "com.android.support:cardview-v7:$supportVersion"
implementation "com.android.support:support-annotations:$supportVersion" implementation "com.android.support:support-annotations:$supportVersion"
implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.1.0-beta3'
implementation('ch.acra:acra:4.8.5') { implementation('ch.acra:acra:4.8.5') {
exclude module: 'support-v4' exclude module: 'support-v4'
@@ -61,8 +65,8 @@ dependencyVerification {
'ch.acra:acra:4.8.5:acra-4.8.5.aar:afd5b28934d5166b55f261c85685ad59e8a4ebe9ca1960906afaa8c76d8dc9eb', 'ch.acra:acra:4.8.5:acra-4.8.5.aar:afd5b28934d5166b55f261c85685ad59e8a4ebe9ca1960906afaa8c76d8dc9eb',
'classworlds:classworlds:1.1-alpha-2:classworlds-1.1-alpha-2.jar:2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3', 'classworlds:classworlds:1.1-alpha-2:classworlds-1.1-alpha-2.jar:2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3',
'com.almworks.sqlite4java:sqlite4java:0.282:sqlite4java-0.282.jar:9e1d8dd83ca6003f841e3af878ce2dc7c22497493a7bb6d1b62ec1b0d0a83c05', 'com.almworks.sqlite4java:sqlite4java:0.282:sqlite4java-0.282.jar:9e1d8dd83ca6003f841e3af878ce2dc7c22497493a7bb6d1b62ec1b0d0a83c05',
'com.android.support.constraint:constraint-layout-solver:1.0.2:constraint-layout-solver-1.0.2.jar:8c62525a9bc5cff5633a96cb9b32fffeccaf41b8841aa87fc22607070dea9b8d', 'com.android.support.constraint:constraint-layout-solver:1.1.0-beta3:constraint-layout-solver-1.1.0-beta3.jar:c9084108415046c423983bdff8cf04c8e9a5bed41b8d5329f3764c08312ee3dd',
'com.android.support.constraint:constraint-layout:1.0.2:constraint-layout-1.0.2.aar:b0c688cc2b7172608f8153a689d746da40f71e52d7e2fe2bfd9df2f92db77085', 'com.android.support.constraint:constraint-layout:1.1.0-beta3:constraint-layout-1.1.0-beta3.aar:1754a6bd135feae485aa2ebf9e170f0f3d3282b392f8aa3067d0ed668839db79',
'com.android.support:animated-vector-drawable:27.0.1:animated-vector-drawable-27.0.1.aar:365050110411c86c7eec86101b49ab53557ffe6667f60b19055f1d35c38a577b', 'com.android.support:animated-vector-drawable:27.0.1:animated-vector-drawable-27.0.1.aar:365050110411c86c7eec86101b49ab53557ffe6667f60b19055f1d35c38a577b',
'com.android.support:appcompat-v7:27.0.1:appcompat-v7-27.0.1.aar:1402c29a49db30346c21a7d40634461765b3ab826f5dd95bc4dcc76787b21851', 'com.android.support:appcompat-v7:27.0.1:appcompat-v7-27.0.1.aar:1402c29a49db30346c21a7d40634461765b3ab826f5dd95bc4dcc76787b21851',
'com.android.support:cardview-v7:27.0.1:cardview-v7-27.0.1.aar:43fccd44086c51eaa9d78be2fcf0dfea1556c8876a6fd325ea8d24e860054202', 'com.android.support:cardview-v7:27.0.1:cardview-v7-27.0.1.aar:43fccd44086c51eaa9d78be2fcf0dfea1556c8876a6fd325ea8d24e860054202',
@@ -169,7 +173,7 @@ def getGitHash = { ->
def stdout = new ByteArrayOutputStream() def stdout = new ByteArrayOutputStream()
try { try {
exec { exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD' commandLine 'git', 'rev-parse', '--short=7', 'HEAD'
standardOutput = stdout standardOutput = stdout
} }
return stdout.toString().trim() return stdout.toString().trim()
@@ -185,8 +189,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 26 targetSdkVersion 26
versionCode 1612 versionCode 1619
versionName "0.16.12" versionName "0.16.19"
applicationId "org.briarproject.briar.beta" applicationId "org.briarproject.briar.beta"
resValue "string", "app_package", "org.briarproject.briar.beta" resValue "string", "app_package", "org.briarproject.briar.beta"
resValue "string", "app_name", "Briar Beta" resValue "string", "app_name", "Briar Beta"
@@ -197,7 +201,7 @@ android {
debug { debug {
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
resValue "string", "app_package", "org.briarproject.briar.beta.debug" resValue "string", "app_package", "org.briarproject.briar.beta.debug"
resValue "string", "app_name", "Briar Debug" resValue "string", "app_name", "Briar Beta Debug"
shrinkResources false shrinkResources false
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'

View File

@@ -77,6 +77,11 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".android.login.OpenDatabaseActivity"
android:label="@string/app_name"
android:launchMode="singleTop"/>
<activity <activity
android:name="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:name="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar" android:theme="@style/BriarTheme.NoActionBar"

View File

@@ -1,5 +1,7 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.content.SharedPreferences;
import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreEagerSingletons; import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
@@ -89,6 +91,8 @@ public interface AndroidComponent
AndroidNotificationManager androidNotificationManager(); AndroidNotificationManager androidNotificationManager();
SharedPreferences sharedPreferences();
ScreenFilterMonitor screenFilterMonitor(); ScreenFilterMonitor screenFilterMonitor();
ConnectionRegistry connectionRegistry(); ConnectionRegistry connectionRegistry();

View File

@@ -1,13 +1,17 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.annotation.TargetApi;
import android.app.Application; import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
@@ -44,6 +48,7 @@ import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -57,8 +62,10 @@ import javax.inject.Inject;
import static android.app.Notification.DEFAULT_LIGHTS; import static android.app.Notification.DEFAULT_LIGHTS;
import static android.app.Notification.DEFAULT_SOUND; import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE; import static android.app.Notification.DEFAULT_VIBRATE;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.content.Context.NOTIFICATION_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE; import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE;
import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL; import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -83,6 +90,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private static final int BLOG_POST_NOTIFICATION_ID = 6; private static final int BLOG_POST_NOTIFICATION_ID = 6;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 7; private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 7;
// Channel IDs
private static final String CONTACT_CHANNEL_ID = "contacts";
private static final String GROUP_CHANNEL_ID = "groups";
private static final String FORUM_CHANNEL_ID = "forums";
private static final String BLOG_CHANNEL_ID = "blogs";
private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2); private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
private static final Logger LOG = private static final Logger LOG =
@@ -91,8 +104,9 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private final Executor dbExecutor; private final Executor dbExecutor;
private final SettingsManager settingsManager; private final SettingsManager settingsManager;
private final AndroidExecutor androidExecutor; private final AndroidExecutor androidExecutor;
private final Context appContext;
private final Clock clock; private final Clock clock;
private final Context appContext;
private final NotificationManager notificationManager;
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
// The following must only be accessed on the main UI thread // The following must only be accessed on the main UI thread
@@ -121,6 +135,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.clock = clock; this.clock = clock;
appContext = app.getApplicationContext(); appContext = app.getApplicationContext();
notificationManager = (NotificationManager)
appContext.getSystemService(NOTIFICATION_SERVICE);
} }
@Override @Override
@@ -132,6 +148,37 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
} catch (DbException e) { } catch (DbException e) {
throw new ServiceException(e); throw new ServiceException(e);
} }
if (SDK_INT >= 26) {
// Create notification channels
Callable<Void> task = () -> {
createNotificationChannel(CONTACT_CHANNEL_ID,
R.string.contact_list_button);
createNotificationChannel(GROUP_CHANNEL_ID,
R.string.groups_button);
createNotificationChannel(FORUM_CHANNEL_ID,
R.string.forums_button);
createNotificationChannel(BLOG_CHANNEL_ID,
R.string.blogs_button);
return null;
};
try {
androidExecutor.runOnUiThread(task).get();
} catch (InterruptedException | ExecutionException e) {
throw new ServiceException(e);
}
}
}
@TargetApi(26)
private void createNotificationChannel(String channelId,
@StringRes int name) {
NotificationChannel nc =
new NotificationChannel(channelId, appContext.getString(name),
IMPORTANCE_DEFAULT);
nc.enableLights(true);
nc.setLightColor(
ContextCompat.getColor(appContext, R.color.briar_green_light));
notificationManager.createNotificationChannel(nc);
} }
@Override @Override
@@ -156,44 +203,34 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private void clearContactNotification() { private void clearContactNotification() {
contactCounts.clear(); contactCounts.clear();
contactTotal = 0; contactTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
NotificationManager nm = (NotificationManager) o;
nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
} }
@UiThread @UiThread
private void clearGroupMessageNotification() { private void clearGroupMessageNotification() {
groupCounts.clear(); groupCounts.clear();
groupTotal = 0; groupTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.cancel(GROUP_MESSAGE_NOTIFICATION_ID);
NotificationManager nm = (NotificationManager) o;
nm.cancel(GROUP_MESSAGE_NOTIFICATION_ID);
} }
@UiThread @UiThread
private void clearForumPostNotification() { private void clearForumPostNotification() {
forumCounts.clear(); forumCounts.clear();
forumTotal = 0; forumTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.cancel(FORUM_POST_NOTIFICATION_ID);
NotificationManager nm = (NotificationManager) o;
nm.cancel(FORUM_POST_NOTIFICATION_ID);
} }
@UiThread @UiThread
private void clearBlogPostNotification() { private void clearBlogPostNotification() {
blogCounts.clear(); blogCounts.clear();
blogTotal = 0; blogTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.cancel(BLOG_POST_NOTIFICATION_ID);
NotificationManager nm = (NotificationManager) o;
nm.cancel(BLOG_POST_NOTIFICATION_ID);
} }
@UiThread @UiThread
private void clearIntroductionSuccessNotification() { private void clearIntroductionSuccessNotification() {
introductionTotal = 0; introductionTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID);
NotificationManager nm = (NotificationManager) o;
nm.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID);
} }
@Override @Override
@@ -269,8 +306,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (contactTotal == 0) { if (contactTotal == 0) {
clearContactNotification(); clearContactNotification();
} else if (settings.getBoolean(PREF_NOTIFY_PRIVATE, true)) { } else if (settings.getBoolean(PREF_NOTIFY_PRIVATE, true)) {
BriarNotificationBuilder b = BriarNotificationBuilder b = new BriarNotificationBuilder(
new BriarNotificationBuilder(appContext); appContext, CONTACT_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_private_message); b.setSmallIcon(R.drawable.notification_private_message);
b.setColorRes(R.color.briar_primary); b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name)); b.setContentTitle(appContext.getText(R.string.app_name));
@@ -305,9 +342,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i); t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
} }
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(PRIVATE_MESSAGE_NOTIFICATION_ID,
NotificationManager nm = (NotificationManager) o; b.build());
nm.notify(PRIVATE_MESSAGE_NOTIFICATION_ID, b.build());
} }
} }
@@ -378,7 +414,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
clearGroupMessageNotification(); clearGroupMessageNotification();
} else if (settings.getBoolean(PREF_NOTIFY_GROUP, true)) { } else if (settings.getBoolean(PREF_NOTIFY_GROUP, true)) {
BriarNotificationBuilder b = BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext); new BriarNotificationBuilder(appContext, GROUP_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_private_group); b.setSmallIcon(R.drawable.notification_private_group);
b.setColorRes(R.color.briar_primary); b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name)); b.setContentTitle(appContext.getText(R.string.app_name));
@@ -414,9 +450,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i); t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
} }
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(GROUP_MESSAGE_NOTIFICATION_ID,
NotificationManager nm = (NotificationManager) o; b.build());
nm.notify(GROUP_MESSAGE_NOTIFICATION_ID, b.build());
} }
} }
@@ -455,7 +490,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
clearForumPostNotification(); clearForumPostNotification();
} else if (settings.getBoolean(PREF_NOTIFY_FORUM, true)) { } else if (settings.getBoolean(PREF_NOTIFY_FORUM, true)) {
BriarNotificationBuilder b = BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext); new BriarNotificationBuilder(appContext, FORUM_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_forum); b.setSmallIcon(R.drawable.notification_forum);
b.setColorRes(R.color.briar_primary); b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name)); b.setContentTitle(appContext.getText(R.string.app_name));
@@ -491,9 +526,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i); t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
} }
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(FORUM_POST_NOTIFICATION_ID, b.build());
NotificationManager nm = (NotificationManager) o;
nm.notify(FORUM_POST_NOTIFICATION_ID, b.build());
} }
} }
@@ -532,7 +565,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
clearBlogPostNotification(); clearBlogPostNotification();
} else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) { } else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) {
BriarNotificationBuilder b = BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext); new BriarNotificationBuilder(appContext, BLOG_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_blog); b.setSmallIcon(R.drawable.notification_blog);
b.setColorRes(R.color.briar_primary); b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name)); b.setContentTitle(appContext.getText(R.string.app_name));
@@ -555,9 +588,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i); t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(BLOG_POST_NOTIFICATION_ID, b.build());
NotificationManager nm = (NotificationManager) o;
nm.notify(BLOG_POST_NOTIFICATION_ID, b.build());
} }
} }
@@ -577,7 +608,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@UiThread @UiThread
private void updateIntroductionNotification() { private void updateIntroductionNotification() {
BriarNotificationBuilder b = new BriarNotificationBuilder(appContext); BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext, CONTACT_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_introduction); b.setSmallIcon(R.drawable.notification_introduction);
b.setColorRes(R.color.briar_primary); b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name)); b.setContentTitle(appContext.getText(R.string.app_name));
@@ -599,9 +631,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i); t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
Object o = appContext.getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID,
NotificationManager nm = (NotificationManager) o; b.build());
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
} }
@Override @Override

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.SharedPreferences;
import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
@@ -93,6 +94,7 @@ public class AppModule {
@Override @Override
public boolean databaseExists() { public boolean databaseExists() {
// FIXME should not run on UiThread #620
if (!dir.isDirectory()) return false; if (!dir.isDirectory()) return false;
File[] files = dir.listFiles(); File[] files = dir.listFiles();
return files != null && files.length > 0; return files != null && files.length > 0;
@@ -157,6 +159,11 @@ public class AppModule {
return devConfig; return devConfig;
} }
@Provides
SharedPreferences provideSharedPreferences(Application app) {
return app.getSharedPreferences("db", MODE_PRIVATE);
}
@Provides @Provides
@Singleton @Singleton
ReferenceManager provideReferenceManager() { ReferenceManager provideReferenceManager() {
@@ -174,8 +181,11 @@ public class AppModule {
} }
@Provides @Provides
@Singleton
ScreenFilterMonitor provideScreenFilterMonitor( ScreenFilterMonitor provideScreenFilterMonitor(
LifecycleManager lifecycleManager,
ScreenFilterMonitorImpl screenFilterMonitor) { ScreenFilterMonitorImpl screenFilterMonitor) {
lifecycleManager.registerService(screenFilterMonitor);
return screenFilterMonitor; return screenFilterMonitor;
} }

View File

@@ -6,8 +6,8 @@ package org.briarproject.briar.android;
*/ */
public interface BriarApplication { public interface BriarApplication {
// This build expires on 31 December 2017 // This build expires on 30 April 2018
long EXPIRY_DATE = 1514761200 * 1000L; long EXPIRY_DATE = 1525046400 * 1000L;
AndroidComponent getApplicationComponent(); AndroidComponent getApplicationComponent();

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
@@ -7,7 +8,6 @@ import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
@@ -25,9 +25,12 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE; import static android.support.v4.app.NotificationCompat.CATEGORY_SERVICE;
import static android.support.v4.app.NotificationCompat.PRIORITY_MIN; import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET; import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
@@ -37,9 +40,21 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResul
public class BriarService extends Service { public class BriarService extends Service {
public static String EXTRA_START_RESULT =
"org.briarproject.briar.START_RESULT";
public static String EXTRA_NOTIFICATION_ID =
"org.briarproject.briar.FAILURE_NOTIFICATION_ID";
public static String EXTRA_STARTUP_FAILED =
"org.briarproject.briar.STARTUP_FAILED";
private static final int ONGOING_NOTIFICATION_ID = 1; private static final int ONGOING_NOTIFICATION_ID = 1;
private static final int FAILURE_NOTIFICATION_ID = 2; private static final int FAILURE_NOTIFICATION_ID = 2;
// Channels are sorted by channel ID in the Settings app, so use IDs
// that will sort below the main channels such as contacts
private static final String ONGOING_CHANNEL_ID = "zForegroundService";
private static final String FAILURE_CHANNEL_ID = "zStartupFailure";
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(BriarService.class.getName()); Logger.getLogger(BriarService.class.getName());
@@ -73,8 +88,27 @@ public class BriarService extends Service {
stopSelf(); stopSelf();
return; return;
} }
// Create notification channels
if (SDK_INT >= 26) {
NotificationManager nm = (NotificationManager)
getSystemService(NOTIFICATION_SERVICE);
NotificationChannel ongoingChannel = new NotificationChannel(
ONGOING_CHANNEL_ID,
getString(R.string.ongoing_notification_title),
IMPORTANCE_NONE);
ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET);
nm.createNotificationChannel(ongoingChannel);
NotificationChannel failureChannel = new NotificationChannel(
FAILURE_CHANNEL_ID,
getString(R.string.startup_failed_notification_title),
IMPORTANCE_DEFAULT);
failureChannel.setLockscreenVisibility(VISIBILITY_SECRET);
nm.createNotificationChannel(failureChannel);
}
// Show an ongoing notification that the service is running // Show an ongoing notification that the service is running
NotificationCompat.Builder b = new NotificationCompat.Builder(this); NotificationCompat.Builder b =
new NotificationCompat.Builder(this, ONGOING_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_ongoing); b.setSmallIcon(R.drawable.notification_ongoing);
b.setColor(ContextCompat.getColor(this, R.color.briar_primary)); b.setColor(ContextCompat.getColor(this, R.color.briar_primary));
b.setContentTitle(getText(R.string.ongoing_notification_title)); b.setContentTitle(getText(R.string.ongoing_notification_title));
@@ -84,7 +118,7 @@ public class BriarService extends Service {
Intent i = new Intent(this, NavDrawerActivity.class); Intent i = new Intent(this, NavDrawerActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0)); b.setContentIntent(PendingIntent.getActivity(this, 0, i, 0));
if (Build.VERSION.SDK_INT >= 21) { if (SDK_INT >= 21) {
b.setCategory(CATEGORY_SERVICE); b.setCategory(CATEGORY_SERVICE);
b.setVisibility(VISIBILITY_SECRET); b.setVisibility(VISIBILITY_SECRET);
} }
@@ -113,8 +147,8 @@ public class BriarService extends Service {
private void showStartupFailureNotification(StartResult result) { private void showStartupFailureNotification(StartResult result) {
androidExecutor.runOnUiThread(() -> { androidExecutor.runOnUiThread(() -> {
NotificationCompat.Builder b = NotificationCompat.Builder b = new NotificationCompat.Builder(
new NotificationCompat.Builder(BriarService.this); BriarService.this, FAILURE_CHANNEL_ID);
b.setSmallIcon(android.R.drawable.stat_notify_error); b.setSmallIcon(android.R.drawable.stat_notify_error);
b.setContentTitle(getText( b.setContentTitle(getText(
R.string.startup_failed_notification_title)); R.string.startup_failed_notification_title));
@@ -123,9 +157,8 @@ public class BriarService extends Service {
Intent i = new Intent(BriarService.this, Intent i = new Intent(BriarService.this,
StartupFailureActivity.class); StartupFailureActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK); i.setFlags(FLAG_ACTIVITY_NEW_TASK);
i.putExtra("briar.START_RESULT", result); i.putExtra(EXTRA_START_RESULT, result);
i.putExtra("briar.FAILURE_NOTIFICATION_ID", i.putExtra(EXTRA_NOTIFICATION_ID, FAILURE_NOTIFICATION_ID);
FAILURE_NOTIFICATION_ID);
b.setContentIntent(PendingIntent.getActivity(BriarService.this, b.setContentIntent(PendingIntent.getActivity(BriarService.this,
0, i, FLAG_UPDATE_CURRENT)); 0, i, FLAG_UPDATE_CURRENT));
Object o = getSystemService(NOTIFICATION_SERVICE); Object o = getSystemService(NOTIFICATION_SERVICE);
@@ -134,7 +167,7 @@ public class BriarService extends Service {
// Bring the dashboard to the front to clear the back stack // Bring the dashboard to the front to clear the back stack
i = new Intent(BriarService.this, NavDrawerActivity.class); i = new Intent(BriarService.this, NavDrawerActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra("briar.STARTUP_FAILED", true); i.putExtra(EXTRA_STARTUP_FAILED, true);
startActivity(i); startActivity(i);
}); });
} }

View File

@@ -1,13 +1,22 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature; import android.content.pm.Signature;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.api.android.ScreenFilterMonitor; import org.briarproject.briar.api.android.ScreenFilterMonitor;
@@ -16,23 +25,33 @@ import java.io.InputStream;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.Manifest.permission.SYSTEM_ALERT_WINDOW; import static android.Manifest.permission.SYSTEM_ALERT_WINDOW;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_CHANGED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
import static android.content.Intent.ACTION_PACKAGE_REPLACED;
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.GET_SIGNATURES; import static android.content.pm.PackageManager.GET_SIGNATURES;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@NotNullByDefault @NotNullByDefault
class ScreenFilterMonitorImpl implements ScreenFilterMonitor { class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ScreenFilterMonitorImpl.class.getName()); Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
@@ -56,54 +75,93 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
"82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" + "82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" +
"0B145B6AA192858E79020103"; "0B145B6AA192858E79020103";
private static final String PREF_KEY_ALLOWED = "allowedOverlayApps";
private final PackageManager pm; private final PackageManager pm;
private final Application app;
private final AndroidExecutor androidExecutor;
private final SharedPreferences prefs;
private final AtomicBoolean used = new AtomicBoolean(false);
// UiThread
@Nullable
private BroadcastReceiver receiver = null;
// UiThread
@Nullable
private Collection<AppDetails> cachedApps = null;
@Inject @Inject
ScreenFilterMonitorImpl(Application app) { ScreenFilterMonitorImpl(Application app, AndroidExecutor androidExecutor,
SharedPreferences prefs) {
pm = app.getPackageManager(); pm = app.getPackageManager();
this.app = app;
this.androidExecutor = androidExecutor;
this.prefs = prefs;
} }
@Override @Override
@UiThread @UiThread
public Set<String> getApps() { public Collection<AppDetails> getApps() {
Set<String> screenFilterApps = new TreeSet<>(); if (cachedApps != null) return cachedApps;
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
Collections.emptySet());
List<AppDetails> apps = new ArrayList<>();
List<PackageInfo> packageInfos = List<PackageInfo> packageInfos =
pm.getInstalledPackages(GET_PERMISSIONS); pm.getInstalledPackages(GET_PERMISSIONS);
for (PackageInfo packageInfo : packageInfos) { for (PackageInfo packageInfo : packageInfos) {
if (isOverlayApp(packageInfo)) { if (!allowed.contains(packageInfo.packageName)
String name = pkgToString(packageInfo); && isOverlayApp(packageInfo)) {
if (name != null) { String name = getAppName(packageInfo);
screenFilterApps.add(name); apps.add(new AppDetails(name, packageInfo.packageName));
} }
} }
} Collections.sort(apps, (a, b) -> a.name.compareTo(b.name));
return screenFilterApps; apps = Collections.unmodifiableList(apps);
cachedApps = apps;
return apps;
} }
// Fetches the application name for a given package. @Override
@Nullable @UiThread
private String pkgToString(PackageInfo pkgInfo) { public void allowApps(Collection<String> packageNames) {
CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo); cachedApps = null;
if (seq != null) { Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
return seq.toString(); Collections.emptySet());
Set<String> merged = new HashSet<>(allowed);
merged.addAll(packageNames);
prefs.edit().putStringSet(PREF_KEY_ALLOWED, merged).apply();
} }
return null;
// Returns the application name for a given package, or the package name
// if no application name is available
private String getAppName(PackageInfo pkgInfo) {
CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo);
return seq == null ? pkgInfo.packageName : seq.toString();
} }
// Checks if an installed package is a user app using the permission. // Checks if an installed package is a user app using the permission.
private boolean isOverlayApp(PackageInfo packageInfo) { private boolean isOverlayApp(PackageInfo packageInfo) {
int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP; int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP;
// Ignore system apps // Ignore system apps
if ((packageInfo.applicationInfo.flags & mask) != 0) { if ((packageInfo.applicationInfo.flags & mask) != 0) return false;
return false;
}
// Ignore Play Services, it's effectively a system app // Ignore Play Services, it's effectively a system app
if (isPlayServices(packageInfo.packageName)) { if (isPlayServices(packageInfo.packageName)) return false;
return false;
}
// Get permissions // Get permissions
String[] requestedPermissions = packageInfo.requestedPermissions; String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions != null) { if (requestedPermissions == null) return false;
if (SDK_INT >= 16 && SDK_INT < 23) {
// Check whether the permission has been requested and granted
int[] flags = packageInfo.requestedPermissionsFlags;
for (int i = 0; i < requestedPermissions.length; i++) {
if (requestedPermissions[i].equals(SYSTEM_ALERT_WINDOW)) {
// 'flags' may be null on Robolectric
return flags == null ||
(flags[i] & REQUESTED_PERMISSION_GRANTED) != 0;
}
}
} else {
// Check whether the permission has been requested
for (String requestedPermission : requestedPermissions) { for (String requestedPermission : requestedPermissions) {
if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) { if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) {
return true; return true;
@@ -113,6 +171,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
return false; return false;
} }
@SuppressLint("PackageManagerGetSignatures")
private boolean isPlayServices(String pkg) { private boolean isPlayServices(String pkg) {
if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false; if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false;
try { try {
@@ -135,4 +194,36 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor {
return false; return false;
} }
} }
@Override
public void startService() throws ServiceException {
if (used.getAndSet(true)) throw new IllegalStateException();
androidExecutor.runOnUiThread(() -> {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_PACKAGE_ADDED);
filter.addAction(ACTION_PACKAGE_CHANGED);
filter.addAction(ACTION_PACKAGE_REMOVED);
filter.addAction(ACTION_PACKAGE_REPLACED);
filter.addDataScheme("package");
receiver = new PackageBroadcastReceiver();
app.registerReceiver(receiver, filter);
cachedApps = null;
});
}
@Override
public void stopService() throws ServiceException {
androidExecutor.runOnUiThread(() -> {
if (receiver != null) app.unregisterReceiver(receiver);
});
}
private class PackageBroadcastReceiver extends BroadcastReceiver {
@Override
@UiThread
public void onReceive(Context context, Intent intent) {
cachedApps = null;
}
}
} }

View File

@@ -3,32 +3,37 @@ package org.briarproject.briar.android;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView;
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.BaseActivity; import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.fragment.ErrorFragment;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID;
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
public class StartupFailureActivity extends BaseActivity { public class StartupFailureActivity extends BaseActivity implements
BaseFragmentListener {
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
setContentView(R.layout.activity_startup_failure); setContentView(R.layout.activity_fragment_container);
handleIntent(getIntent()); handleIntent(getIntent());
} }
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
component.inject(this);
} }
private void handleIntent(Intent i) { private void handleIntent(Intent i) {
StartResult result = (StartResult) i.getSerializableExtra("briar.START_RESULT"); StartResult result =
int notificationId = i.getIntExtra("briar.FAILURE_NOTIFICATION_ID", -1); (StartResult) i.getSerializableExtra(EXTRA_START_RESULT);
int notificationId = i.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
// cancel notification // cancel notification
if (notificationId > -1) { if (notificationId > -1) {
@@ -38,12 +43,31 @@ public class StartupFailureActivity extends BaseActivity {
} }
// show proper error message // show proper error message
TextView view = findViewById(R.id.errorView); String errorMsg;
if (result.equals(StartResult.DB_ERROR)) { switch (result) {
view.setText(getText(R.string.startup_failed_db_error)); case DATA_TOO_OLD_ERROR:
} else if (result.equals(StartResult.SERVICE_ERROR)) { errorMsg = getString(R.string.startup_failed_db_error);
view.setText(getText(R.string.startup_failed_service_error)); break;
case DATA_TOO_NEW_ERROR:
errorMsg =
getString(R.string.startup_failed_data_too_new_error);
break;
case DB_ERROR:
errorMsg =
getString(R.string.startup_failed_data_too_old_error);
break;
case SERVICE_ERROR:
errorMsg = getString(R.string.startup_failed_service_error);
break;
default:
throw new IllegalArgumentException();
} }
showInitialFragment(ErrorFragment.newInstance(errorMsg));
}
@Override
public void runOnDbThread(Runnable runnable) {
throw new AssertionError("Deprecated and should not be used");
} }
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.activity;
import android.app.Activity; 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.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.BlogModule;
@@ -21,6 +22,7 @@ import org.briarproject.briar.android.forum.CreateForumActivity;
import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.forum.ForumListFragment; import org.briarproject.briar.android.forum.ForumListFragment;
import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.forum.ForumModule;
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;
@@ -30,6 +32,7 @@ import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
import org.briarproject.briar.android.login.AuthorNameFragment; import org.briarproject.briar.android.login.AuthorNameFragment;
import org.briarproject.briar.android.login.ChangePasswordActivity; import org.briarproject.briar.android.login.ChangePasswordActivity;
import org.briarproject.briar.android.login.DozeFragment; import org.briarproject.briar.android.login.DozeFragment;
import org.briarproject.briar.android.login.OpenDatabaseActivity;
import org.briarproject.briar.android.login.PasswordActivity; import org.briarproject.briar.android.login.PasswordActivity;
import org.briarproject.briar.android.login.PasswordFragment; import org.briarproject.briar.android.login.PasswordFragment;
import org.briarproject.briar.android.login.SetupActivity; import org.briarproject.briar.android.login.SetupActivity;
@@ -86,6 +89,8 @@ public interface ActivityComponent {
void inject(SetupActivity activity); void inject(SetupActivity activity);
void inject(OpenDatabaseActivity activity);
void inject(NavDrawerActivity activity); void inject(NavDrawerActivity activity);
void inject(PasswordActivity activity); void inject(PasswordActivity activity);
@@ -150,9 +155,13 @@ public interface ActivityComponent {
void inject(RssFeedManageActivity activity); void inject(RssFeedManageActivity activity);
void inject(StartupFailureActivity activity);
// Fragments // Fragments
void inject(AuthorNameFragment fragment); void inject(AuthorNameFragment fragment);
void inject(PasswordFragment fragment); void inject(PasswordFragment fragment);
void inject(DozeFragment fragment); void inject(DozeFragment fragment);
void inject(ContactListFragment fragment); void inject(ContactListFragment fragment);
@@ -189,4 +198,5 @@ public interface ActivityComponent {
void inject(SettingsFragment fragment); void inject(SettingsFragment fragment);
void inject(ScreenFilterDialogFragment fragment);
} }

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.activity; package org.briarproject.briar.android.activity;
import android.app.Activity; import android.app.Activity;
import android.content.SharedPreferences;
import org.briarproject.briar.android.controller.BriarController; import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.BriarControllerImpl; import org.briarproject.briar.android.controller.BriarControllerImpl;
@@ -19,7 +18,6 @@ import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import static android.content.Context.MODE_PRIVATE;
import static org.briarproject.briar.android.BriarService.BriarServiceConnection; import static org.briarproject.briar.android.BriarService.BriarServiceConnection;
@Module @Module
@@ -57,12 +55,6 @@ public class ActivityModule {
return configController; return configController;
} }
@ActivityScope
@Provides
SharedPreferences provideSharedPreferences(Activity activity) {
return activity.getSharedPreferences("db", MODE_PRIVATE);
}
@ActivityScope @ActivityScope
@Provides @Provides
PasswordController providePasswordController( PasswordController providePasswordController(

View File

@@ -4,6 +4,8 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.View; import android.view.View;
@@ -20,13 +22,15 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.widget.TapSafeFrameLayout; import org.briarproject.briar.android.widget.TapSafeFrameLayout;
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener; import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
import org.briarproject.briar.api.android.ScreenFilterMonitor; import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.android.ScreenFilterMonitor.AppDetails;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
@@ -47,7 +51,10 @@ public abstract class BaseActivity extends AppCompatActivity
private final List<ActivityLifecycleController> lifecycleControllers = private final List<ActivityLifecycleController> lifecycleControllers =
new ArrayList<>(); new ArrayList<>();
private boolean destroyed = false; private boolean destroyed = false;
private ScreenFilterDialogFragment dialogFrag;
@Nullable
private Toolbar toolbar = null;
private boolean searchedForToolbar = false;
public abstract void injectActivity(ActivityComponent component); public abstract void injectActivity(ActivityComponent component);
@@ -56,8 +63,8 @@ public abstract class BaseActivity extends AppCompatActivity
} }
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle state) {
super.onCreate(savedInstanceState); super.onCreate(state);
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
@@ -96,6 +103,16 @@ public abstract class BaseActivity extends AppCompatActivity
for (ActivityLifecycleController alc : lifecycleControllers) { for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityStart(); alc.onActivityStart();
} }
protectToolbar();
ScreenFilterDialogFragment f = findDialogFragment();
if (f != null) f.setDismissListener(this::protectToolbar);
}
@Nullable
private ScreenFilterDialogFragment findDialogFragment() {
Fragment f = getSupportFragmentManager().findFragmentByTag(
ScreenFilterDialogFragment.TAG);
return (ScreenFilterDialogFragment) f;
} }
@Override @Override
@@ -106,15 +123,6 @@ public abstract class BaseActivity extends AppCompatActivity
} }
} }
@Override
protected void onPause() {
super.onPause();
if (dialogFrag != null) {
dialogFrag.dismiss();
dialogFrag = null;
}
}
protected void showInitialFragment(BaseFragment f) { protected void showInitialFragment(BaseFragment f) {
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag()) .replace(R.id.fragmentContainer, f, f.getUniqueTag())
@@ -131,14 +139,27 @@ public abstract class BaseActivity extends AppCompatActivity
.commit(); .commit();
} }
private void showScreenFilterWarning() { private boolean showScreenFilterWarning() {
if (dialogFrag != null && dialogFrag.isVisible()) return; // If the dialog is already visible, filter the tap
Set<String> apps = screenFilterMonitor.getApps(); ScreenFilterDialogFragment f = findDialogFragment();
if (apps.isEmpty()) return; if (f != null && f.isVisible()) return false;
dialogFrag = Collection<AppDetails> apps = screenFilterMonitor.getApps();
ScreenFilterDialogFragment.newInstance(new ArrayList<>(apps)); // If all overlay apps have been allowed, allow the tap
dialogFrag.setCancelable(false); if (apps.isEmpty()) return true;
dialogFrag.show(getSupportFragmentManager(), dialogFrag.getTag()); // Show dialog unless onSaveInstanceState() has been called, see #1112
FragmentManager fm = getSupportFragmentManager();
if (!fm.isStateSaved()) {
// Create dialog
f = ScreenFilterDialogFragment.newInstance(apps);
// When dialog is dismissed, update protection of toolbar
f.setDismissListener(this::protectToolbar);
// Hide soft keyboard when (re)showing dialog
View focus = getCurrentFocus();
if (focus != null) hideSoftKeyboard(focus);
f.show(fm, ScreenFilterDialogFragment.TAG);
}
// Filter the tap
return false;
} }
@Override @Override
@@ -192,15 +213,25 @@ public abstract class BaseActivity extends AppCompatActivity
* is outside the wrapper. * is outside the wrapper.
*/ */
private void protectToolbar() { private void protectToolbar() {
View decorView = getWindow().getDecorView(); findToolbar();
if (decorView instanceof ViewGroup) { if (toolbar != null) {
Toolbar toolbar = findToolbar((ViewGroup) decorView); boolean filter = !screenFilterMonitor.getApps().isEmpty();
if (toolbar != null) toolbar.setFilterTouchesWhenObscured(true); UiUtils.setFilterTouchesWhenObscured(toolbar, filter);
} }
} }
private void findToolbar() {
if (searchedForToolbar) return;
View decorView = getWindow().getDecorView();
if (decorView instanceof ViewGroup)
toolbar = findToolbar((ViewGroup) decorView);
searchedForToolbar = true;
}
@Nullable @Nullable
private Toolbar findToolbar(ViewGroup vg) { private Toolbar findToolbar(ViewGroup vg) {
// Views inside tap-safe layouts are already protected
if (vg instanceof TapSafeFrameLayout) return null;
for (int i = 0, len = vg.getChildCount(); i < len; i++) { for (int i = 0, len = vg.getChildCount(); i < len; i++) {
View child = vg.getChildAt(i); View child = vg.getChildAt(i);
if (child instanceof Toolbar) return (Toolbar) child; if (child instanceof Toolbar) return (Toolbar) child;
@@ -220,23 +251,20 @@ public abstract class BaseActivity extends AppCompatActivity
@Override @Override
public void setContentView(View v) { public void setContentView(View v) {
super.setContentView(makeTapSafeWrapper(v)); super.setContentView(makeTapSafeWrapper(v));
protectToolbar();
} }
@Override @Override
public void setContentView(View v, LayoutParams layoutParams) { public void setContentView(View v, LayoutParams layoutParams) {
super.setContentView(makeTapSafeWrapper(v), layoutParams); super.setContentView(makeTapSafeWrapper(v), layoutParams);
protectToolbar();
} }
@Override @Override
public void addContentView(View v, LayoutParams layoutParams) { public void addContentView(View v, LayoutParams layoutParams) {
super.addContentView(makeTapSafeWrapper(v), layoutParams); super.addContentView(makeTapSafeWrapper(v), layoutParams);
protectToolbar();
} }
@Override @Override
public void onTapFiltered() { public boolean shouldAllowTap() {
showScreenFilterWarning(); return showScreenFilterWarning();
} }
} }

View File

@@ -31,11 +31,11 @@ import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent; import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
@SuppressLint("Registered") @SuppressLint("Registered")
public abstract class BriarActivity extends BaseActivity { public abstract class BriarActivity extends BaseActivity {
public static final String KEY_STARTUP_FAILED = "briar.STARTUP_FAILED";
public static final String GROUP_ID = "briar.GROUP_ID"; public static final String GROUP_ID = "briar.GROUP_ID";
public static final String GROUP_NAME = "briar.GROUP_NAME"; public static final String GROUP_NAME = "briar.GROUP_NAME";
@@ -79,6 +79,10 @@ public abstract class BriarActivity extends BaseActivity {
public void setSceneTransitionAnimation() { public void setSceneTransitionAnimation() {
if (SDK_INT < 21) return; if (SDK_INT < 21) return;
// workaround for #1007
if (isSamsung7()) {
return;
}
Transition slide = new Slide(Gravity.RIGHT); Transition slide = new Slide(Gravity.RIGHT);
slide.excludeTarget(android.R.id.statusBarBackground, true); slide.excludeTarget(android.R.id.statusBarBackground, true);
slide.excludeTarget(android.R.id.navigationBarBackground, true); slide.excludeTarget(android.R.id.navigationBarBackground, true);
@@ -126,8 +130,8 @@ public abstract class BriarActivity extends BaseActivity {
b.setNegativeButton(R.string.cancel, b.setNegativeButton(R.string.cancel,
(dialog, which) -> dialog.dismiss()); (dialog, which) -> dialog.dismiss());
b.setOnDismissListener(dialog -> { b.setOnDismissListener(dialog -> {
CheckBox checkBox = (CheckBox) ((AlertDialog) dialog) CheckBox checkBox =
.findViewById(R.id.checkbox); ((AlertDialog) dialog).findViewById(R.id.checkbox);
if (checkBox.isChecked()) if (checkBox.isChecked())
briarController.doNotAskAgainForDozeWhiteListing(); briarController.doNotAskAgainForDozeWhiteListing();
}); });

View File

@@ -11,5 +11,6 @@ public interface RequestCodes {
int REQUEST_RINGTONE = 7; int REQUEST_RINGTONE = 7;
int REQUEST_PERMISSION_CAMERA = 8; int REQUEST_PERMISSION_CAMERA = 8;
int REQUEST_DOZE_WHITELISTING = 9; int REQUEST_DOZE_WHITELISTING = 9;
int REQUEST_ENABLE_BLUETOOTH = 10;
} }

View File

@@ -73,7 +73,7 @@ abstract class BasePostFragment extends BaseFragment {
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
getContext().startActivity(i); getContext().startActivity(i);
} }
}); }, getFragmentManager());
return view; return view;
} }

View File

@@ -91,7 +91,8 @@ public class BlogFragment extends BaseFragment
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(getActivity(), this); adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter); list.setAdapter(adapter);

View File

@@ -1,21 +1,30 @@
package org.briarproject.briar.android.blog; package org.briarproject.briar.android.blog;
import android.content.Context; import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter; import org.briarproject.briar.android.util.BriarAdapter;
class BlogPostAdapter @MethodsNotNullByDefault
extends BriarAdapter<BlogPostItem, BlogPostViewHolder> { @ParametersNotNullByDefault
class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
private final OnBlogPostClickListener listener; private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) { BlogPostAdapter(Context ctx, OnBlogPostClickListener listener,
@Nullable FragmentManager fragmentManager) {
super(ctx, BlogPostItem.class); super(ctx, BlogPostItem.class);
this.listener = listener; this.listener = listener;
this.fragmentManager = fragmentManager;
} }
@Override @Override
@@ -23,8 +32,7 @@ class BlogPostAdapter
int viewType) { int viewType) {
View v = LayoutInflater.from(ctx).inflate( View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_blog_post, parent, false); R.layout.list_item_blog_post, parent, false);
BlogPostViewHolder ui = new BlogPostViewHolder(v, false, listener); return new BlogPostViewHolder(v, false, listener, fragmentManager);
return ui;
} }
@Override @Override

View File

@@ -8,6 +8,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.Spanned; import android.text.Spanned;
@@ -34,6 +35,7 @@ import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH; import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.getSpanned; import static org.briarproject.briar.android.util.UiUtils.getSpanned;
import static org.briarproject.briar.android.util.UiUtils.getTeaser; import static org.briarproject.briar.android.util.UiUtils.getTeaser;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable; import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.briar.api.blog.MessageType.POST; import static org.briarproject.briar.api.blog.MessageType.POST;
@@ -51,12 +53,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
@NonNull @NonNull
private final OnBlogPostClickListener listener; private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostViewHolder(View v, boolean fullText, BlogPostViewHolder(View v, boolean fullText,
@NonNull OnBlogPostClickListener listener) { @NonNull OnBlogPostClickListener listener,
@Nullable FragmentManager fragmentManager) {
super(v); super(v);
this.fullText = fullText; this.fullText = fullText;
this.listener = listener; this.listener = listener;
this.fragmentManager = fragmentManager;
ctx = v.getContext(); ctx = v.getContext();
layout = v.findViewById(R.id.postLayout); layout = v.findViewById(R.id.postLayout);
@@ -116,7 +122,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
if (fullText) { if (fullText) {
body.setText(bodyText); body.setText(bodyText);
body.setTextIsSelectable(true); body.setTextIsSelectable(true);
makeLinksClickable(body); makeLinksClickable(body, fragmentManager);
} else { } else {
body.setTextIsSelectable(false); body.setTextIsSelectable(false);
if (bodyText.length() > TEASER_LENGTH) if (bodyText.length() > TEASER_LENGTH)
@@ -130,7 +136,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
i.putExtra(GROUP_ID, item.getGroupId().getBytes()); i.putExtra(GROUP_ID, item.getGroupId().getBytes());
i.putExtra(POST_ID, item.getId().getBytes()); i.putExtra(POST_ID, item.getId().getBytes());
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23 && !isSamsung7()) {
ActivityOptionsCompat options = ActivityOptionsCompat options =
makeSceneTransitionAnimation((Activity) ctx, layout, makeSceneTransitionAnimation((Activity) ctx, layout,
getTransitionName(item.getId())); getTransitionName(item.getId()));
@@ -138,6 +144,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
options.toBundle()); options.toBundle());
} else { } else {
// work-around for android bug #224270 // work-around for android bug #224270
// work-around for Samsung Android 7 bug #1007
ctx.startActivity(i); ctx.startActivity(i);
} }
}); });

View File

@@ -74,7 +74,8 @@ public class FeedFragment extends BaseFragment implements
View v = inflater.inflate(R.layout.fragment_blog, container, false); View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(getActivity(), this); adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
layoutManager = new LinearLayoutManager(getActivity()); layoutManager = new LinearLayoutManager(getActivity());
list = v.findViewById(R.id.postList); list = v.findViewById(R.id.postList);

View File

@@ -146,7 +146,7 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
ui.input.setVisibility(VISIBLE); ui.input.setVisibility(VISIBLE);
} }
private static class ViewHolder { private class ViewHolder {
private final ScrollView scrollView; private final ScrollView scrollView;
private final ProgressBar progressBar; private final ProgressBar progressBar;
@@ -167,7 +167,7 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
public void onAuthorClick(BlogPostItem post) { public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here // probably don't want to allow author clicks here
} }
}); }, getFragmentManager());
input = v.findViewById(R.id.inputText); input = v.findViewById(R.id.inputText);
} }
} }

View File

@@ -17,4 +17,7 @@ public interface ConfigController {
void deleteAccount(Context ctx); void deleteAccount(Context ctx);
boolean accountExists(); boolean accountExists();
boolean accountSignedIn();
} }

View File

@@ -52,4 +52,10 @@ public class ConfigControllerImpl implements ConfigController {
String hex = getEncryptedDatabaseKey(); String hex = getEncryptedDatabaseKey();
return hex != null && databaseConfig.databaseExists(); return hex != null && databaseConfig.databaseExists();
} }
@Override
public boolean accountSignedIn() {
return databaseConfig.getEncryptionKey() != null;
}
} }

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ErrorFragment extends BaseFragment {
private static final String TAG = ErrorFragment.class.getSimpleName();
private static final String ERROR_MSG = "errorMessage";
public static ErrorFragment newInstance(String message) {
ErrorFragment f = new ErrorFragment();
Bundle args = new Bundle();
args.putString(ERROR_MSG, message);
f.setArguments(args);
return f;
}
private String errorMessage;
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args == null) throw new AssertionError();
errorMessage = args.getString(ERROR_MSG);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_error, container, false);
TextView msg = v.findViewById(R.id.errorMessage);
msg.setText(errorMessage);
return v;
}
@Override
public void injectFragment(ActivityComponent component) {
// not necessary
}
}

View File

@@ -1,40 +1,106 @@
package org.briarproject.briar.android.fragment; package org.briarproject.briar.android.fragment;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.android.ScreenFilterMonitor.AppDetails;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject;
@NotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ScreenFilterDialogFragment extends DialogFragment { public class ScreenFilterDialogFragment extends DialogFragment {
public static final String TAG = ScreenFilterDialogFragment.class.getName();
@Inject
ScreenFilterMonitor screenFilterMonitor;
DismissListener dismissListener = null;
public static ScreenFilterDialogFragment newInstance( public static ScreenFilterDialogFragment newInstance(
ArrayList<String> apps) { Collection<AppDetails> apps) {
ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment(); ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putStringArrayList("apps", apps); ArrayList<String> appNames = new ArrayList<>();
for (AppDetails a : apps) appNames.add(a.name);
args.putStringArrayList("appNames", appNames);
ArrayList<String> packageNames = new ArrayList<>();
for (AppDetails a : apps) packageNames.add(a.packageName);
args.putStringArrayList("packageNames", packageNames);
frag.setArguments(args); frag.setArguments(args);
return frag; return frag;
} }
public void setDismissListener(DismissListener dismissListener) {
this.dismissListener = dismissListener;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Activity activity = getActivity();
if (activity == null) throw new IllegalStateException();
((BaseActivity) activity).getActivityComponent().inject(this);
}
@Override @Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), Activity activity = getActivity();
if (activity == null) throw new IllegalStateException();
AlertDialog.Builder builder = new AlertDialog.Builder(activity,
R.style.BriarDialogThemeNoFilter); R.style.BriarDialogThemeNoFilter);
builder.setTitle(R.string.screen_filter_title); builder.setTitle(R.string.screen_filter_title);
ArrayList<String> apps = getArguments().getStringArrayList("apps"); Bundle args = getArguments();
builder.setMessage(getString(R.string.screen_filter_body, if (args == null) throw new IllegalStateException();
TextUtils.join("\n", apps))); ArrayList<String> appNames = args.getStringArrayList("appNames");
builder.setNeutralButton(R.string.continue_button, ArrayList<String> packageNames =
(dialog, which) -> dialog.dismiss()); args.getStringArrayList("packageNames");
if (appNames == null || packageNames == null)
throw new IllegalStateException();
LayoutInflater inflater = activity.getLayoutInflater();
// See https://stackoverflow.com/a/24720976/6314875
@SuppressLint("InflateParams")
View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null);
builder.setView(dialogView);
TextView message = dialogView.findViewById(R.id.screen_filter_message);
message.setText(getString(R.string.screen_filter_body,
TextUtils.join("\n", appNames)));
CheckBox allow = dialogView.findViewById(R.id.screen_filter_checkbox);
builder.setNeutralButton(R.string.continue_button, (dialog, which) -> {
if (allow.isChecked()) screenFilterMonitor.allowApps(packageNames);
dialog.dismiss();
});
builder.setCancelable(false);
return builder.create(); return builder.create();
} }
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (dismissListener != null) dismissListener.onDialogDismissed();
}
public interface DismissListener {
void onDialogDismissed();
}
} }

View File

@@ -6,13 +6,13 @@ import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo; import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters; import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size; import android.hardware.Camera.Size;
import android.os.Build;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.SurfaceView; import android.view.SurfaceView;
import android.view.View;
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;
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT; import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
import static android.hardware.Camera.Parameters.FLASH_MODE_OFF; import static android.hardware.Camera.Parameters.FLASH_MODE_OFF;
import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO; import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
@@ -31,6 +32,7 @@ import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO; import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO; import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE; import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -38,18 +40,27 @@ import static java.util.logging.Level.WARNING;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class CameraView extends SurfaceView implements SurfaceHolder.Callback, public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
AutoFocusCallback { AutoFocusCallback, View.OnClickListener {
// Heuristic for the ideal preview size - small previews don't have enough
// detail, large previews are slow to decode
private static final int IDEAL_PIXELS = 500 * 1000;
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(CameraView.class.getName()); Logger.getLogger(CameraView.class.getName());
private final Runnable autoFocusRetry = this::retryAutoFocus;
@Nullable @Nullable
private Camera camera = null; private Camera camera = null;
private int cameraIndex = 0;
private PreviewConsumer previewConsumer = null; private PreviewConsumer previewConsumer = null;
private Surface surface = null; private Surface surface = null;
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0; private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
private boolean previewStarted = false, autoFocus = false; private boolean previewStarted = false;
private boolean autoFocusSupported = false, autoFocusRunning = false;
public CameraView(Context context) { public CameraView(Context context) {
super(context); super(context);
@@ -74,6 +85,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
super.onAttachedToWindow(); super.onAttachedToWindow();
setKeepScreenOn(true); setKeepScreenOn(true);
getHolder().addCallback(this); getHolder().addCallback(this);
setOnClickListener(this);
} }
@Override @Override
@@ -84,15 +96,32 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} }
@UiThread @UiThread
public void start() throws CameraException { public void start(int rotationDegrees) throws CameraException {
LOG.info("Opening camera"); LOG.info("Opening camera");
try { try {
camera = Camera.open(); int cameras = Camera.getNumberOfCameras();
if (cameras == 0) throw new CameraException("No camera");
// Try to find a back-facing camera
for (int i = 0; i < cameras; i++) {
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == CAMERA_FACING_BACK) {
LOG.info("Using back-facing camera");
camera = Camera.open(i);
cameraIndex = i;
break;
}
}
// If we can't find a back-facing camera, use a front-facing one
if (camera == null) {
LOG.info("Using front-facing camera");
camera = Camera.open(0);
cameraIndex = 0;
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
if (camera == null) throw new CameraException("No back-facing camera"); setDisplayOrientation(rotationDegrees);
setDisplayOrientation(0);
// Use barcode scene mode if it's available // Use barcode scene mode if it's available
Parameters params = camera.getParameters(); Parameters params = camera.getParameters();
params = setSceneMode(camera, params); params = setSceneMode(camera, params);
@@ -157,36 +186,54 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void startConsumer() throws CameraException { private void startConsumer() throws CameraException {
if (camera == null) throw new CameraException("Camera is null"); if (camera == null) throw new CameraException("Camera is null");
if (autoFocus) { startAutoFocus();
previewConsumer.start(camera, cameraIndex);
}
@UiThread
private void startAutoFocus() throws CameraException {
if (camera != null && autoFocusSupported && !autoFocusRunning) {
try { try {
removeCallbacks(autoFocusRetry);
camera.autoFocus(this); camera.autoFocus(this);
autoFocusRunning = true;
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
} }
previewConsumer.start(camera);
} }
@UiThread @UiThread
private void stopConsumer() throws CameraException { private void stopConsumer() throws CameraException {
if (camera == null) throw new CameraException("Camera is null"); if (camera == null) throw new CameraException("Camera is null");
if (autoFocus) { cancelAutoFocus();
try {
camera.cancelAutoFocus();
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
previewConsumer.stop(); previewConsumer.stop();
} }
@UiThread
private void cancelAutoFocus() throws CameraException {
if (camera != null && autoFocusSupported && autoFocusRunning) {
try {
removeCallbacks(autoFocusRetry);
camera.cancelAutoFocus();
autoFocusRunning = false;
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
}
/**
* See {@link Camera#setDisplayOrientation(int)}.
*/
@UiThread @UiThread
private void setDisplayOrientation(int rotationDegrees) private void setDisplayOrientation(int rotationDegrees)
throws CameraException { throws CameraException {
if (camera == null) throw new CameraException("Camera is null");
int orientation; int orientation;
CameraInfo info = new CameraInfo(); CameraInfo info = new CameraInfo();
try { try {
Camera.getCameraInfo(0, info); Camera.getCameraInfo(cameraIndex, info);
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
@@ -196,9 +243,11 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} else { } else {
orientation = (info.orientation - rotationDegrees + 360) % 360; orientation = (info.orientation - rotationDegrees + 360) % 360;
} }
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO)) {
LOG.info("Display orientation " + orientation + " degrees"); LOG.info("Screen rotation " + rotationDegrees
if (camera == null) throw new CameraException("Camera is null"); + " degrees, camera orientation " + orientation
+ " degrees");
}
try { try {
camera.setDisplayOrientation(orientation); camera.setDisplayOrientation(orientation);
} catch (RuntimeException e) { } catch (RuntimeException e) {
@@ -266,8 +315,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void setVideoStabilisation(Parameters params) { private void setVideoStabilisation(Parameters params) {
if (Build.VERSION.SDK_INT >= 15 && if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) {
params.isVideoStabilizationSupported()) {
params.setVideoStabilization(true); params.setVideoStabilization(true);
} }
} }
@@ -294,6 +342,8 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void setPreviewSize(Parameters params) { private void setPreviewSize(Parameters params) {
if (surfaceWidth == 0 || surfaceHeight == 0) return; if (surfaceWidth == 0 || surfaceHeight == 0) return;
// Choose a preview size that's close to the aspect ratio of the
// surface and close to the ideal size for decoding
float idealRatio = (float) surfaceWidth / surfaceHeight; float idealRatio = (float) surfaceWidth / surfaceHeight;
boolean rotatePreview = displayOrientation % 180 == 90; boolean rotatePreview = displayOrientation % 180 == 90;
List<Size> sizes = params.getSupportedPreviewSizes(); List<Size> sizes = params.getSupportedPreviewSizes();
@@ -304,11 +354,12 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
int height = rotatePreview ? size.width : size.height; int height = rotatePreview ? size.width : size.height;
float ratio = (float) width / height; float ratio = (float) width / height;
float stretch = Math.max(ratio / idealRatio, idealRatio / ratio); float stretch = Math.max(ratio / idealRatio, idealRatio / ratio);
int pixels = width * height; float pixels = width * height;
float score = pixels / stretch; float zoom = Math.max(pixels / IDEAL_PIXELS, IDEAL_PIXELS / pixels);
float score = 1 / (stretch * zoom);
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("Size " + size.width + "x" + size.height LOG.info("Size " + size.width + "x" + size.height
+ ", stretch " + stretch + ", pixels " + pixels + ", stretch " + stretch + ", zoom " + zoom
+ ", score " + score); + ", score " + score);
} }
if (bestSize == null || score > bestScore) { if (bestSize == null || score > bestScore) {
@@ -325,7 +376,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void enableAutoFocus(String focusMode) { private void enableAutoFocus(String focusMode) {
autoFocus = FOCUS_MODE_AUTO.equals(focusMode) || autoFocusSupported = FOCUS_MODE_AUTO.equals(focusMode) ||
FOCUS_MODE_MACRO.equals(focusMode); FOCUS_MODE_MACRO.equals(focusMode);
} }
@@ -339,7 +390,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
if (Build.VERSION.SDK_INT >= 15) { if (SDK_INT >= 15) {
LOG.info("Video stabilisation enabled: " LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization()); + params.getVideoStabilization());
} }
@@ -370,8 +421,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
surface.release(); surface.release();
} }
surface = holder.getSurface(); surface = holder.getSurface();
// Start the preview when the camera and the surface are both ready // We'll start the preview when surfaceChanged() is called
if (camera != null && !previewStarted) startPreview(holder);
} }
@Override @Override
@@ -397,7 +447,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
surfaceWidth = w; surfaceWidth = w;
surfaceHeight = h; surfaceHeight = h;
if (camera == null) return; // We are stopped if (camera == null) return; // We are stopped
stopPreview(); if (previewStarted) stopPreview();
try { try {
Parameters params = camera.getParameters(); Parameters params = camera.getParameters();
setPreviewSize(params); setPreviewSize(params);
@@ -427,16 +477,23 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@Override @Override
public void onAutoFocus(boolean success, Camera camera) { public void onAutoFocus(boolean success, Camera camera) {
if (LOG.isLoggable(INFO))
LOG.info("Auto focus succeeded: " + success); LOG.info("Auto focus succeeded: " + success);
postDelayed(this::retryAutoFocus, AUTO_FOCUS_RETRY_DELAY); autoFocusRunning = false;
postDelayed(autoFocusRetry, AUTO_FOCUS_RETRY_DELAY);
} }
@UiThread @UiThread
private void retryAutoFocus() { private void retryAutoFocus() {
try { try {
if (camera != null) camera.autoFocus(this); startAutoFocus();
} catch (RuntimeException e) { } catch (CameraException e) {
LOG.log(WARNING, "Error retrying auto focus", e); LOG.log(WARNING, e.toString(), e);
} }
} }
@Override
public void onClick(View v) {
retryAutoFocus();
}
} }

View File

@@ -1,6 +1,11 @@
package org.briarproject.briar.android.keyagreement; package org.briarproject.briar.android.keyagreement;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
@@ -23,6 +28,7 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;
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.plugin.event.BluetoothEnabledEvent;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.R.string; import org.briarproject.briar.R.string;
import org.briarproject.briar.R.style; import org.briarproject.briar.R.style;
@@ -39,9 +45,15 @@ import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.CAMERA;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ENABLE_BLUETOOTH;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -50,6 +62,10 @@ public class KeyAgreementActivity extends BriarActivity implements
BaseFragmentListener, IntroScreenSeenListener, EventListener, BaseFragmentListener, IntroScreenSeenListener, EventListener,
ContactExchangeListener { ContactExchangeListener {
private enum BluetoothState {
UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED
}
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(KeyAgreementActivity.class.getName()); Logger.getLogger(KeyAgreementActivity.class.getName());
@@ -62,7 +78,10 @@ public class KeyAgreementActivity extends BriarActivity implements
@Inject @Inject
volatile IdentityManager identityManager; volatile IdentityManager identityManager;
private boolean isResumed = false, enableWasRequested = false;
private boolean continueClicked, gotCameraPermission; private boolean continueClicked, gotCameraPermission;
private BluetoothState bluetoothState = BluetoothState.UNKNOWN;
private BroadcastReceiver bluetoothReceiver = null;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -84,6 +103,15 @@ public class KeyAgreementActivity extends BriarActivity implements
if (state == null) { if (state == null) {
showInitialFragment(IntroFragment.newInstance()); showInitialFragment(IntroFragment.newInstance());
} }
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
bluetoothReceiver = new BluetoothStateReceiver();
registerReceiver(bluetoothReceiver, filter);
}
@Override
public void onDestroy() {
super.onDestroy();
if (bluetoothReceiver != null) unregisterReceiver(bluetoothReceiver);
} }
@Override @Override
@@ -112,24 +140,72 @@ public class KeyAgreementActivity extends BriarActivity implements
@Override @Override
protected void onPostResume() { protected void onPostResume() {
super.onPostResume(); super.onPostResume();
isResumed = true;
// Workaround for // Workaround for
// https://code.google.com/p/android/issues/detail?id=190966 // https://code.google.com/p/android/issues/detail?id=190966
if (continueClicked && gotCameraPermission) { if (canShowQrCodeFragment()) showQrCodeFragment();
showQrCodeFragment();
} }
boolean canShowQrCodeFragment() {
return isResumed && continueClicked
&& (SDK_INT < 23 || gotCameraPermission)
&& bluetoothState != BluetoothState.UNKNOWN
&& bluetoothState != BluetoothState.WAITING;
}
@Override
protected void onPause() {
super.onPause();
isResumed = false;
} }
@Override @Override
public void showNextScreen() { public void showNextScreen() {
// FIXME #824
// showNextFragment(ShowQrCodeFragment.newInstance());
continueClicked = true; continueClicked = true;
if (checkPermissions()) { if (checkPermissions()) {
showQrCodeFragment(); if (shouldRequestEnableBluetooth()) requestEnableBluetooth();
else if (canShowQrCodeFragment()) showQrCodeFragment();
} }
} }
private boolean shouldRequestEnableBluetooth() {
return bluetoothState == BluetoothState.UNKNOWN
|| bluetoothState == BluetoothState.REFUSED;
}
private void requestEnableBluetooth() {
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
if (bt == null) {
setBluetoothState(BluetoothState.NO_ADAPTER);
} else if (bt.isEnabled()) {
setBluetoothState(BluetoothState.ENABLED);
} else {
enableWasRequested = true;
setBluetoothState(BluetoothState.WAITING);
Intent i = new Intent(ACTION_REQUEST_ENABLE);
startActivityForResult(i, REQUEST_ENABLE_BLUETOOTH);
}
}
private void setBluetoothState(BluetoothState bluetoothState) {
LOG.info("Setting Bluetooth state to " + bluetoothState);
this.bluetoothState = bluetoothState;
if (enableWasRequested && bluetoothState == BluetoothState.ENABLED) {
eventBus.broadcast(new BluetoothEnabledEvent());
enableWasRequested = false;
}
if (canShowQrCodeFragment()) showQrCodeFragment();
}
@Override
public void onActivityResult(int request, int result, Intent data) {
// If the request was granted we'll catch the state change event
if (request == REQUEST_ENABLE_BLUETOOTH && result == RESULT_CANCELED)
setBluetoothState(BluetoothState.REFUSED);
}
private void showQrCodeFragment() { private void showQrCodeFragment() {
// FIXME #824
BaseFragment f = ShowQrCodeFragment.newInstance(); BaseFragment f = ShowQrCodeFragment.newInstance();
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag()) .replace(R.id.fragmentContainer, f, f.getUniqueTag())
@@ -154,8 +230,10 @@ public class KeyAgreementActivity extends BriarActivity implements
} else { } else {
requestPermission(); requestPermission();
} }
gotCameraPermission = false;
return false; return false;
} else { } else {
gotCameraPermission = true;
return true; return true;
} }
} }
@@ -174,6 +252,7 @@ public class KeyAgreementActivity extends BriarActivity implements
if (grantResults.length > 0 && if (grantResults.length > 0 &&
grantResults[0] == PERMISSION_GRANTED) { grantResults[0] == PERMISSION_GRANTED) {
gotCameraPermission = true; gotCameraPermission = true;
showNextScreen();
} else { } else {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
CAMERA)) { CAMERA)) {
@@ -258,4 +337,14 @@ public class KeyAgreementActivity extends BriarActivity implements
finish(); finish();
}); });
} }
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if (state == STATE_ON) setBluetoothState(BluetoothState.ENABLED);
else setBluetoothState(BluetoothState.UNKNOWN);
}
}
} }

View File

@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
interface PreviewConsumer { interface PreviewConsumer {
@UiThread @UiThread
void start(Camera camera); void start(Camera camera, int cameraIndex);
@UiThread @UiThread
void stop(); void stop();

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.keyagreement; package org.briarproject.briar.android.keyagreement;
import android.hardware.Camera; import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size; import android.hardware.Camera.Size;
import android.os.AsyncTask; import android.os.AsyncTask;
@@ -36,20 +37,23 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
private final ResultCallback callback; private final ResultCallback callback;
private Camera camera = null; private Camera camera = null;
private int cameraIndex = 0;
QrCodeDecoder(ResultCallback callback) { QrCodeDecoder(ResultCallback callback) {
this.callback = callback; this.callback = callback;
} }
@Override @Override
public void start(Camera camera) { public void start(Camera camera, int cameraIndex) {
this.camera = camera; this.camera = camera;
this.cameraIndex = cameraIndex;
askForPreviewFrame(); askForPreviewFrame();
} }
@Override @Override
public void stop() { public void stop() {
camera = null; camera = null;
cameraIndex = 0;
} }
@UiThread @UiThread
@@ -61,45 +65,64 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@Override @Override
public void onPreviewFrame(byte[] data, Camera camera) { public void onPreviewFrame(byte[] data, Camera camera) {
if (camera == this.camera) { if (camera == this.camera) {
LOG.info("Got preview frame");
try { try {
Size size = camera.getParameters().getPreviewSize(); Size size = camera.getParameters().getPreviewSize();
new DecoderTask(data, size.width, size.height).execute(); // The preview should be in NV21 format: width * height bytes of
// Y followed by width * height / 2 bytes of interleaved U and V
if (data.length == size.width * size.height * 3 / 2) {
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(cameraIndex, info);
new DecoderTask(data, size.width, size.height,
info.orientation).execute();
} else {
// Camera parameters have changed - ask for a new preview
LOG.info("Preview size does not match camera parameters");
askForPreviewFrame();
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.log(WARNING, "Error getting camera parameters.", e); LOG.log(WARNING, "Error getting camera parameters.", e);
} }
} else {
LOG.info("Camera has changed, ignoring preview frame");
} }
} }
private class DecoderTask extends AsyncTask<Void, Void, Void> { private class DecoderTask extends AsyncTask<Void, Void, Void> {
private final byte[] data; private final byte[] data;
private final int width, height; private final int width, height, orientation;
private DecoderTask(byte[] data, int width, int height) { private DecoderTask(byte[] data, int width, int height,
int orientation) {
this.data = data; this.data = data;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.orientation = orientation;
} }
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
LuminanceSource src = new PlanarYUVLuminanceSource(data, width, BinaryBitmap bitmap = binarize(data, width, height, orientation);
height, 0, 0, width, height, false);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
Result result = null; Result result = null;
try { try {
result = reader.decode(bitmap); result = reader.decode(bitmap);
} catch (ReaderException e) {
return null; // No barcode found
} catch (RuntimeException e) {
return null; // Preview data did not match width and height
} finally {
reader.reset();
}
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Decoding barcode took " + duration + " ms"); LOG.info("Decoding barcode took " + duration + " ms");
} catch (ReaderException e) {
// No barcode found
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("No barcode found after " + duration + " ms");
return null;
} catch (RuntimeException e) {
LOG.warning("Invalid preview frame");
return null;
} finally {
reader.reset();
}
callback.handleResult(result); callback.handleResult(result);
return null; return null;
} }
@@ -110,6 +133,19 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
} }
} }
private static BinaryBitmap binarize(byte[] data, int width, int height,
int orientation) {
// Crop to a square at the top (portrait) or left (landscape) of the
// screen - this will be faster to decode and should include
// everything visible in the viewfinder
int crop = Math.min(width, height);
int left = orientation >= 180 ? width - crop : 0;
int top = orientation >= 180 ? height - crop : 0;
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
height, left, top, crop, crop, false);
return new BinaryBitmap(new HybridBinarizer(src));
}
@NotNullByDefault @NotNullByDefault
interface ResultCallback { interface ResultCallback {

View File

@@ -32,21 +32,24 @@ class QrCodeUtils {
// Generate QR code // Generate QR code
BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE, BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE,
smallestDimen, smallestDimen); smallestDimen, smallestDimen);
// Convert QR code to Bitmap return renderQrCode(encoded);
int width = encoded.getWidth();
int height = encoded.getHeight();
int[] pixels = new int[width * height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pixels[y * width + x] = encoded.get(x, y) ? BLACK : WHITE;
}
}
Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888);
qr.setPixels(pixels, 0, width, 0, 0, width, height);
return qr;
} catch (WriterException e) { } catch (WriterException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null; return null;
} }
} }
private static Bitmap renderQrCode(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
int[] pixels = new int[width * height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pixels[y * width + x] = matrix.get(x, y) ? BLACK : WHITE;
}
}
Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888);
qr.setPixels(pixels, 0, width, 0, 0, width, height);
return qr;
}
} }

View File

@@ -1,17 +1,16 @@
package org.briarproject.briar.android.keyagreement; package org.briarproject.briar.android.keyagreement;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.hardware.Camera;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.Base64; import android.util.Base64;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.Display;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.AlphaAnimation; import android.view.animation.AlphaAnimation;
@@ -37,7 +36,6 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
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.plugin.event.EnableBluetoothEvent;
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.fragment.BaseEventFragment; import org.briarproject.briar.android.fragment.BaseEventFragment;
@@ -49,9 +47,6 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
@@ -86,8 +81,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
private TextView mainProgressTitle; private TextView mainProgressTitle;
private ViewGroup mainProgressContainer; private ViewGroup mainProgressContainer;
private BluetoothStateReceiver receiver; private boolean gotRemotePayload;
private boolean gotRemotePayload, waitingForBluetooth;
private volatile boolean gotLocalPayload; private volatile boolean gotLocalPayload;
private KeyAgreementTask task; private KeyAgreementTask task;
@@ -144,31 +138,36 @@ public class ShowQrCodeFragment extends BaseEventFragment
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
try { try {
cameraView.start(); cameraView.start(getScreenRotationDegrees());
} catch (CameraException e) { } catch (CameraException e) {
logCameraExceptionAndFinish(e); logCameraExceptionAndFinish(e);
} }
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
receiver = new BluetoothStateReceiver();
getActivity().registerReceiver(receiver, filter);
// Enable BT adapter if it is not already on.
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null && !adapter.isEnabled()) {
waitingForBluetooth = true;
eventBus.broadcast(new EnableBluetoothEvent());
} else {
startListening(); startListening();
} }
/**
* See {@link Camera#setDisplayOrientation(int)}.
*/
private int getScreenRotationDegrees() {
Display d = getActivity().getWindowManager().getDefaultDisplay();
switch (d.getRotation()) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
default:
throw new AssertionError();
}
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
stopListening(); stopListening();
if (receiver != null) getActivity().unregisterReceiver(receiver);
try { try {
cameraView.stop(); cameraView.stop();
} catch (CameraException e) { } catch (CameraException e) {
@@ -340,16 +339,4 @@ public class ShowQrCodeFragment extends BaseEventFragment
protected void finish() { protected void finish() {
getActivity().getSupportFragmentManager().popBackStack(); getActivity().getSupportFragmentManager().popBackStack();
} }
private class BluetoothStateReceiver extends BroadcastReceiver {
@UiThread
@Override
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if (state == STATE_ON && waitingForBluetooth) {
waitingForBluetooth = false;
startListening();
}
}
}
} }

View File

@@ -17,7 +17,7 @@ import android.widget.Toast;
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.BaseActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.controller.handler.UiResultHandler; import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
@@ -27,7 +27,7 @@ import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
public class ChangePasswordActivity extends BaseActivity public class ChangePasswordActivity extends BriarActivity
implements OnClickListener, OnEditorActionListener { implements OnClickListener, OnEditorActionListener {
@Inject @Inject

View File

@@ -1,17 +1,19 @@
package org.briarproject.briar.android.login; package org.briarproject.briar.android.login;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
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.login.PowerView.OnCheckedChangedListener;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
@@ -19,12 +21,15 @@ import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@TargetApi(23) @NotNullByDefault
public class DozeFragment extends SetupFragment { public class DozeFragment extends SetupFragment
implements OnCheckedChangedListener {
private final static String TAG = DozeFragment.class.getName(); private final static String TAG = DozeFragment.class.getName();
private Button dozeButton; private DozeView dozeView;
private HuaweiView huaweiView;
private Button next;
private ProgressBar progressBar; private ProgressBar progressBar;
private boolean secondAttempt = false; private boolean secondAttempt = false;
@@ -33,15 +38,22 @@ public class DozeFragment extends SetupFragment {
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater,
Bundle savedInstanceState) { @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
getActivity().setTitle(getString(R.string.setup_doze_title)); getActivity().setTitle(getString(R.string.setup_doze_title));
setHasOptionsMenu(false);
View v = inflater.inflate(R.layout.fragment_setup_doze, container, View v = inflater.inflate(R.layout.fragment_setup_doze, container,
false); false);
dozeButton = v.findViewById(R.id.dozeButton); dozeView = v.findViewById(R.id.dozeView);
dozeView.setOnCheckedChangedListener(this);
huaweiView = v.findViewById(R.id.huaweiView);
huaweiView.setOnCheckedChangedListener(this);
next = v.findViewById(R.id.next);
progressBar = v.findViewById(R.id.progress); progressBar = v.findViewById(R.id.progress);
dozeButton.setOnClickListener(view -> askForDozeWhitelisting()); dozeView.setOnButtonClickListener(this::askForDozeWhitelisting);
next.setOnClickListener(this);
return v; return v;
} }
@@ -65,25 +77,34 @@ public class DozeFragment extends SetupFragment {
public void onActivityResult(int request, int result, Intent data) { public void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_DOZE_WHITELISTING) { if (request == REQUEST_DOZE_WHITELISTING) {
if (!setupController.needsDozeWhitelisting() || secondAttempt) { if (!dozeView.needsToBeShown() || secondAttempt) {
dozeButton.setEnabled(false); dozeView.setChecked(true);
onClick(dozeButton); } else if (getContext() != null) {
} else {
secondAttempt = true; secondAttempt = true;
showOnboardingDialog(getContext(), getHelpText()); showOnboardingDialog(getContext(), getHelpText());
} }
} }
} }
@Override
public void onCheckedChanged() {
if (dozeView.isChecked() && huaweiView.isChecked()) {
next.setEnabled(true);
} else {
next.setEnabled(false);
}
}
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
private void askForDozeWhitelisting() { private void askForDozeWhitelisting() {
if (getContext() == null) return;
Intent i = UiUtils.getDozeWhitelistingIntent(getContext()); Intent i = UiUtils.getDozeWhitelistingIntent(getContext());
startActivityForResult(i, REQUEST_DOZE_WHITELISTING); startActivityForResult(i, REQUEST_DOZE_WHITELISTING);
} }
@Override @Override
public void onClick(View view) { public void onClick(View view) {
dozeButton.setVisibility(INVISIBLE); next.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE); progressBar.setVisibility(VISIBLE);
setupController.createAccount(); setupController.createAccount();
} }

View File

@@ -0,0 +1,60 @@
package org.briarproject.briar.android.login;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
@UiThread
@NotNullByDefault
class DozeView extends PowerView {
@Nullable
private Runnable onButtonClickListener;
public DozeView(Context context) {
this(context, null);
}
public DozeView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DozeView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
setText(R.string.setup_doze_intro);
setButtonText(R.string.setup_doze_button);
}
@Override
public boolean needsToBeShown() {
return needsToBeShown(getContext());
}
public static boolean needsToBeShown(Context context) {
return needsDozeWhitelisting(context);
}
@Override
protected int getHelpText() {
return R.string.setup_doze_explanation;
}
@Override
protected void onButtonClick() {
if (onButtonClickListener == null) throw new IllegalStateException();
onButtonClickListener.run();
}
public void setOnButtonClickListener(Runnable runnable) {
onButtonClickListener = runnable;
}
}

View File

@@ -0,0 +1,72 @@
package org.briarproject.briar.android.login;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import java.util.List;
import javax.annotation.Nullable;
@UiThread
@NotNullByDefault
class HuaweiView extends PowerView {
private final static String PACKAGE_NAME = "com.huawei.systemmanager";
private final static String CLASS_NAME =
PACKAGE_NAME + ".optimize.process.ProtectActivity";
public HuaweiView(Context context) {
this(context, null);
}
public HuaweiView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HuaweiView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
setText(R.string.setup_huawei_text);
setButtonText(R.string.setup_huawei_button);
}
@Override
public boolean needsToBeShown() {
return needsToBeShown(getContext());
}
public static boolean needsToBeShown(Context context) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(),
PackageManager.MATCH_DEFAULT_ONLY);
return !resolveInfos.isEmpty();
}
@Override
@StringRes
protected int getHelpText() {
return R.string.setup_huawei_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

@@ -0,0 +1,93 @@
package org.briarproject.briar.android.login;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
@ParametersAreNonnullByDefault
public class OpenDatabaseActivity extends BriarActivity
implements EventListener {
@Inject
LifecycleManager lifecycleManager;
@Inject
EventBus eventBus;
private TextView textView;
private ImageView imageView;
private boolean showingMigration = false;
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_open_database);
textView = findViewById(R.id.textView);
imageView = findViewById(R.id.imageView);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();
LifecycleState state = lifecycleManager.getLifecycleState();
if (state.isAfter(STARTING_SERVICES)) {
finishAndStartApp();
} else {
if (state == MIGRATING_DATABASE) showMigration();
eventBus.addListener(this);
}
}
@Override
protected void onStop() {
super.onStop();
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof LifecycleEvent) {
LifecycleState state = ((LifecycleEvent) e).getLifecycleState();
if (state.isAfter(STARTING_SERVICES))
runOnUiThreadUnlessDestroyed(this::finishAndStartApp);
else if (state == MIGRATING_DATABASE)
runOnUiThreadUnlessDestroyed(this::showMigration);
}
}
private void showMigration() {
if (showingMigration) return;
textView.setText(R.string.startup_migrate_database);
imageView.setImageResource(R.drawable.startup_migration);
showingMigration = true;
}
private void finishAndStartApp() {
startActivity(new Intent(this, NavDrawerActivity.class));
supportFinishAfterTransition();
}
}

View File

@@ -66,7 +66,7 @@ public class PasswordFragment extends SetupFragment {
component.inject(this); component.inject(this);
// the controller is not yet available in onCreateView() // the controller is not yet available in onCreateView()
if (!setupController.needsDozeWhitelisting()) { if (!setupController.needToShowDozeFragment()) {
nextButton.setText(R.string.create_account_button); nextButton.setText(R.string.create_account_button);
} }
} }
@@ -102,7 +102,7 @@ public class PasswordFragment extends SetupFragment {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (!setupController.needsDozeWhitelisting()) { if (!setupController.needToShowDozeFragment()) {
nextButton.setVisibility(INVISIBLE); nextButton.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE); progressBar.setVisibility(VISIBLE);
} }

View File

@@ -0,0 +1,162 @@
package org.briarproject.briar.android.login;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.constraint.ConstraintLayout;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@UiThread
@NotNullByDefault
abstract class PowerView extends ConstraintLayout {
private final TextView textView;
private final ImageView checkImage;
private final Button button;
private boolean checked = false;
@Nullable
private OnCheckedChangedListener onCheckedChangedListener;
public PowerView(Context context) {
this(context, null);
}
public PowerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@SuppressWarnings("ConstantConditions")
public PowerView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(LAYOUT_INFLATER_SERVICE);
View v = inflater.inflate(R.layout.power_view, this, true);
textView = v.findViewById(R.id.textView);
checkImage = v.findViewById(R.id.checkImage);
button = v.findViewById(R.id.button);
button.setOnClickListener(view -> onButtonClick());
ImageButton helpButton = v.findViewById(R.id.helpButton);
helpButton.setOnClickListener(view -> onHelpButtonClick());
// we need to manage the checkImage state ourselves, because automatic
// state saving is done based on the view's ID and there can be
// multiple ImageViews with the same ID in the view hierarchy
setSaveFromParentEnabled(true);
if (!isInEditMode() && !needsToBeShown()) {
setVisibility(GONE);
}
}
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.value = new boolean[] {checked};
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setChecked(ss.value[0]); // also calls listener
}
public abstract boolean needsToBeShown();
public void setChecked(boolean checked) {
this.checked = checked;
if (checked) {
checkImage.setImageResource(R.drawable.ic_check_white);
} else {
checkImage.setImageResource(R.drawable.contact_disconnected);
}
if (onCheckedChangedListener != null) {
onCheckedChangedListener.onCheckedChanged();
}
}
public boolean isChecked() {
return getVisibility() == GONE || checked;
}
public void setOnCheckedChangedListener(
OnCheckedChangedListener onCheckedChangedListener) {
this.onCheckedChangedListener = onCheckedChangedListener;
}
@StringRes
protected abstract int getHelpText();
protected void setText(@StringRes int res) {
textView.setText(res);
}
protected void setButtonText(@StringRes int res) {
button.setText(res);
}
protected abstract void onButtonClick();
private void onHelpButtonClick() {
showOnboardingDialog(getContext(),
getContext().getString(getHelpText()));
}
private static class SavedState extends BaseSavedState {
private boolean[] value = {false};
private SavedState(@Nullable Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
in.readBooleanArray(value);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeBooleanArray(value);
}
static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
interface OnCheckedChangedListener {
void onCheckedChanged();
}
}

View File

@@ -8,7 +8,6 @@ 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.BaseActivity; import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import javax.inject.Inject; import javax.inject.Inject;
@@ -48,7 +47,7 @@ public class SetupActivity extends BaseActivity
} }
public void showApp() { public void showApp() {
Intent i = new Intent(this, NavDrawerActivity.class); Intent i = new Intent(this, OpenDatabaseActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK); i.setFlags(FLAG_ACTIVITY_NEW_TASK);
startActivity(i); startActivity(i);
supportFinishAfterTransition(); supportFinishAfterTransition();

View File

@@ -8,7 +8,7 @@ public interface SetupController {
void setSetupActivity(SetupActivity setupActivity); void setSetupActivity(SetupActivity setupActivity);
boolean needsDozeWhitelisting(); boolean needToShowDozeFragment();
void setAuthorName(String authorName); void setAuthorName(String authorName);

View File

@@ -11,7 +11,6 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultHandler; import org.briarproject.briar.android.controller.handler.ResultHandler;
import org.briarproject.briar.android.controller.handler.UiResultHandler; import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.util.UiUtils;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -41,9 +40,10 @@ public class SetupControllerImpl extends PasswordControllerImpl
} }
@Override @Override
public boolean needsDozeWhitelisting() { public boolean needToShowDozeFragment() {
if (setupActivity == null) throw new IllegalStateException(); if (setupActivity == null) throw new IllegalStateException();
return UiUtils.needsDozeWhitelisting(setupActivity); return DozeView.needsToBeShown(setupActivity) ||
HuaweiView.needsToBeShown(setupActivity);
} }
@Override @Override
@@ -61,7 +61,7 @@ public class SetupControllerImpl extends PasswordControllerImpl
@Override @Override
public void showDozeOrCreateAccount() { public void showDozeOrCreateAccount() {
if (setupActivity == null) throw new IllegalStateException(); if (setupActivity == null) throw new IllegalStateException();
if (needsDozeWhitelisting()) { if (needToShowDozeFragment()) {
setupActivity.showDozeFragment(); setupActivity.showDozeFragment();
} else { } else {
createAccount(); createAccount();

View File

@@ -51,6 +51,7 @@ import static android.support.v4.view.GravityCompat.START;
import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED; import static android.support.v4.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO; import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE; import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE;
@@ -167,7 +168,7 @@ public class NavDrawerActivity extends BriarActivity implements
} }
private void exitIfStartupFailed(Intent intent) { private void exitIfStartupFailed(Intent intent) {
if (intent.getBooleanExtra(KEY_STARTUP_FAILED, false)) { if (intent.getBooleanExtra(EXTRA_STARTUP_FAILED, false)) {
finish(); finish();
LOG.info("Exiting"); LOG.info("Exiting");
System.exit(0); System.exit(0);
@@ -211,13 +212,6 @@ public class NavDrawerActivity extends BriarActivity implements
public void onBackPressed() { public void onBackPressed() {
if (drawerLayout.isDrawerOpen(START)) { if (drawerLayout.isDrawerOpen(START)) {
drawerLayout.closeDrawer(START); drawerLayout.closeDrawer(START);
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
getSupportFragmentManager()
.findFragmentByTag(ContactListFragment.TAG) != null) {
Intent i = new Intent(Intent.ACTION_MAIN);
i.addCategory(Intent.CATEGORY_HOME);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0 && } else if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
getSupportFragmentManager() getSupportFragmentManager()
.findFragmentByTag(ContactListFragment.TAG) == null) { .findFragmentByTag(ContactListFragment.TAG) == null) {

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