Compare commits

...

67 Commits

Author SHA1 Message Date
Michael Rogers
ef2286ab53 Bumped version number for beta release. 2017-09-20 14:51:10 +01:00
akwizgran
47b25f3221 Merge branch '1064-rss-date-npe' into 'master'
Fix NPE when some RSS items don't have dates and add test

Closes #1064

See merge request !591
2017-09-20 12:21:06 +00:00
Torsten Grote
c30bfa12ce Fix NPE when some RSS items don't have dates and add test 2017-09-20 09:11:06 -03:00
akwizgran
d0fc04251d Merge branch 'three-new-langs' into 'master'
Add Norwegian Bokmål, Occitan (post 1500) and Serbian

See merge request !593
2017-09-20 11:15:44 +00:00
akwizgran
dcbb41eb7a Merge branch '1069-forum-sharing-exception' into 'master'
Fix crash when sharing a forum while it was just shared with us

Closes #1069

See merge request !592
2017-09-20 11:14:20 +00:00
Torsten Grote
999bdf8866 Add Norwegian Bokmål, Occitan (post 1500) and Serbian 2017-09-19 14:47:39 -03:00
Torsten Grote
911c0c0fd9 Fix crash when sharing a forum while it was just shared with us 2017-09-19 14:30:57 -03:00
akwizgran
99d8cc64a6 Merge branch '1024-message-tree-npe' into 'master'
Don't add threaded messages to the UI before their parents

Closes #1024

See merge request !585
2017-09-19 15:37:58 +00:00
akwizgran
ba727d7568 Don't add threaded messages to the UI before their parents. 2017-09-19 16:31:27 +01:00
Torsten Grote
ed01048f9f Merge branch 'remove-old-bluetooth-code' into 'master'
Remove old Bluetooth code and location permission

See merge request !584
2017-09-19 14:16:13 +00:00
Torsten Grote
043ee3c58e Merge branch '1044-crash-when-setting-ringtone' into 'master'
Don't crash if the chosen ringtone can't be loaded

Closes #1044

See merge request !586
2017-09-19 13:11:44 +00:00
Torsten Grote
6e0af7deda Merge branch '1060-upgrade-tor' into 'master'
Upgrade Tor to 0.2.9.12

Closes #1060

See merge request !590
2017-09-19 12:14:55 +00:00
akwizgran
9591db2097 Upgrade Tor to 0.2.9.12.
Libevent 2.0.22-stable, OpenSSL 1.0.2l and GeoIP 2017-09-06.
2017-09-19 12:49:22 +01:00
akwizgran
329a4c64f6 Merge branch '1028-lost-reply-id' into 'master'
Keep the reply ID up to date in ThreadListActivity

Closes #1028

See merge request !587
2017-09-18 15:10:38 +00:00
Torsten Grote
79015bc5ae Merge branch '1042-catch-npe-when-getting-socket-streams' into 'master'
Catch NPE when getting socket input/output streams

Closes #1042

See merge request !589
2017-09-18 14:55:08 +00:00
akwizgran
27422ab9f9 Catch NPE when getting socket input/output streams.
Works around a bug in Android 7, fixed in 7.1.
2017-09-18 15:47:12 +01:00
Torsten Grote
abcb682498 Merge branch '1040-rss-feed-illegal-argument-exception' into 'master'
Catch IllegalArgumentException when parsing RSS feed

Closes #1040

See merge request !588
2017-09-18 14:38:22 +00:00
akwizgran
5044127c46 Catch IllegalArgumentException when parsing RSS feed. 2017-09-18 15:26:12 +01:00
akwizgran
0e4b8ca62e Keep the activity's reply ID up to date. 2017-09-18 15:13:16 +01:00
akwizgran
822017c69c Don't crash if the chosen ringtone can't be loaded. 2017-09-18 13:37:10 +01:00
akwizgran
eb6561b93d Updated translations for German, French and Russian. 2017-09-15 10:40:05 +01:00
Michael Rogers
d24b1884a2 Removed old Bluetooth code and the location permission it requires. 2017-08-11 12:42:47 +01:00
Michael Rogers
078534889e Bumped version number for beta release. 2017-08-04 15:16:51 +01:00
Torsten Grote
e92713006a Fix string in Spanish translation 2017-08-04 10:57:43 -03:00
akwizgran
18f43f3bc1 Merge branch '871-rss-feeds-lost' into 'master'
Fix bug where RSS feeds got lost when a fetching error occured

Closes #871

See merge request !583
2017-08-04 13:52:26 +00:00
akwizgran
a4118b40e1 Merge branch 'debug-build-alongside-beta' into 'master'
Make debug builds installable alongside official beta build

See merge request !582
2017-08-02 16:54:25 +00:00
Torsten Grote
de29fbc324 Fix bug where RSS feeds got lost when a fetching error occured 2017-08-01 15:32:51 -03:00
Torsten Grote
3197dcf9b5 Merge branch 'checked-camera-exceptions' into 'master'
Throw checked exceptions for camera errors

See merge request !580
2017-08-01 16:54:45 +00:00
akwizgran
35aad409fd Merge branch '994-notification-sound-delay' into 'master'
Always play a notification sound, if at least 2sec after last one

Closes #994

See merge request !581
2017-08-01 16:20:35 +00:00
Torsten Grote
08ce6a7331 Change app name for debug builds 2017-08-01 13:08:12 -03:00
Torsten Grote
33a0099065 Make debug builds installable alongside official beta build 2017-08-01 12:57:11 -03:00
Torsten Grote
34d20fafda Always play a notification sound, if at least 2sec after last one
This is the same behavior as Signal.
We might want to adjust the delay later on.

This is also introduces a new BriarNotificationBuilder as a first step
to clean up the Notification Manager code.
2017-08-01 12:47:11 -03:00
Michael Rogers
aafddcd0f0 Bumped version number for beta release (for real this time). 2017-08-01 16:43:47 +01:00
akwizgran
0d6983b4ef Throw checked exceptions for camera errors. 2017-08-01 15:56:20 +01:00
akwizgran
69bfb72171 Merge branch '1002-cam-get-params-npe' into 'master'
Catch RuntimeException when getting camera parameters

See merge request !579
2017-08-01 13:56:45 +00:00
Torsten Grote
1aa33ec9b2 Catch RuntimeException when getting camera parameters 2017-08-01 10:49:04 -03:00
akwizgran
6702df1e22 Merge branch '1008-qr-decoding-crash' into 'master'
Catch IllegalArgumentException when decoding QrCode

Closes #1008

See merge request !578
2017-08-01 13:36:09 +00:00
akwizgran
c1748c9a86 Bumped version number for beta release. 2017-08-01 14:32:05 +01:00
akwizgran
9df624c62a Merge branch '1009-camera-npe' into 'master'
Prevent NPE in CameraView

Closes #1009 and #997

See merge request !577
2017-08-01 13:29:33 +00:00
Torsten Grote
0ee6197d7f Catch IllegalArgumentException when decoding QrCode 2017-08-01 10:21:02 -03:00
Torsten Grote
b03a7dce3e Catch runtime exception when setting best camera parameters
Closes #997
2017-08-01 10:09:21 -03:00
Torsten Grote
6c59d7dd5f Prevent NPE in CameraView
This prevents crashes, but still might cause the camera to not show up
thus preventing the user from adding contacts.
2017-08-01 09:41:42 -03:00
Michael Rogers
050191f0ef Bumped version number for beta release. 2017-08-01 12:31:47 +01:00
akwizgran
4b5a19ce5d Merge branch 'update-translations' into 'master'
Update translations, add Turkish and Russian

See merge request !575
2017-08-01 09:28:17 +00:00
akwizgran
7c4dd991b9 Merge branch '1016-reblog-runtime-error' into 'master'
Runtime error fix due to window requests

Closes #1016 and #1007

See merge request !576
2017-08-01 09:25:39 +00:00
Ernir Erlingsson
8455569e88 moved window requests above onCreate 2017-07-30 22:42:03 +02:00
Torsten Grote
d25676559c Update translations, add Turkish and Russian 2017-07-29 11:03:51 -03:00
Michael Rogers
a9437f7985 Bumped version number for beta release. 2017-07-28 18:01:19 +01:00
akwizgran
8141a97fc9 Merge branch '1015-recent-emoji-crash' into 'master'
Prevent a crash caused by empty emoji

Closes #1015

See merge request !571
2017-07-28 16:59:02 +00:00
Torsten Grote
db842bd7e4 Prevent a crash caused by empty emoji
The crash happens because the serialization of recently used emoji uses
';' to separate the emojis.
One of the ASCII emojis however has a ';' in the beginning.
When this one is used by the user,
it causes an empty string to be returned when deserializing.

This commit prevents the crash by changing the separator to a tab.
It uses a different settings string to store the emoji,
so users will lose the list of recently used emoji when they update to
this version.

PS. That wasn't my idea ;)
2017-07-28 13:49:51 -03:00
Torsten Grote
6dbec3a864 Merge branch 'enable-logging-for-beta-builds' into 'master'
Enable logging for beta builds

See merge request !573
2017-07-28 15:58:01 +00:00
akwizgran
29f658cf4d Merge branch '1006-blog-crash' into 'master'
Prevent crash in blog by ensuring a listener always exists

Closes #1006

See merge request !574
2017-07-28 15:53:43 +00:00
akwizgran
ca83744a84 Merge branch 'close-feed-stream' into 'master'
Close InputStream from RSS feed and prevent NPE

See merge request !572
2017-07-28 15:48:01 +00:00
Torsten Grote
d91a9e2be4 Prevent crash in blog by ensuring a listener always exists 2017-07-28 12:42:56 -03:00
akwizgran
8408c3f467 Enable logging for beta builds.
Some devices were logging and others not, due to the log level being set in the SplashScreenActivity constructor.
2017-07-28 16:41:24 +01:00
Torsten Grote
544c83a64c Close InputStream from RSS feed and prevent NPE 2017-07-28 10:38:01 -03:00
Michael Rogers
3800cd5e4f Bumped version number for beta release. 2017-07-28 11:17:09 +01:00
akwizgran
259f2cd419 Merge branch '993-fix-full-text-blog-posts' into 'master'
Show blog posts with full text when clicked

Closes #993

See merge request !570
2017-07-26 11:01:38 +00:00
Torsten Grote
20eb022c36 Show blog posts with full text when clicked
This fixes a regression that was introduced in !551.
2017-07-25 15:50:04 -03:00
akwizgran
531e555b52 Bumped version number for beta release. 2017-07-25 18:43:19 +01:00
akwizgran
a9024aa34b Merge branch '955-shared-with-update' into 'master'
Fix "shared with" counter not being updated

Closes #955

See merge request !569
2017-07-25 17:40:40 +00:00
akwizgran
d4e3b7842c Merge branch 'blog-sharing-tests' into 'master'
Add unit tests for BlogSharingManager

See merge request !567
2017-07-25 17:40:29 +00:00
Torsten Grote
167fddfbcc Add unit tests for BlogSharingManager 2017-07-25 12:45:36 -03:00
Torsten Grote
a48d642648 Fix UI bug in CreateForumActivity and adapt group creation 2017-07-25 12:32:53 -03:00
Torsten Grote
9a70f054c7 Use proper GroupId when reacting to accepted invitations
Fixes #955
2017-07-25 10:03:13 -03:00
Torsten Grote
ca43d13bd6 Merge branch 'inject-properties-module-eager-singletons' into 'master'
Inject properties module's eager singletons

See merge request !568
2017-07-25 12:55:59 +00:00
akwizgran
5b71004179 Inject properties module's eager singletons. 2017-07-25 13:49:15 +01:00
121 changed files with 3247 additions and 3458 deletions

View File

@@ -12,8 +12,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 22
versionCode 14
versionName "0.14"
versionCode 1610
versionName "0.16.10"
consumerProguardFiles 'proguard-rules.txt'
}
@@ -32,31 +32,31 @@ dependencies {
def torBinaryDir = 'src/main/res/raw'
task downloadTorGeoIp(type: Download) {
src 'https://briarproject.org/build/geoip-2017-05-02.zip'
src 'https://briarproject.org/build/geoip-2017-09-06.zip'
dest "$torBinaryDir/geoip.zip"
onlyIfNewer true
}
task downloadTorBinaryArm(type: Download) {
src 'https://briarproject.org/build/tor-0.2.9.11-arm.zip'
src 'https://briarproject.org/build/tor-0.2.9.12-arm.zip'
dest "$torBinaryDir/tor_arm.zip"
onlyIfNewer true
}
task downloadTorBinaryArmPie(type: Download) {
src 'https://briarproject.org/build/tor-0.2.9.11-arm-pie.zip'
src 'https://briarproject.org/build/tor-0.2.9.12-arm-pie.zip'
dest "$torBinaryDir/tor_arm_pie.zip"
onlyIfNewer true
}
task downloadTorBinaryX86(type: Download) {
src 'https://briarproject.org/build/tor-0.2.9.11-x86.zip'
src 'https://briarproject.org/build/tor-0.2.9.12-x86.zip'
dest "$torBinaryDir/tor_x86.zip"
onlyIfNewer true
}
task downloadTorBinaryX86Pie(type: Download) {
src 'https://briarproject.org/build/tor-0.2.9.11-x86-pie.zip'
src 'https://briarproject.org/build/tor-0.2.9.12-x86-pie.zip'
dest "$torBinaryDir/tor_x86_pie.zip"
onlyIfNewer true
}
@@ -64,31 +64,31 @@ task downloadTorBinaryX86Pie(type: Download) {
task verifyTorGeoIp(type: Verify, dependsOn: 'downloadTorGeoIp') {
src "$torBinaryDir/geoip.zip"
algorithm 'SHA-256'
checksum '51f4d1272fb867e1f3b36b67a584e2a33c40b40f62305457d799fd399cd77c9b'
checksum 'fe49d3adb86d3c512373101422a017dbb86c85a570524663f09dd8ce143a24f3'
}
task verifyTorBinaryArm(type: Verify, dependsOn: 'downloadTorBinaryArm') {
src "$torBinaryDir/tor_arm.zip"
algorithm 'SHA-256'
checksum '1da6008663a8ad98b349e62acbbf42c379f65ec504fa467cb119c187cd5a4c6b'
checksum '8ed0b347ffed1d6a4d2fd14495118eb92be83e9cc06e057e15220dc288b31688'
}
task verifyTorBinaryArmPie(type: Verify, dependsOn: 'downloadTorBinaryArmPie') {
src "$torBinaryDir/tor_arm_pie.zip"
algorithm 'SHA-256'
checksum 'eb061f880829e05f104690ac744848133f2dacef04759d425a2cff0df32c271e'
checksum '64403262511c29f462ca5e7c7621bfc3c944898364d1d5ad35a016bb8a034283'
}
task verifyTorBinaryX86(type: Verify, dependsOn: 'downloadTorBinaryX86') {
src "$torBinaryDir/tor_x86.zip"
algorithm 'SHA-256'
checksum 'f5308aff8303daca082f82227d02b51ddedba4ab1d1420739ada0427ae5dbb41'
checksum '61e014607a2079bcf1646289c67bff6372b1aded6e1d8d83d7791efda9a4d5ab'
}
task verifyTorBinaryX86Pie(type: Verify, dependsOn: 'downloadTorBinaryX86Pie') {
src "$torBinaryDir/tor_x86_pie.zip"
algorithm 'SHA-256'
checksum '889a6c81ac73d05d35ed610ca5a913cee44d333e4ae1749c2a107f2f7dd8197b'
checksum '18fbc98356697dd0895836ab46d5c9877d1c539193464f7db1e82a65adaaf288'
}
project.afterEvaluate {

View File

@@ -11,7 +11,6 @@ import android.content.IntentFilter;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
@@ -30,23 +29,14 @@ import org.briarproject.bramble.util.StringUtils;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
@@ -61,8 +51,6 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERA
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
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;
@@ -79,10 +67,6 @@ class DroidtoothPlugin implements DuplexPlugin {
private static final Logger LOG =
Logger.getLogger(DroidtoothPlugin.class.getName());
private static final String FOUND =
"android.bluetooth.device.action.FOUND";
private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
@@ -374,90 +358,6 @@ class DroidtoothPlugin implements DuplexPlugin {
return new DroidtoothTransportConnection(this, s);
}
@Override
public boolean supportsInvitations() {
return true;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) {
if (!isRunning()) return null;
// Use the invitation codes to generate the UUID
byte[] b = r.nextBytes(UUID_BYTES);
UUID uuid = UUID.nameUUIDFromBytes(b);
if (LOG.isLoggable(INFO)) LOG.info("Invitation UUID " + uuid);
// Bind a server socket for receiving invitation connections
BluetoothServerSocket ss;
try {
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
"RFCOMM", uuid);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
// Create the background tasks
CompletionService<BluetoothSocket> complete =
new ExecutorCompletionService<>(ioExecutor);
List<Future<BluetoothSocket>> futures = new ArrayList<>();
if (alice) {
// Return the first connected socket
futures.add(complete.submit(new ListeningTask(ss)));
futures.add(complete.submit(new DiscoveryTask(uuid.toString())));
} else {
// Return the first socket with readable data
futures.add(complete.submit(new ReadableTask(
new ListeningTask(ss))));
futures.add(complete.submit(new ReadableTask(
new DiscoveryTask(uuid.toString()))));
}
BluetoothSocket chosen = null;
try {
Future<BluetoothSocket> f = complete.poll(timeout, MILLISECONDS);
if (f == null) return null; // No task completed within the timeout
chosen = f.get();
return new DroidtoothTransportConnection(this, chosen);
} catch (InterruptedException e) {
LOG.info("Interrupted while exchanging invitations");
Thread.currentThread().interrupt();
return null;
} catch (ExecutionException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} finally {
// Closing the socket will terminate the listener task
tryToClose(ss);
closeSockets(futures, chosen);
}
}
private void closeSockets(final List<Future<BluetoothSocket>> futures,
@Nullable final BluetoothSocket chosen) {
ioExecutor.execute(new Runnable() {
@Override
public void run() {
for (Future<BluetoothSocket> f : futures) {
try {
if (f.cancel(true)) {
LOG.info("Cancelled task");
} else {
BluetoothSocket s = f.get();
if (s != null && s != chosen) {
LOG.info("Closing unwanted socket");
s.close();
}
}
} catch (InterruptedException e) {
LOG.info("Interrupted while closing sockets");
return;
} catch (ExecutionException | IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}
});
}
@Override
public boolean supportsKeyAgreement() {
return true;
@@ -472,7 +372,7 @@ class DroidtoothPlugin implements DuplexPlugin {
// 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 invitation connections
// Bind a server socket for receiving key agreement connections
BluetoothServerSocket ss;
try {
ss = adapter.listenUsingInsecureRfcommWithServiceRecord(
@@ -536,115 +436,6 @@ class DroidtoothPlugin implements DuplexPlugin {
}
}
private class DiscoveryTask implements Callable<BluetoothSocket> {
private final String uuid;
private DiscoveryTask(String uuid) {
this.uuid = uuid;
}
@Override
public BluetoothSocket call() throws Exception {
// Repeat discovery until we connect or get interrupted
while (true) {
// Discover nearby devices
LOG.info("Discovering nearby devices");
List<String> addresses = discoverDevices();
if (addresses.isEmpty()) {
LOG.info("No devices discovered");
continue;
}
// Connect to any device with the right UUID
for (String address : addresses) {
BluetoothSocket s = connect(address, uuid);
if (s != null) {
LOG.info("Outgoing connection");
return s;
}
}
}
}
private List<String> discoverDevices() throws InterruptedException {
IntentFilter filter = new IntentFilter();
filter.addAction(FOUND);
filter.addAction(DISCOVERY_FINISHED);
DiscoveryReceiver disco = new DiscoveryReceiver();
appContext.registerReceiver(disco, filter);
LOG.info("Starting discovery");
adapter.startDiscovery();
return disco.waitForAddresses();
}
}
private static class DiscoveryReceiver extends BroadcastReceiver {
private final CountDownLatch finished = new CountDownLatch(1);
private final List<String> addresses = new CopyOnWriteArrayList<>();
@Override
public void onReceive(Context ctx, Intent intent) {
String action = intent.getAction();
if (action.equals(DISCOVERY_FINISHED)) {
LOG.info("Discovery finished");
ctx.unregisterReceiver(this);
finished.countDown();
} else if (action.equals(FOUND)) {
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
if (LOG.isLoggable(INFO)) {
LOG.info("Discovered device: " +
scrubMacAddress(d.getAddress()));
}
addresses.add(d.getAddress());
}
}
private List<String> waitForAddresses() throws InterruptedException {
finished.await();
List<String> shuffled = new ArrayList<>(addresses);
Collections.shuffle(shuffled);
return shuffled;
}
}
private static class ListeningTask implements Callable<BluetoothSocket> {
private final BluetoothServerSocket serverSocket;
private ListeningTask(BluetoothServerSocket serverSocket) {
this.serverSocket = serverSocket;
}
@Override
public BluetoothSocket call() throws IOException {
BluetoothSocket s = serverSocket.accept();
LOG.info("Incoming connection");
return s;
}
}
private static class ReadableTask implements Callable<BluetoothSocket> {
private final Callable<BluetoothSocket> connectionTask;
private ReadableTask(Callable<BluetoothSocket> connectionTask) {
this.connectionTask = connectionTask;
}
@Override
public BluetoothSocket call() throws Exception {
BluetoothSocket s = connectionTask.call();
InputStream in = s.getInputStream();
while (in.available() == 0) {
LOG.info("Waiting for data");
Thread.sleep(1000);
}
LOG.info("Data available");
return s;
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final BluetoothServerSocket ss;

View File

@@ -17,7 +17,6 @@ import net.freehaven.tor.control.EventHandler;
import net.freehaven.tor.control.TorControlConnection;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
@@ -589,17 +588,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
}
@Override
public boolean supportsInvitations() {
return false;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) {
throw new UnsupportedOperationException();
}
@Override
public boolean supportsKeyAgreement() {
return false;

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
import org.briarproject.bramble.util.IoUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -21,12 +22,12 @@ class TorTransportConnection extends AbstractDuplexTransportConnection {
@Override
protected InputStream getInputStream() throws IOException {
return socket.getInputStream();
return IoUtils.getInputStream(socket);
}
@Override
protected OutputStream getOutputStream() throws IOException {
return socket.getOutputStream();
return IoUtils.getOutputStream(socket);
}
@Override

View File

@@ -10,8 +10,6 @@ public interface CryptoComponent {
SecretKey generateSecretKey();
PseudoRandom getPseudoRandom(int seed1, int seed2);
SecureRandom getSecureRandom();
KeyPair generateAgreementKeyPair();
@@ -24,15 +22,6 @@ public interface CryptoComponent {
KeyParser getMessageKeyParser();
/** Generates a random invitation code. */
int generateBTInvitationCode();
/**
* Derives a confirmation code from the given master secret.
* @param alice whether the code is for use by Alice or Bob.
*/
int deriveBTConfirmationCode(SecretKey master, boolean alice);
/**
* Derives a stream header key from the given master secret.
* @param alice whether the key is for use by Alice or Bob.

View File

@@ -1,12 +0,0 @@
package org.briarproject.bramble.api.crypto;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* A deterministic pseudo-random number generator.
*/
@NotNullByDefault
public interface PseudoRandom {
byte[] nextBytes(int bytes);
}

View File

@@ -14,8 +14,9 @@ public interface StreamDecrypterFactory {
StreamDecrypter createStreamDecrypter(InputStream in, StreamContext ctx);
/**
* Creates a {@link StreamDecrypter} for decrypting an invitation stream.
* Creates a {@link StreamDecrypter} for decrypting a contact exchange
* stream.
*/
StreamDecrypter createInvitationStreamDecrypter(InputStream in,
StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
SecretKey headerKey);
}

View File

@@ -14,8 +14,9 @@ public interface StreamEncrypterFactory {
StreamEncrypter createStreamEncrypter(OutputStream out, StreamContext ctx);
/**
* Creates a {@link StreamEncrypter} for encrypting an invitation stream.
* Creates a {@link StreamEncrypter} for encrypting a contact exchange
* stream.
*/
StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
StreamEncrypter createContactExchangeStreamDecrypter(OutputStream out,
SecretKey headerKey);
}

View File

@@ -1,20 +0,0 @@
package org.briarproject.bramble.api.invitation;
public interface InvitationConstants {
/**
* The connection timeout in milliseconds.
*/
long CONNECTION_TIMEOUT = 60 * 1000;
/**
* The confirmation timeout in milliseconds.
*/
long CONFIRMATION_TIMEOUT = 60 * 1000;
/**
* The number of bits in an invitation or confirmation code. Codes must fit
* into six decimal digits.
*/
int CODE_BITS = 19;
}

View File

@@ -1,47 +0,0 @@
package org.briarproject.bramble.api.invitation;
/**
* An interface for receiving updates about the state of an
* {@link InvitationTask}.
*/
public interface InvitationListener {
/** Called if a connection to the remote peer is established. */
void connectionSucceeded();
/**
* Called if a connection to the remote peer cannot be established. This
* indicates that the protocol has ended unsuccessfully.
*/
void connectionFailed();
/** Called if key agreement with the remote peer succeeds. */
void keyAgreementSucceeded(int localCode, int remoteCode);
/**
* Called if key agreement with the remote peer fails or the connection is
* lost. This indicates that the protocol has ended unsuccessfully.
*/
void keyAgreementFailed();
/** Called if the remote peer's confirmation check succeeds. */
void remoteConfirmationSucceeded();
/**
* Called if remote peer's confirmation check fails or the connection is
* lost. This indicates that the protocol has ended unsuccessfully.
*/
void remoteConfirmationFailed();
/**
* Called if the exchange of pseudonyms succeeds. This indicates that the
* protocol has ended successfully.
*/
void pseudonymExchangeSucceeded(String remoteName);
/**
* Called if the exchange of pseudonyms fails or the connection is lost.
* This indicates that the protocol has ended unsuccessfully.
*/
void pseudonymExchangeFailed();
}

View File

@@ -1,85 +0,0 @@
package org.briarproject.bramble.api.invitation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* A snapshot of the state of an {@link InvitationTask}.
*/
@Immutable
@NotNullByDefault
public class InvitationState {
private final int localInvitationCode, remoteInvitationCode;
private final int localConfirmationCode, remoteConfirmationCode;
private final boolean connected, connectionFailed;
private final boolean localCompared, remoteCompared;
private final boolean localMatched, remoteMatched;
@Nullable
private final String contactName;
public InvitationState(int localInvitationCode, int remoteInvitationCode,
int localConfirmationCode, int remoteConfirmationCode,
boolean connected, boolean connectionFailed, boolean localCompared,
boolean remoteCompared, boolean localMatched,
boolean remoteMatched, @Nullable String contactName) {
this.localInvitationCode = localInvitationCode;
this.remoteInvitationCode = remoteInvitationCode;
this.localConfirmationCode = localConfirmationCode;
this.remoteConfirmationCode = remoteConfirmationCode;
this.connected = connected;
this.connectionFailed = connectionFailed;
this.localCompared = localCompared;
this.remoteCompared = remoteCompared;
this.localMatched = localMatched;
this.remoteMatched = remoteMatched;
this.contactName = contactName;
}
public int getLocalInvitationCode() {
return localInvitationCode;
}
public int getRemoteInvitationCode() {
return remoteInvitationCode;
}
public int getLocalConfirmationCode() {
return localConfirmationCode;
}
public int getRemoteConfirmationCode() {
return remoteConfirmationCode;
}
public boolean getConnected() {
return connected;
}
public boolean getConnectionFailed() {
return connectionFailed;
}
public boolean getLocalCompared() {
return localCompared;
}
public boolean getRemoteCompared() {
return remoteCompared;
}
public boolean getLocalMatched() {
return localMatched;
}
public boolean getRemoteMatched() {
return remoteMatched;
}
@Nullable
public String getContactName() {
return contactName;
}
}

View File

@@ -1,38 +0,0 @@
package org.briarproject.bramble.api.invitation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* A task for exchanging invitations with a remote peer.
*/
@NotNullByDefault
public interface InvitationTask {
/**
* Adds a listener to be informed of state changes and returns the
* task's current state.
*/
InvitationState addListener(InvitationListener l);
/**
* Removes the given listener.
*/
void removeListener(InvitationListener l);
/**
* Asynchronously starts the connection process.
*/
void connect();
/**
* Asynchronously informs the remote peer that the local peer's
* confirmation codes matched.
*/
void localConfirmationSucceeded();
/**
* Asynchronously informs the remote peer that the local peer's
* confirmation codes did not match.
*/
void localConfirmationFailed();
}

View File

@@ -1,15 +0,0 @@
package org.briarproject.bramble.api.invitation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* Creates tasks for exchanging invitations with remote peers.
*/
@NotNullByDefault
public interface InvitationTaskFactory {
/**
* Creates a task using the given local and remote invitation codes.
*/
InvitationTask createTask(int localCode, int remoteCode);
}

View File

@@ -32,11 +32,6 @@ public interface PluginManager {
*/
Collection<DuplexPlugin> getDuplexPlugins();
/**
* Returns any duplex plugins that support invitations.
*/
Collection<DuplexPlugin> getInvitationPlugins();
/**
* Returns any duplex plugins that support key agreement.
*/

View File

@@ -1,7 +1,6 @@
package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -23,20 +22,6 @@ public interface DuplexPlugin extends Plugin {
@Nullable
DuplexTransportConnection createConnection(ContactId c);
/**
* Returns true if the plugin supports exchanging invitations.
*/
boolean supportsInvitations();
/**
* Attempts to create and return an invitation connection to the remote
* peer. Returns null if no connection can be established within the given
* time.
*/
@Nullable
DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice);
/**
* Returns true if the plugin supports short-range key agreement.
*/

View File

@@ -15,9 +15,9 @@ public interface StreamReaderFactory {
InputStream createStreamReader(InputStream in, StreamContext ctx);
/**
* Creates an {@link InputStream InputStream} for reading from an
* invitation stream.
* Creates an {@link InputStream InputStream} for reading from a contact
* exchangestream.
*/
InputStream createInvitationStreamReader(InputStream in,
InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey);
}

View File

@@ -15,9 +15,9 @@ public interface StreamWriterFactory {
OutputStream createStreamWriter(OutputStream out, StreamContext ctx);
/**
* Creates an {@link OutputStream OutputStream} for writing to an
* invitation stream.
* Creates an {@link OutputStream OutputStream} for writing to a contact
* exchange stream.
*/
OutputStream createInvitationStreamWriter(OutputStream out,
OutputStream createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey);
}

View File

@@ -8,6 +8,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import javax.annotation.Nullable;
@@ -59,4 +60,24 @@ public class IoUtils {
offset += read;
}
}
// Workaround for a bug in Android 7, see
// https://android-review.googlesource.com/#/c/271775/
public static InputStream getInputStream(Socket s) throws IOException {
try {
return s.getInputStream();
} catch (NullPointerException e) {
throw new IOException(e);
}
}
// Workaround for a bug in Android 7, see
// https://android-review.googlesource.com/#/c/271775/
public static OutputStream getOutputStream(Socket s) throws IOException {
try {
return s.getOutputStream();
} catch (NullPointerException e) {
throw new IOException(e);
}
}
}

View File

@@ -8,7 +8,6 @@ import org.briarproject.bramble.db.DatabaseExecutorModule;
import org.briarproject.bramble.db.DatabaseModule;
import org.briarproject.bramble.event.EventModule;
import org.briarproject.bramble.identity.IdentityModule;
import org.briarproject.bramble.invitation.InvitationModule;
import org.briarproject.bramble.keyagreement.KeyAgreementModule;
import org.briarproject.bramble.lifecycle.LifecycleModule;
import org.briarproject.bramble.plugin.PluginModule;
@@ -32,7 +31,6 @@ import dagger.Module;
DatabaseExecutorModule.class,
EventModule.class,
IdentityModule.class,
InvitationModule.class,
KeyAgreementModule.class,
LifecycleModule.class,
PluginModule.class,
@@ -54,6 +52,7 @@ public class BrambleCoreModule {
c.inject(new IdentityModule.EagerSingletons());
c.inject(new LifecycleModule.EagerSingletons());
c.inject(new PluginModule.EagerSingletons());
c.inject(new PropertiesModule.EagerSingletons());
c.inject(new SyncModule.EagerSingletons());
c.inject(new SystemModule.EagerSingletons());
c.inject(new TransportModule.EagerSingletons());

View File

@@ -80,7 +80,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
private volatile boolean alice;
@Inject
public ContactExchangeTaskImpl(DatabaseComponent db,
ContactExchangeTaskImpl(DatabaseComponent db,
AuthorFactory authorFactory, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, Clock clock,
ConnectionManager connectionManager, ContactManager contactManager,
@@ -146,12 +146,12 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
// Create the readers
InputStream streamReader =
streamReaderFactory.createInvitationStreamReader(in,
streamReaderFactory.createContactExchangeStreamReader(in,
alice ? bobHeaderKey : aliceHeaderKey);
BdfReader r = bdfReaderFactory.createReader(streamReader);
// Create the writers
OutputStream streamWriter =
streamWriterFactory.createInvitationStreamWriter(out,
streamWriterFactory.createContactExchangeStreamWriter(out,
alice ? aliceHeaderKey : bobHeaderKey);
BdfWriter w = bdfWriterFactory.createWriter(streamWriter);

View File

@@ -4,7 +4,6 @@ import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.plugin.TransportId;
@@ -41,7 +40,6 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static org.briarproject.bramble.api.invitation.InvitationConstants.CODE_BITS;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
import static org.briarproject.bramble.crypto.EllipticCurveConstants.PARAMETERS;
@@ -68,9 +66,6 @@ class CryptoComponentImpl implements CryptoComponent {
return s.getBytes(Charset.forName("US-ASCII"));
}
// KDF labels for bluetooth confirmation code derivation
private static final byte[] BT_A_CONFIRM = ascii("ALICE_CONFIRMATION_CODE");
private static final byte[] BT_B_CONFIRM = ascii("BOB_CONFIRMATION_CODE");
// KDF labels for contact exchange stream header key derivation
private static final byte[] A_INVITE = ascii("ALICE_INVITATION_KEY");
private static final byte[] B_INVITE = ascii("BOB_INVITATION_KEY");
@@ -171,14 +166,6 @@ class CryptoComponentImpl implements CryptoComponent {
return new SecretKey(b);
}
@Override
public PseudoRandom getPseudoRandom(int seed1, int seed2) {
byte[] seed = new byte[INT_32_BYTES * 2];
ByteUtils.writeUint32(seed1, seed, 0);
ByteUtils.writeUint32(seed2, seed, INT_32_BYTES);
return new PseudoRandomImpl(seed);
}
@Override
public SecureRandom getSecureRandom() {
return secureRandom;
@@ -250,20 +237,6 @@ class CryptoComponentImpl implements CryptoComponent {
return messageEncrypter.getKeyParser();
}
@Override
public int generateBTInvitationCode() {
int codeBytes = (CODE_BITS + 7) / 8;
byte[] random = new byte[codeBytes];
secureRandom.nextBytes(random);
return ByteUtils.readUint(random, CODE_BITS);
}
@Override
public int deriveBTConfirmationCode(SecretKey master, boolean alice) {
byte[] b = macKdf(master, alice ? BT_A_CONFIRM : BT_B_CONFIRM);
return ByteUtils.readUint(b, CODE_BITS);
}
@Override
public SecretKey deriveHeaderKey(SecretKey master,
boolean alice) {

View File

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

View File

@@ -46,8 +46,8 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
}
@Override
public StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
SecretKey headerKey) {
public StreamEncrypter createContactExchangeStreamDecrypter(
OutputStream out, SecretKey headerKey) {
AuthenticatedCipher cipher = cipherProvider.get();
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
crypto.getSecureRandom().nextBytes(streamHeaderNonce);

View File

@@ -1,119 +0,0 @@
package org.briarproject.bramble.invitation;
import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.BdfWriter;
import org.briarproject.bramble.api.data.BdfWriterFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
/**
* A connection thread for the peer being Alice in the invitation protocol.
*/
@NotNullByDefault
class AliceConnector extends Connector {
private static final Logger LOG =
Logger.getLogger(AliceConnector.class.getName());
AliceConnector(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ContactExchangeTask contactExchangeTask, ConnectorGroup group,
DuplexPlugin plugin, LocalAuthor localAuthor, PseudoRandom random) {
super(crypto, bdfReaderFactory, bdfWriterFactory, contactExchangeTask,
group, plugin, localAuthor, random);
}
@Override
public void run() {
// Create an incoming or outgoing connection
DuplexTransportConnection conn = createInvitationConnection(true);
if (conn == null) return;
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
// Don't proceed with more than one connection
if (group.getAndSetConnected()) {
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " redundant");
tryToClose(conn, false);
return;
}
// Carry out the key agreement protocol
InputStream in;
OutputStream out;
BdfReader r;
BdfWriter w;
SecretKey master;
try {
in = conn.getReader().getInputStream();
out = conn.getWriter().getOutputStream();
r = bdfReaderFactory.createReader(in);
w = bdfWriterFactory.createWriter(out);
// Alice goes first
sendPublicKeyHash(w);
byte[] hash = receivePublicKeyHash(r);
sendPublicKey(w);
byte[] key = receivePublicKey(r);
master = deriveMasterSecret(hash, key, true);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
group.keyAgreementFailed();
tryToClose(conn, true);
return;
} catch (GeneralSecurityException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
group.keyAgreementFailed();
tryToClose(conn, true);
return;
}
// The key agreement succeeded - derive the confirmation codes
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
int aliceCode = crypto.deriveBTConfirmationCode(master, true);
int bobCode = crypto.deriveBTConfirmationCode(master, false);
group.keyAgreementSucceeded(aliceCode, bobCode);
// Exchange confirmation results
boolean localMatched, remoteMatched;
try {
localMatched = group.waitForLocalConfirmationResult();
sendConfirmation(w, localMatched);
remoteMatched = receiveConfirmation(r);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
group.remoteConfirmationFailed();
tryToClose(conn, true);
return;
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for confirmation");
group.remoteConfirmationFailed();
tryToClose(conn, true);
Thread.currentThread().interrupt();
return;
}
if (remoteMatched) group.remoteConfirmationSucceeded();
else group.remoteConfirmationFailed();
if (!(localMatched && remoteMatched)) {
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " confirmation failed");
tryToClose(conn, false);
return;
}
// Confirmation succeeded - upgrade to a secure connection
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " confirmation succeeded");
contactExchangeTask.startExchange(group, localAuthor, master, conn,
plugin.getId(), true);
}
}

View File

@@ -1,119 +0,0 @@
package org.briarproject.bramble.invitation;
import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.BdfWriter;
import org.briarproject.bramble.api.data.BdfWriterFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
/**
* A connection thread for the peer being Bob in the invitation protocol.
*/
@NotNullByDefault
class BobConnector extends Connector {
private static final Logger LOG =
Logger.getLogger(BobConnector.class.getName());
BobConnector(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ContactExchangeTask contactExchangeTask, ConnectorGroup group,
DuplexPlugin plugin, LocalAuthor localAuthor, PseudoRandom random) {
super(crypto, bdfReaderFactory, bdfWriterFactory, contactExchangeTask,
group, plugin, localAuthor, random);
}
@Override
public void run() {
// Create an incoming or outgoing connection
DuplexTransportConnection conn = createInvitationConnection(false);
if (conn == null) return;
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
// Carry out the key agreement protocol
InputStream in;
OutputStream out;
BdfReader r;
BdfWriter w;
SecretKey master;
try {
in = conn.getReader().getInputStream();
out = conn.getWriter().getOutputStream();
r = bdfReaderFactory.createReader(in);
w = bdfWriterFactory.createWriter(out);
// Alice goes first
byte[] hash = receivePublicKeyHash(r);
// Don't proceed with more than one connection
if (group.getAndSetConnected()) {
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " redundant");
tryToClose(conn, false);
return;
}
sendPublicKeyHash(w);
byte[] key = receivePublicKey(r);
sendPublicKey(w);
master = deriveMasterSecret(hash, key, false);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
group.keyAgreementFailed();
tryToClose(conn, true);
return;
} catch (GeneralSecurityException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
group.keyAgreementFailed();
tryToClose(conn, true);
return;
}
// The key agreement succeeded - derive the confirmation codes
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
int aliceCode = crypto.deriveBTConfirmationCode(master, true);
int bobCode = crypto.deriveBTConfirmationCode(master, false);
group.keyAgreementSucceeded(bobCode, aliceCode);
// Exchange confirmation results
boolean localMatched, remoteMatched;
try {
remoteMatched = receiveConfirmation(r);
localMatched = group.waitForLocalConfirmationResult();
sendConfirmation(w, localMatched);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
group.remoteConfirmationFailed();
tryToClose(conn, true);
return;
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for confirmation");
group.remoteConfirmationFailed();
tryToClose(conn, true);
Thread.currentThread().interrupt();
return;
}
if (remoteMatched) group.remoteConfirmationSucceeded();
else group.remoteConfirmationFailed();
if (!(localMatched && remoteMatched)) {
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " confirmation failed");
tryToClose(conn, false);
return;
}
// Confirmation succeeded - upgrade to a secure connection
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " confirmation succeeded");
contactExchangeTask.startExchange(group, localAuthor, master, conn,
plugin.getId(), false);
}
}

View File

@@ -1,150 +0,0 @@
package org.briarproject.bramble.invitation;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.BdfWriter;
import org.briarproject.bramble.api.data.BdfWriterFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.invitation.InvitationConstants.CONNECTION_TIMEOUT;
// FIXME: This class has way too many dependencies
@NotNullByDefault
abstract class Connector extends Thread {
private static final Logger LOG =
Logger.getLogger(Connector.class.getName());
private static final String LABEL_PUBLIC_KEY =
"org.briarproject.bramble.invitation.PUBLIC_KEY";
protected final CryptoComponent crypto;
protected final BdfReaderFactory bdfReaderFactory;
protected final BdfWriterFactory bdfWriterFactory;
protected final ContactExchangeTask contactExchangeTask;
protected final ConnectorGroup group;
protected final DuplexPlugin plugin;
protected final LocalAuthor localAuthor;
protected final PseudoRandom random;
protected final String pluginName;
private final KeyPair keyPair;
private final KeyParser keyParser;
Connector(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ContactExchangeTask contactExchangeTask, ConnectorGroup group,
DuplexPlugin plugin, LocalAuthor localAuthor, PseudoRandom random) {
super("Connector");
this.crypto = crypto;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.contactExchangeTask = contactExchangeTask;
this.group = group;
this.plugin = plugin;
this.localAuthor = localAuthor;
this.random = random;
pluginName = plugin.getClass().getName();
keyPair = crypto.generateAgreementKeyPair();
keyParser = crypto.getAgreementKeyParser();
}
@Nullable
DuplexTransportConnection createInvitationConnection(boolean alice) {
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " creating invitation connection");
return plugin.createInvitationConnection(random, CONNECTION_TIMEOUT,
alice);
}
void sendPublicKeyHash(BdfWriter w) throws IOException {
byte[] hash =
crypto.hash(LABEL_PUBLIC_KEY, keyPair.getPublic().getEncoded());
w.writeRaw(hash);
w.flush();
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
}
byte[] receivePublicKeyHash(BdfReader r) throws IOException {
int hashLength = crypto.getHashLength();
byte[] b = r.readRaw(hashLength);
if (b.length < hashLength) throw new FormatException();
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
return b;
}
void sendPublicKey(BdfWriter w) throws IOException {
byte[] key = keyPair.getPublic().getEncoded();
w.writeRaw(key);
w.flush();
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " sent key");
}
byte[] receivePublicKey(BdfReader r)
throws GeneralSecurityException, IOException {
byte[] b = r.readRaw(MAX_PUBLIC_KEY_LENGTH);
keyParser.parsePublicKey(b);
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " received key");
return b;
}
SecretKey deriveMasterSecret(byte[] hash, byte[] key, boolean alice)
throws GeneralSecurityException {
// Check that the hash matches the key
byte[] keyHash =
crypto.hash(LABEL_PUBLIC_KEY, keyPair.getPublic().getEncoded());
if (!Arrays.equals(hash, keyHash)) {
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " hash does not match key");
throw new GeneralSecurityException();
}
// Derive the master secret
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " deriving master secret");
return crypto.deriveMasterSecret(key, keyPair, alice);
}
void sendConfirmation(BdfWriter w, boolean confirmed) throws IOException {
w.writeBoolean(confirmed);
w.flush();
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " sent confirmation: " + confirmed);
}
boolean receiveConfirmation(BdfReader r) throws IOException {
boolean confirmed = r.readBoolean();
if (LOG.isLoggable(INFO))
LOG.info(pluginName + " received confirmation: " + confirmed);
return confirmed;
}
protected void tryToClose(DuplexTransportConnection conn,
boolean exception) {
try {
LOG.info("Closing connection");
conn.getReader().dispose(exception, true);
conn.getWriter().dispose(exception);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}

View File

@@ -1,278 +0,0 @@
package org.briarproject.bramble.invitation;
import org.briarproject.bramble.api.contact.ContactExchangeListener;
import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.BdfWriterFactory;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.invitation.InvitationListener;
import org.briarproject.bramble.api.invitation.InvitationState;
import org.briarproject.bramble.api.invitation.InvitationTask;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.invitation.InvitationConstants.CONFIRMATION_TIMEOUT;
/**
* A task consisting of one or more parallel connection attempts.
*/
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class ConnectorGroup extends Thread implements InvitationTask,
ContactExchangeListener {
private static final Logger LOG =
Logger.getLogger(ConnectorGroup.class.getName());
private final CryptoComponent crypto;
private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final ContactExchangeTask contactExchangeTask;
private final IdentityManager identityManager;
private final PluginManager pluginManager;
private final int localInvitationCode, remoteInvitationCode;
private final Collection<InvitationListener> listeners;
private final AtomicBoolean connected;
private final CountDownLatch localConfirmationLatch;
private final Lock lock = new ReentrantLock();
// The following are locking: lock
private int localConfirmationCode = -1, remoteConfirmationCode = -1;
private boolean connectionFailed = false;
private boolean localCompared = false, remoteCompared = false;
private boolean localMatched = false, remoteMatched = false;
private String remoteName = null;
ConnectorGroup(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ContactExchangeTask contactExchangeTask,
IdentityManager identityManager, PluginManager pluginManager,
int localInvitationCode, int remoteInvitationCode) {
super("ConnectorGroup");
this.crypto = crypto;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.contactExchangeTask = contactExchangeTask;
this.identityManager = identityManager;
this.pluginManager = pluginManager;
this.localInvitationCode = localInvitationCode;
this.remoteInvitationCode = remoteInvitationCode;
listeners = new CopyOnWriteArrayList<InvitationListener>();
connected = new AtomicBoolean(false);
localConfirmationLatch = new CountDownLatch(1);
}
@Override
public InvitationState addListener(InvitationListener l) {
lock.lock();
try {
listeners.add(l);
return new InvitationState(localInvitationCode,
remoteInvitationCode, localConfirmationCode,
remoteConfirmationCode, connected.get(), connectionFailed,
localCompared, remoteCompared, localMatched, remoteMatched,
remoteName);
} finally {
lock.unlock();
}
}
@Override
public void removeListener(InvitationListener l) {
listeners.remove(l);
}
@Override
public void connect() {
start();
}
@Override
public void run() {
LocalAuthor localAuthor;
// Load the local pseudonym
try {
localAuthor = identityManager.getLocalAuthor();
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
lock.lock();
try {
connectionFailed = true;
} finally {
lock.unlock();
}
for (InvitationListener l : listeners) l.connectionFailed();
return;
}
// Start the connection threads
Collection<Connector> connectors = new ArrayList<Connector>();
// Alice is the party with the smaller invitation code
if (localInvitationCode < remoteInvitationCode) {
for (DuplexPlugin plugin : pluginManager.getInvitationPlugins()) {
Connector c = createAliceConnector(plugin, localAuthor);
connectors.add(c);
c.start();
}
} else {
for (DuplexPlugin plugin : pluginManager.getInvitationPlugins()) {
Connector c = createBobConnector(plugin, localAuthor);
connectors.add(c);
c.start();
}
}
// Wait for the connection threads to finish
try {
for (Connector c : connectors) c.join();
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for connectors");
Thread.currentThread().interrupt();
}
// If none of the threads connected, inform the listeners
if (!connected.get()) {
lock.lock();
try {
connectionFailed = true;
} finally {
lock.unlock();
}
for (InvitationListener l : listeners) l.connectionFailed();
}
}
private Connector createAliceConnector(DuplexPlugin plugin,
LocalAuthor localAuthor) {
PseudoRandom random = crypto.getPseudoRandom(localInvitationCode,
remoteInvitationCode);
return new AliceConnector(crypto, bdfReaderFactory, bdfWriterFactory,
contactExchangeTask, this, plugin, localAuthor, random);
}
private Connector createBobConnector(DuplexPlugin plugin,
LocalAuthor localAuthor) {
PseudoRandom random = crypto.getPseudoRandom(remoteInvitationCode,
localInvitationCode);
return new BobConnector(crypto, bdfReaderFactory, bdfWriterFactory,
contactExchangeTask, this, plugin, localAuthor, random);
}
@Override
public void localConfirmationSucceeded() {
lock.lock();
try {
localCompared = true;
localMatched = true;
} finally {
lock.unlock();
}
localConfirmationLatch.countDown();
}
@Override
public void localConfirmationFailed() {
lock.lock();
try {
localCompared = true;
localMatched = false;
} finally {
lock.unlock();
}
localConfirmationLatch.countDown();
}
boolean getAndSetConnected() {
boolean redundant = connected.getAndSet(true);
if (!redundant)
for (InvitationListener l : listeners) l.connectionSucceeded();
return redundant;
}
void keyAgreementSucceeded(int localCode, int remoteCode) {
lock.lock();
try {
localConfirmationCode = localCode;
remoteConfirmationCode = remoteCode;
} finally {
lock.unlock();
}
for (InvitationListener l : listeners)
l.keyAgreementSucceeded(localCode, remoteCode);
}
void keyAgreementFailed() {
for (InvitationListener l : listeners) l.keyAgreementFailed();
}
boolean waitForLocalConfirmationResult() throws InterruptedException {
localConfirmationLatch.await(CONFIRMATION_TIMEOUT, MILLISECONDS);
lock.lock();
try {
return localMatched;
} finally {
lock.unlock();
}
}
void remoteConfirmationSucceeded() {
lock.lock();
try {
remoteCompared = true;
remoteMatched = true;
} finally {
lock.unlock();
}
for (InvitationListener l : listeners) l.remoteConfirmationSucceeded();
}
void remoteConfirmationFailed() {
lock.lock();
try {
remoteCompared = true;
remoteMatched = false;
} finally {
lock.unlock();
}
for (InvitationListener l : listeners) l.remoteConfirmationFailed();
}
@Override
public void contactExchangeSucceeded(Author remoteAuthor) {
String name = remoteAuthor.getName();
lock.lock();
try {
remoteName = name;
} finally {
lock.unlock();
}
for (InvitationListener l : listeners)
l.pseudonymExchangeSucceeded(name);
}
@Override
public void duplicateContact(Author remoteAuthor) {
// TODO differentiate
for (InvitationListener l : listeners) l.pseudonymExchangeFailed();
}
@Override
public void contactExchangeFailed() {
for (InvitationListener l : listeners) l.pseudonymExchangeFailed();
}
}

View File

@@ -1,16 +0,0 @@
package org.briarproject.bramble.invitation;
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
import dagger.Module;
import dagger.Provides;
@Module
public class InvitationModule {
@Provides
InvitationTaskFactory provideInvitationTaskFactory(
InvitationTaskFactoryImpl invitationTaskFactory) {
return invitationTaskFactory;
}
}

View File

@@ -1,47 +0,0 @@
package org.briarproject.bramble.invitation;
import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.BdfWriterFactory;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.invitation.InvitationTask;
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class InvitationTaskFactoryImpl implements InvitationTaskFactory {
private final CryptoComponent crypto;
private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final ContactExchangeTask contactExchangeTask;
private final IdentityManager identityManager;
private final PluginManager pluginManager;
@Inject
InvitationTaskFactoryImpl(CryptoComponent crypto,
BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory,
ContactExchangeTask contactExchangeTask,
IdentityManager identityManager, PluginManager pluginManager) {
this.crypto = crypto;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.contactExchangeTask = contactExchangeTask;
this.identityManager = identityManager;
this.pluginManager = pluginManager;
}
@Override
public InvitationTask createTask(int localCode, int remoteCode) {
return new ConnectorGroup(crypto, bdfReaderFactory, bdfWriterFactory,
contactExchangeTask, identityManager, pluginManager,
localCode, remoteCode);
}
}

View File

@@ -164,14 +164,6 @@ class PluginManagerImpl implements PluginManager, Service {
return new ArrayList<DuplexPlugin>(duplexPlugins);
}
@Override
public Collection<DuplexPlugin> getInvitationPlugins() {
List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
for (DuplexPlugin d : duplexPlugins)
if (d.supportsInvitations()) supported.add(d);
return supported;
}
@Override
public Collection<DuplexPlugin> getKeyAgreementPlugins() {
List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();

View File

@@ -1,7 +1,6 @@
package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -281,17 +280,6 @@ abstract class TcpPlugin implements DuplexPlugin {
}
}
@Override
public boolean supportsInvitations() {
return false;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) {
throw new UnsupportedOperationException();
}
@Override
public boolean supportsKeyAgreement() {
return false;

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tcp;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
import org.briarproject.bramble.util.IoUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -24,12 +25,12 @@ class TcpTransportConnection extends AbstractDuplexTransportConnection {
@Override
protected InputStream getInputStream() throws IOException {
return socket.getInputStream();
return IoUtils.getInputStream(socket);
}
@Override
protected OutputStream getOutputStream() throws IOException {
return socket.getOutputStream();
return IoUtils.getOutputStream(socket);
}
@Override

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.reporting;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.IoUtils;
import java.io.Closeable;
import java.io.File;
@@ -130,7 +131,7 @@ public class DevReportServer {
OutputStream out = null;
try {
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
in = socket.getInputStream();
in = IoUtils.getInputStream(socket);
reportDir.mkdirs();
reportFile = File.createTempFile(FILE_PREFIX, FILE_SUFFIX,
reportDir);

View File

@@ -93,7 +93,7 @@ class DevReporterImpl implements DevReporter {
InputStream in = null;
try {
Socket s = connectToDevelopers();
out = s.getOutputStream();
out = IoUtils.getOutputStream(s);
in = new FileInputStream(f);
IoUtils.copyAndClose(in, out);
f.delete();

View File

@@ -57,8 +57,8 @@ class SocksSocket extends Socket {
// Connect to the proxy
super.connect(proxy, connectToProxyTimeout);
OutputStream out = getOutputStream();
InputStream in = getInputStream();
OutputStream out = IoUtils.getOutputStream(this);
InputStream in = IoUtils.getInputStream(this);
// Request SOCKS 5 with no authentication
sendMethodRequest(out);

View File

@@ -29,10 +29,10 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
}
@Override
public InputStream createInvitationStreamReader(InputStream in,
public InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(
streamDecrypterFactory.createInvitationStreamDecrypter(in,
streamDecrypterFactory.createContactExchangeStreamDecrypter(in,
headerKey));
}
}

View File

@@ -30,10 +30,10 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
}
@Override
public OutputStream createInvitationStreamWriter(OutputStream out,
public OutputStream createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(
streamEncrypterFactory.createInvitationStreamEncrypter(out,
streamEncrypterFactory.createContactExchangeStreamDecrypter(out,
headerKey));
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.bramble.crypto;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.spongycastle.crypto.Digest;
import org.spongycastle.crypto.engines.Salsa20Engine;
@@ -11,11 +10,11 @@ import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@NotNullByDefault
class PseudoRandomImpl implements PseudoRandom {
class PseudoRandom {
private final Salsa20Engine cipher = new Salsa20Engine();
PseudoRandomImpl(byte[] seed) {
PseudoRandom(byte[] seed) {
// Hash the seed to produce a 32-byte key
byte[] key = new byte[32];
Digest digest = new Blake2sDigest();
@@ -26,8 +25,7 @@ class PseudoRandomImpl implements PseudoRandom {
cipher.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
}
@Override
public byte[] nextBytes(int length) {
byte[] nextBytes(int length) {
byte[] in = new byte[length], out = new byte[length];
cipher.processBytes(in, 0, length, out, 0);
return out;

View File

@@ -1,7 +1,5 @@
package org.briarproject.bramble.crypto;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
@@ -19,7 +17,7 @@ class PseudoSecureRandom extends SecureRandom {
private final PseudoRandom pseudoRandom;
private PseudoSecureRandomSpi(byte[] seed) {
pseudoRandom = new PseudoRandomImpl(seed);
pseudoRandom = new PseudoRandom(seed);
}
@Override

View File

@@ -2,7 +2,6 @@ package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
@@ -19,33 +18,23 @@ import org.briarproject.bramble.util.OsUtils;
import org.briarproject.bramble.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static javax.bluetooth.DiscoveryAgent.GIAC;
@@ -67,7 +56,6 @@ class BluetoothPlugin implements DuplexPlugin {
private final Backoff backoff;
private final DuplexPluginCallback callback;
private final int maxLatency;
private final Semaphore discoverySemaphore = new Semaphore(1);
private final AtomicBoolean used = new AtomicBoolean(false);
private volatile boolean running = false;
@@ -273,95 +261,6 @@ class BluetoothPlugin implements DuplexPlugin {
return new BluetoothTransportConnection(this, s);
}
@Override
public boolean supportsInvitations() {
return true;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) {
if (!running) return null;
// Use the invitation codes to generate the UUID
byte[] b = r.nextBytes(UUID_BYTES);
String uuid = UUID.nameUUIDFromBytes(b).toString();
String url = makeUrl("localhost", uuid);
// Make the device discoverable if possible
makeDeviceDiscoverable();
// Bind a server socket for receiving invitation connections
final StreamConnectionNotifier ss;
try {
ss = (StreamConnectionNotifier) Connector.open(url);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
if (!running) {
tryToClose(ss);
return null;
}
// Create the background tasks
CompletionService<StreamConnection> complete =
new ExecutorCompletionService<>(ioExecutor);
List<Future<StreamConnection>> futures = new ArrayList<>();
if (alice) {
// Return the first connected socket
futures.add(complete.submit(new ListeningTask(ss)));
futures.add(complete.submit(new DiscoveryTask(uuid)));
} else {
// Return the first socket with readable data
futures.add(complete.submit(new ReadableTask(
new ListeningTask(ss))));
futures.add(complete.submit(new ReadableTask(
new DiscoveryTask(uuid))));
}
StreamConnection chosen = null;
try {
Future<StreamConnection> f = complete.poll(timeout, MILLISECONDS);
if (f == null) return null; // No task completed within the timeout
chosen = f.get();
return new BluetoothTransportConnection(this, chosen);
} catch (InterruptedException e) {
LOG.info("Interrupted while exchanging invitations");
Thread.currentThread().interrupt();
return null;
} catch (ExecutionException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} finally {
// Closing the socket will terminate the listener task
tryToClose(ss);
closeSockets(futures, chosen);
}
}
private void closeSockets(final List<Future<StreamConnection>> futures,
@Nullable final StreamConnection chosen) {
ioExecutor.execute(new Runnable() {
@Override
public void run() {
for (Future<StreamConnection> f : futures) {
try {
if (f.cancel(true)) {
LOG.info("Cancelled task");
} else {
StreamConnection s = f.get();
if (s != null && s != chosen) {
LOG.info("Closing unwanted socket");
s.close();
}
}
} catch (InterruptedException e) {
LOG.info("Interrupted while closing sockets");
return;
} catch (ExecutionException | IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}
});
}
@Override
public boolean supportsKeyAgreement() {
return true;
@@ -376,7 +275,7 @@ class BluetoothPlugin implements DuplexPlugin {
String url = makeUrl("localhost", uuid);
// Make the device discoverable if possible
makeDeviceDiscoverable();
// Bind a server socket for receiving invitation connections
// Bind a server socket for receiving key agreementconnections
final StreamConnectionNotifier ss;
try {
ss = (StreamConnectionNotifier) Connector.open(url);
@@ -431,77 +330,6 @@ class BluetoothPlugin implements DuplexPlugin {
}
}
private class DiscoveryTask implements Callable<StreamConnection> {
private final String uuid;
private DiscoveryTask(String uuid) {
this.uuid = uuid;
}
@Override
public StreamConnection call() throws Exception {
// Repeat discovery until we connect or get interrupted
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
while (true) {
if (!discoverySemaphore.tryAcquire())
throw new Exception("Discovery is already in progress");
try {
InvitationListener listener =
new InvitationListener(discoveryAgent, uuid);
discoveryAgent.startInquiry(GIAC, listener);
String url = listener.waitForUrl();
if (url != null) {
StreamConnection s = connect(url);
if (s != null) {
LOG.info("Outgoing connection");
return s;
}
}
} finally {
discoverySemaphore.release();
}
}
}
}
private static class ListeningTask implements Callable<StreamConnection> {
private final StreamConnectionNotifier serverSocket;
private ListeningTask(StreamConnectionNotifier serverSocket) {
this.serverSocket = serverSocket;
}
@Override
public StreamConnection call() throws Exception {
StreamConnection s = serverSocket.acceptAndOpen();
LOG.info("Incoming connection");
return s;
}
}
private static class ReadableTask implements Callable<StreamConnection> {
private final Callable<StreamConnection> connectionTask;
private ReadableTask(Callable<StreamConnection> connectionTask) {
this.connectionTask = connectionTask;
}
@Override
public StreamConnection call() throws Exception {
StreamConnection s = connectionTask.call();
InputStream in = s.openInputStream();
while (in.available() == 0) {
LOG.info("Waiting for data");
Thread.sleep(1000);
}
LOG.info("Data available");
return s;
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final StreamConnectionNotifier ss;

View File

@@ -1,109 +0,0 @@
package org.briarproject.bramble.plugin.bluetooth;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DataElement;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import static java.util.logging.Level.WARNING;
class InvitationListener implements DiscoveryListener {
private static final Logger LOG =
Logger.getLogger(InvitationListener.class.getName());
private final AtomicInteger searches = new AtomicInteger(1);
private final CountDownLatch finished = new CountDownLatch(1);
private final DiscoveryAgent discoveryAgent;
private final String uuid;
private volatile String url = null;
InvitationListener(DiscoveryAgent discoveryAgent, String uuid) {
this.discoveryAgent = discoveryAgent;
this.uuid = uuid;
}
String waitForUrl() throws InterruptedException {
finished.await();
return url;
}
@Override
public void deviceDiscovered(RemoteDevice device, DeviceClass deviceClass) {
UUID[] uuids = new UUID[] {new UUID(uuid, false)};
// Try to discover the services associated with the UUID
try {
discoveryAgent.searchServices(null, uuids, device, this);
searches.incrementAndGet();
} catch (BluetoothStateException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
public void servicesDiscovered(int transaction, ServiceRecord[] services) {
for (ServiceRecord record : services) {
// Does this service have a URL?
String serviceUrl = record.getConnectionURL(
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
if (serviceUrl == null) continue;
// Does this service have the UUID we're looking for?
Collection<String> uuids = new TreeSet<>();
findNestedClassIds(record.getAttributeValue(0x1), uuids);
for (String u : uuids) {
if (uuid.equalsIgnoreCase(u)) {
// The UUID matches - store the URL
url = serviceUrl;
finished.countDown();
return;
}
}
}
}
@Override
public void inquiryCompleted(int discoveryType) {
if (searches.decrementAndGet() == 0) finished.countDown();
}
@Override
public void serviceSearchCompleted(int transaction, int response) {
if (searches.decrementAndGet() == 0) finished.countDown();
}
// UUIDs are sometimes buried in nested data elements
private void findNestedClassIds(Object o, Collection<String> ids) {
o = getDataElementValue(o);
if (o instanceof Enumeration<?>) {
for (Object o1 : Collections.list((Enumeration<?>) o))
findNestedClassIds(o1, ids);
} else if (o instanceof UUID) {
ids.add(o.toString());
}
}
private Object getDataElementValue(Object o) {
if (o instanceof DataElement) {
// Bluecove throws an exception if the type is unknown
try {
return ((DataElement) o).getValue();
} catch (ClassCastException e) {
return null;
}
}
return null;
}
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.bramble.plugin.modem;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.PseudoRandom;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -167,17 +166,6 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
return new ModemTransportConnection();
}
@Override
public boolean supportsInvitations() {
return false;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout, boolean alice) {
throw new UnsupportedOperationException();
}
@Override
public boolean supportsKeyAgreement() {
return false;

View File

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

View File

@@ -78,15 +78,19 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 22
versionCode 1602
versionName "0.16.2"
versionCode 1610
versionName "0.16.10"
applicationId "org.briarproject.briar.beta"
resValue "string", "app_package", "org.briarproject.briar.beta"
resValue "string", "app_name", "Briar Beta"
buildConfigField "String", "GitHash", "\"${getGitHash()}\""
}
buildTypes {
debug {
applicationIdSuffix ".debug"
resValue "string", "app_package", "org.briarproject.briar.beta.debug"
resValue "string", "app_name", "Briar Debug"
shrinkResources false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'

View File

@@ -15,8 +15,6 @@
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Since API 23, this is needed to add contacts via Bluetooth -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name="org.briarproject.briar.android.BriarApplicationImpl"
@@ -292,16 +290,6 @@
/>
</activity>
<activity
android:name="org.briarproject.briar.android.invitation.AddContactActivity"
android:label="@string/add_contact_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
/>
</activity>
<activity
android:name="org.briarproject.briar.android.keyagreement.KeyAgreementActivity"
android:label="@string/add_contact_title"

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
import org.briarproject.bramble.api.keyagreement.PayloadParser;
@@ -89,8 +88,6 @@ public interface AndroidComponent
EventBus eventBus();
InvitationTaskFactory invitationTaskFactory();
AndroidNotificationManager androidNotificationManager();
ScreenFilterMonitor screenFilterMonitor();

View File

@@ -5,11 +5,8 @@ import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.UiThread;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseExecutor;
@@ -25,12 +22,14 @@ import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.ConversationActivity;
import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.util.BriarNotificationBuilder;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
@@ -48,6 +47,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
@@ -61,8 +61,6 @@ import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
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.VISIBILITY_PRIVATE;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.contact.ConversationActivity.CONTACT_ID;
@@ -95,6 +93,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private static final String BLOG_URI =
"content://org.briarproject.briar/blog";
private static final long SOUND_DELAY = TimeUnit.SECONDS.toMillis(2);
private static final Logger LOG =
Logger.getLogger(AndroidNotificationManagerImpl.class.getName());
@@ -102,6 +102,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private final SettingsManager settingsManager;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final Clock clock;
private final AtomicBoolean used = new AtomicBoolean(false);
// The following must only be accessed on the main UI thread
@@ -117,16 +118,18 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private boolean blockContacts = false, blockGroups = false;
private boolean blockForums = false, blockBlogs = false;
private boolean blockIntroductions = false;
private long lastSound = 0;
private volatile Settings settings = new Settings();
@Inject
AndroidNotificationManagerImpl(@DatabaseExecutor Executor dbExecutor,
SettingsManager settingsManager, AndroidExecutor androidExecutor,
Application app) {
Application app, Clock clock) {
this.dbExecutor = dbExecutor;
this.settingsManager = settingsManager;
this.androidExecutor = androidExecutor;
this.clock = clock;
appContext = app.getApplicationContext();
}
@@ -288,22 +291,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (contactTotal == 0) {
clearContactNotification();
} else if (settings.getBoolean(PREF_NOTIFY_PRIVATE, true)) {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext);
b.setSmallIcon(R.drawable.notification_private_message);
b.setColor(ContextCompat.getColor(appContext,
R.color.briar_primary));
b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name));
b.setContentText(appContext.getResources().getQuantityString(
R.plurals.private_message_notification_text, contactTotal,
contactTotal));
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (sound && !StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
b.setNumber(contactTotal);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
b.setLockscreenVisibility(CATEGORY_MESSAGE, showOnLockScreen);
playSound(b);
if (contactCounts.size() == 1) {
// Touching the notification shows the relevant conversation
Intent i = new Intent(appContext, ConversationActivity.class);
@@ -326,21 +326,27 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
}
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_MESSAGE);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
if (showOnLockScreen)
b.setVisibility(VISIBILITY_PRIVATE);
else
b.setVisibility(VISIBILITY_SECRET);
}
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(PRIVATE_MESSAGE_NOTIFICATION_ID, b.build());
}
}
@UiThread
private void playSound(BriarNotificationBuilder b) {
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
if (!sound) return;
long currentTime = clock.currentTimeMillis();
if (currentTime - lastSound > SOUND_DELAY) {
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (!StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
lastSound = clock.currentTimeMillis();
}
}
@UiThread
private int getDefaults() {
int defaults = DEFAULT_LIGHTS;
@@ -387,21 +393,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (groupTotal == 0) {
clearGroupMessageNotification();
} else if (settings.getBoolean(PREF_NOTIFY_GROUP, true)) {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext);
b.setSmallIcon(R.drawable.notification_private_group);
b.setColor(ContextCompat.getColor(appContext,
R.color.briar_primary));
b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name));
b.setContentText(appContext.getResources().getQuantityString(
R.plurals.group_message_notification_text, groupTotal,
groupTotal));
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (!StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
b.setNumber(groupTotal);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
playSound(b);
if (groupCounts.size() == 1) {
// Touching the notification shows the relevant group
Intent i = new Intent(appContext, GroupActivity.class);
@@ -425,15 +429,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
}
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_SOCIAL);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
if (showOnLockScreen)
b.setVisibility(VISIBILITY_PRIVATE);
else
b.setVisibility(VISIBILITY_SECRET);
}
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(GROUP_MESSAGE_NOTIFICATION_ID, b.build());
@@ -474,21 +469,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (forumTotal == 0) {
clearForumPostNotification();
} else if (settings.getBoolean(PREF_NOTIFY_FORUM, true)) {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext);
b.setSmallIcon(R.drawable.notification_forum);
b.setColor(ContextCompat.getColor(appContext,
R.color.briar_primary));
b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name));
b.setContentText(appContext.getResources().getQuantityString(
R.plurals.forum_post_notification_text, forumTotal,
forumTotal));
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (!StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
b.setNumber(forumTotal);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
playSound(b);
if (forumCounts.size() == 1) {
// Touching the notification shows the relevant forum
Intent i = new Intent(appContext, ForumActivity.class);
@@ -512,15 +505,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
}
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_SOCIAL);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
if (showOnLockScreen)
b.setVisibility(VISIBILITY_PRIVATE);
else
b.setVisibility(VISIBILITY_SECRET);
}
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(FORUM_POST_NOTIFICATION_ID, b.build());
@@ -561,21 +545,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (blogTotal == 0) {
clearBlogPostNotification();
} else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext);
b.setSmallIcon(R.drawable.notification_blog);
b.setColor(ContextCompat.getColor(appContext,
R.color.briar_primary));
b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name));
b.setContentText(appContext.getResources().getQuantityString(
R.plurals.blog_post_notification_text, blogTotal,
blogTotal));
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (!StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
b.setNumber(blogTotal);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
playSound(b);
// Touching the notification shows the combined blog feed
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_BLOGS, true);
@@ -585,15 +567,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_SOCIAL);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
if (showOnLockScreen)
b.setVisibility(VISIBILITY_PRIVATE);
else
b.setVisibility(VISIBILITY_SECRET);
}
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(BLOG_POST_NOTIFICATION_ID, b.build());
@@ -623,20 +597,17 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@UiThread
private void updateIntroductionNotification() {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
BriarNotificationBuilder b = new BriarNotificationBuilder(appContext);
b.setSmallIcon(R.drawable.notification_introduction);
b.setColor(ContextCompat.getColor(appContext, R.color.briar_primary));
b.setColorRes(R.color.briar_primary);
b.setContentTitle(appContext.getText(R.string.app_name));
b.setContentText(appContext.getResources().getQuantityString(
R.plurals.introduction_notification_text, introductionTotal,
introductionTotal));
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (!StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
b.setLockscreenVisibility(CATEGORY_MESSAGE, showOnLockScreen);
playSound(b);
// Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_CONTACTS, true);
@@ -646,15 +617,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_MESSAGE);
boolean showOnLockScreen =
settings.getBoolean(PREF_NOTIFY_LOCK_SCREEN, false);
if (showOnLockScreen)
b.setVisibility(VISIBILITY_PRIVATE);
else
b.setVisibility(VISIBILITY_SECRET);
}
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());

View File

@@ -2,6 +2,9 @@ package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.os.StrictMode.VmPolicy;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
@@ -33,6 +36,8 @@ import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_APP_START_DATE;
import static org.acra.ReportField.USER_CRASH_DATE;
import static org.briarproject.briar.android.TestingConstants.DEFAULT_LOG_LEVEL;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@ReportsCrashes(
reportPrimerClass = BriarReportPrimer.class,
@@ -72,6 +77,9 @@ public class BriarApplicationImpl extends Application
@Override
public void onCreate() {
super.onCreate();
if (IS_DEBUG_BUILD) enableStrictMode();
Logger.getLogger("").setLevel(DEFAULT_LOG_LEVEL);
LOG.info("Created");
applicationComponent = DaggerAndroidComponent.builder()
@@ -85,6 +93,17 @@ public class BriarApplicationImpl extends Application
AndroidEagerSingletons.initEagerSingletons(applicationComponent);
}
private void enableStrictMode() {
ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder();
threadPolicy.detectAll();
threadPolicy.penaltyLog();
StrictMode.setThreadPolicy(threadPolicy.build());
VmPolicy.Builder vmPolicy = new VmPolicy.Builder();
vmPolicy.detectAll();
vmPolicy.penaltyLog();
StrictMode.setVmPolicy(vmPolicy.build());
}
@Override
public AndroidComponent getApplicationComponent() {
return applicationComponent;

View File

@@ -10,13 +10,21 @@ import static java.util.logging.Level.OFF;
public interface TestingConstants {
/**
* Whether this is an alpha or beta build. This should be set to false for
* Whether this is a debug build.
*/
boolean IS_DEBUG_BUILD = BuildConfig.DEBUG;
/**
* Whether this is a beta build. This should be set to false for final
* release builds.
*/
boolean TESTING = BuildConfig.DEBUG;
boolean IS_BETA_BUILD = true;
/** Default log level. */
Level DEFAULT_LOG_LEVEL = TESTING ? INFO : OFF;
/**
* Default log level. Disable logging for final release builds.
*/
@SuppressWarnings("ConstantConditions")
Level DEFAULT_LOG_LEVEL = IS_DEBUG_BUILD || IS_BETA_BUILD ? INFO : OFF;
/**
* Whether to prevent screenshots from being taken. Setting this to true
@@ -24,5 +32,5 @@ public interface TestingConstants {
* Unfortunately this also prevents the user from taking screenshots
* intentionally.
*/
boolean PREVENT_SCREENSHOTS = !TESTING;
boolean PREVENT_SCREENSHOTS = !IS_DEBUG_BUILD;
}

View File

@@ -24,7 +24,6 @@ import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
import org.briarproject.briar.android.invitation.AddContactActivity;
import org.briarproject.briar.android.keyagreement.IntroFragment;
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
@@ -91,8 +90,6 @@ public interface ActivityComponent {
void inject(PanicPreferencesActivity activity);
void inject(AddContactActivity activity);
void inject(KeyAgreementActivity activity);
void inject(ConversationActivity activity);

View File

@@ -60,8 +60,7 @@ abstract class BasePostFragment extends BaseFragment {
false);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
ui = new BlogPostViewHolder(view);
ui.setOnBlogPostClickListener(new OnBlogPostClickListener() {
ui = new BlogPostViewHolder(view, true, new OnBlogPostClickListener() {
@Override
public void onBlogPostClick(BlogPostItem post) {
// We're already there

View File

@@ -106,7 +106,7 @@ class BlogControllerImpl extends BaseControllerImpl
BlogInvitationResponseReceivedEvent b =
(BlogInvitationResponseReceivedEvent) e;
InvitationResponse r = b.getResponse();
if (r.getGroupId().equals(groupId) && r.wasAccepted()) {
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Blog invitation accepted");
onBlogInvitationAccepted(b.getContactId());
}

View File

@@ -23,8 +23,7 @@ class BlogPostAdapter
int viewType) {
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_blog_post, parent, false);
BlogPostViewHolder ui = new BlogPostViewHolder(v);
ui.setOnBlogPostClickListener(listener);
BlogPostViewHolder ui = new BlogPostViewHolder(v, false, listener);
return ui;
}

View File

@@ -8,14 +8,16 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.BaseController.BlogListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import javax.inject.Inject;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BlogPostFragment extends BasePostFragment {
public class BlogPostFragment extends BasePostFragment implements BlogListener {
private static final String TAG = BlogPostFragment.class.getName();
@@ -40,6 +42,7 @@ public class BlogPostFragment extends BasePostFragment {
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogListener(this);
}
@Override
@@ -59,4 +62,15 @@ public class BlogPostFragment extends BasePostFragment {
}
});
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
// doesn't matter here
}
@Override
public void onBlogRemoved() {
finish();
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
@@ -47,12 +48,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final ImageView reblogButton;
private final TextView body;
private final ViewGroup commentContainer;
private final boolean fullText;
@Nullable
private OnBlogPostClickListener listener;
@NonNull
private final OnBlogPostClickListener listener;
BlogPostViewHolder(View v) {
BlogPostViewHolder(View v, boolean fullText,
@NonNull OnBlogPostClickListener listener) {
super(v);
this.fullText = fullText;
this.listener = listener;
ctx = v.getContext();
layout = (ViewGroup) v.findViewById(R.id.postLayout);
@@ -64,10 +69,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
(ViewGroup) v.findViewById(R.id.commentContainer);
}
void setOnBlogPostClickListener(OnBlogPostClickListener listener) {
this.listener = listener;
}
void setVisibility(int visibility) {
layout.setVisibility(visibility);
}
@@ -92,7 +93,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
if (item == null) return;
setTransitionName(item.getId());
if (listener != null) {
if (!fullText) {
layout.setClickable(true);
layout.setOnClickListener(new OnClickListener() {
@Override
@@ -111,7 +112,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
author.setPersona(
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
// TODO make author clickable more often #624
if (listener != null && item.getHeader().getType() == POST) {
if (!fullText && item.getHeader().getType() == POST) {
author.setAuthorClickable(new OnClickListener() {
@Override
public void onClick(View v) {
@@ -124,7 +125,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
// post body
Spanned bodyText = getSpanned(item.getBody());
if (listener == null) {
if (fullText) {
body.setText(bodyText);
body.setTextIsSelectable(true);
makeLinksClickable(body);
@@ -170,7 +171,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
reblogger.setAuthor(item.getAuthor());
reblogger.setAuthorStatus(item.getAuthorStatus());
reblogger.setDate(item.getTimestamp());
if (listener != null) {
if (!fullText) {
reblogger.setAuthorClickable(new OnClickListener() {
@Override
public void onClick(View v) {
@@ -200,7 +201,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
// TODO make author clickable #624
body.setText(c.getComment());
if (listener == null) body.setTextIsSelectable(true);
if (fullText) body.setTextIsSelectable(true);
commentContainer.addView(v);
}

View File

@@ -18,8 +18,8 @@ public class ReblogActivity extends BriarActivity implements
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSceneTransitionAnimation();
super.onCreate(savedInstanceState);
Intent intent = getIntent();
byte[] groupId = intent.getByteArrayExtra(GROUP_ID);

View File

@@ -161,7 +161,18 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
private ViewHolder(View v) {
scrollView = (ScrollView) v.findViewById(R.id.scrollView);
progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout));
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
true, new OnBlogPostClickListener() {
@Override
public void onBlogPostClick(BlogPostItem post) {
// do nothing
}
@Override
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
});
input = (TextInputView) v.findViewById(R.id.inputText);
}
}

View File

@@ -186,8 +186,8 @@ public class ConversationActivity extends BriarActivity
@SuppressWarnings("ConstantConditions")
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
setSceneTransitionAnimation();
super.onCreate(state);
Intent i = getIntent();
int id = i.getIntExtra(CONTACT_ID, -1);

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.forum;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
@@ -43,10 +44,10 @@ public class CreateForumActivity extends BriarActivity {
private static final Logger LOG =
Logger.getLogger(CreateForumActivity.class.getName());
private TextInputLayout nameEntryLayout;
private EditText nameEntry;
private Button createForumButton;
private ProgressBar progress;
private TextView feedback;
// Fields that are accessed from background threads must be volatile
@Inject
@@ -58,6 +59,8 @@ public class CreateForumActivity extends BriarActivity {
setContentView(R.layout.activity_create_forum);
nameEntryLayout =
(TextInputLayout) findViewById(R.id.createForumNameLayout);
nameEntry = (EditText) findViewById(R.id.createForumNameEntry);
nameEntry.addTextChangedListener(new TextWatcher() {
@@ -85,8 +88,6 @@ public class CreateForumActivity extends BriarActivity {
}
});
feedback = (TextView) findViewById(R.id.createForumFeedback);
createForumButton = (Button) findViewById(R.id.createForumButton);
createForumButton.setOnClickListener(new OnClickListener() {
@Override
@@ -118,10 +119,10 @@ public class CreateForumActivity extends BriarActivity {
String name = nameEntry.getText().toString();
int length = StringUtils.toUtf8(name).length;
if (length > MAX_FORUM_NAME_LENGTH) {
feedback.setText(R.string.name_too_long);
nameEntryLayout.setError(getString(R.string.name_too_long));
return false;
}
feedback.setText("");
nameEntryLayout.setError(null);
return length > 0;
}

View File

@@ -28,7 +28,6 @@ import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumPostHeader;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -41,7 +40,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_BOD
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ForumActivity extends
ThreadListActivity<Forum, ThreadItemAdapter<ForumItem>, ForumItem, ForumPostHeader>
ThreadListActivity<Forum, ForumItem, ThreadItemAdapter<ForumItem>>
implements ForumListener {
@Inject
@@ -53,7 +52,7 @@ public class ForumActivity extends
}
@Override
protected ThreadListController<Forum, ForumItem, ForumPostHeader> getController() {
protected ThreadListController<Forum, ForumItem> getController() {
return forumController;
}

View File

@@ -6,13 +6,11 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumPostHeader;
@NotNullByDefault
interface ForumController
extends ThreadListController<Forum, ForumItem, ForumPostHeader> {
interface ForumController extends ThreadListController<Forum, ForumItem> {
interface ForumListener extends ThreadListListener<ForumPostHeader> {
interface ForumListener extends ThreadListListener<ForumItem> {
@UiThread
void onForumLeft(ContactId c);
}

View File

@@ -75,25 +75,25 @@ class ForumControllerImpl extends
super.eventOccurred(e);
if (e instanceof ForumPostReceivedEvent) {
ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
if (pe.getGroupId().equals(getGroupId())) {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
if (f.getGroupId().equals(getGroupId())) {
LOG.info("Forum post received, adding...");
onForumPostHeaderReceived(pe.getForumPostHeader());
onForumPostReceived(f.getHeader(), f.getBody());
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
ForumInvitationResponseReceivedEvent f =
(ForumInvitationResponseReceivedEvent) e;
ForumInvitationResponse r =
(ForumInvitationResponse) f.getResponse();
if (r.getGroupId().equals(getGroupId()) && r.wasAccepted()) {
if (r.getShareableId().equals(getGroupId()) && r.wasAccepted()) {
LOG.info("Forum invitation was accepted");
onForumInvitationAccepted(r.getContactId());
}
} else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
if (s.getGroupId().equals(getGroupId())) {
ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
if (c.getGroupId().equals(getGroupId())) {
LOG.info("Forum left by contact");
onForumLeft(s.getContactId());
onForumLeft(c.getContactId());
}
}
}
@@ -195,11 +195,12 @@ class ForumControllerImpl extends
return new ForumItem(header, body);
}
private void onForumPostHeaderReceived(final ForumPostHeader h) {
private void onForumPostReceived(ForumPostHeader h, String body) {
final ForumItem item = buildItem(h, body);
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
listener.onHeaderReceived(h);
listener.onItemReceived(item);
}
});
}

View File

@@ -254,7 +254,7 @@ public class ForumListFragment extends BaseEventFragment implements
} else if (e instanceof ForumPostReceivedEvent) {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
LOG.info("Forum post added, updating item");
updateItem(f.getGroupId(), f.getForumPostHeader());
updateItem(f.getGroupId(), f.getHeader());
} else if (e instanceof ForumInvitationRequestReceivedEvent) {
LOG.info("Forum invitation received, reloading available forums");
loadAvailableForums();

View File

@@ -1,449 +0,0 @@
package org.briarproject.briar.android.invitation;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.invitation.InvitationListener;
import org.briarproject.bramble.api.invitation.InvitationState;
import org.briarproject.bramble.api.invitation.InvitationTask;
import org.briarproject.bramble.api.invitation.InvitationTaskFactory;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.api.android.ReferenceManager;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.CONNECTED;
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.DETAILS;
import static org.briarproject.briar.android.invitation.ConfirmationCodeView.ConfirmationState.WAIT_FOR_CONTACT;
public class AddContactActivity extends BriarActivity
implements InvitationListener {
private static final Logger LOG =
Logger.getLogger(AddContactActivity.class.getName());
@Inject
CryptoComponent crypto;
@Inject
InvitationTaskFactory invitationTaskFactory;
@Inject
ReferenceManager referenceManager;
private AddContactView view = null;
private InvitationTask task = null;
private long taskHandle = -1;
private AuthorId localAuthorId = null;
private int localInvitationCode = -1, remoteInvitationCode = -1;
private int localConfirmationCode = -1, remoteConfirmationCode = -1;
private boolean connected = false, connectionFailed = false;
private boolean localCompared = false, remoteCompared = false;
private boolean localMatched = false, remoteMatched = false;
private String contactName = null;
// Fields that are accessed from background threads must be volatile
@Inject
volatile IdentityManager identityManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
if (state == null) {
// This is a new activity
setView(new ChooseIdentityView(this));
} else {
// Restore the activity's state
byte[] b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
if (b != null) localAuthorId = new AuthorId(b);
taskHandle = state.getLong("briar.TASK_HANDLE", -1);
task = referenceManager.getReference(taskHandle,
InvitationTask.class);
if (task == null) {
// No background task - we must be in an initial or final state
localInvitationCode = state.getInt("briar.LOCAL_CODE");
remoteInvitationCode = state.getInt("briar.REMOTE_CODE");
connectionFailed = state.getBoolean("briar.FAILED");
contactName = state.getString("briar.CONTACT_NAME");
if (contactName != null) {
localCompared = remoteCompared = true;
localMatched = remoteMatched = true;
}
// Set the appropriate view for the state
if (localInvitationCode == -1) {
setView(new ChooseIdentityView(this));
} else if (remoteInvitationCode == -1) {
setView(new InvitationCodeView(this));
} else if (connectionFailed) {
setView(new ErrorView(this, R.string.connection_failed,
R.string.could_not_find_contact));
} else if (contactName == null) {
setView(new ErrorView(this, R.string.codes_do_not_match,
R.string.interfering));
} else {
showToastAndFinish();
}
} else {
// A background task exists - listen to it and get its state
InvitationState s = task.addListener(this);
localInvitationCode = s.getLocalInvitationCode();
remoteInvitationCode = s.getRemoteInvitationCode();
localConfirmationCode = s.getLocalConfirmationCode();
remoteConfirmationCode = s.getRemoteConfirmationCode();
connected = s.getConnected();
connectionFailed = s.getConnectionFailed();
localCompared = s.getLocalCompared();
remoteCompared = s.getRemoteCompared();
localMatched = s.getLocalMatched();
remoteMatched = s.getRemoteMatched();
contactName = s.getContactName();
// Set the appropriate view for the state
if (localInvitationCode == -1) {
setView(new ChooseIdentityView(this));
} else if (remoteInvitationCode == -1) {
setView(new InvitationCodeView(this));
} else if (connectionFailed) {
setView(new ErrorView(AddContactActivity.this,
R.string.connection_failed,
R.string.could_not_find_contact));
} else if (connected && localConfirmationCode == -1) {
setView(new ConfirmationCodeView(this, CONNECTED));
} else if (localConfirmationCode == -1) {
setView(new InvitationCodeView(this, true));
} else if (!localCompared) {
setView(new ConfirmationCodeView(this));
} else if (!remoteCompared) {
setView(new ConfirmationCodeView(this, WAIT_FOR_CONTACT));
} else if (localMatched && remoteMatched) {
if (contactName == null) {
setView(new ConfirmationCodeView(this, DETAILS));
} else {
showToastAndFinish();
}
} else {
setView(new ErrorView(this, R.string.codes_do_not_match,
R.string.interfering));
}
}
}
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
private void showToastAndFinish() {
String format = getString(R.string.contact_added_toast);
String text = String.format(format, contactName);
Toast.makeText(this, text, LENGTH_LONG).show();
supportFinishAfterTransition();
}
@Override
public void onStart() {
super.onStart();
view.populate();
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
if (localAuthorId != null) {
byte[] b = localAuthorId.getBytes();
state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
}
state.putInt("briar.LOCAL_CODE", localInvitationCode);
state.putInt("briar.REMOTE_CODE", remoteInvitationCode);
state.putBoolean("briar.FAILED", connectionFailed);
state.putString("briar.CONTACT_NAME", contactName);
if (task != null) state.putLong("briar.TASK_HANDLE", taskHandle);
}
@Override
public void onDestroy() {
super.onDestroy();
if (task != null) task.removeListener(this);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
if (request == REQUEST_BLUETOOTH) {
if (result != RESULT_CANCELED) reset(new InvitationCodeView(this));
}
}
@SuppressWarnings("ConstantConditions")
void setView(AddContactView view) {
this.view = view;
view.init(this);
setContentView(view);
getSupportActionBar().setTitle(R.string.add_contact_title);
}
void reset(AddContactView view) {
// Don't reset localAuthorId
task = null;
taskHandle = -1;
localInvitationCode = -1;
localConfirmationCode = remoteConfirmationCode = -1;
connected = connectionFailed = false;
localCompared = remoteCompared = false;
localMatched = remoteMatched = false;
contactName = null;
setView(view);
}
void loadLocalAuthor() {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
LocalAuthor author = identityManager.getLocalAuthor();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading author took " + duration + " ms");
setLocalAuthorId(author.getId());
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
void setLocalAuthorId(final AuthorId localAuthorId) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
AddContactActivity.this.localAuthorId = localAuthorId;
}
});
}
int getLocalInvitationCode() {
if (localInvitationCode == -1)
localInvitationCode = crypto.generateBTInvitationCode();
return localInvitationCode;
}
int getRemoteInvitationCode() {
return remoteInvitationCode;
}
void remoteInvitationCodeEntered(int code) {
if (localAuthorId == null) throw new IllegalStateException();
if (localInvitationCode == -1) throw new IllegalStateException();
remoteInvitationCode = code;
// change UI to show a progress indicator
setView(new InvitationCodeView(this, true));
task = invitationTaskFactory.createTask(localInvitationCode, code);
taskHandle = referenceManager.putReference(task, InvitationTask.class);
task.addListener(AddContactActivity.this);
// Add a second listener so we can remove the first in onDestroy(),
// allowing the activity to be garbage collected if it's destroyed
task.addListener(new ReferenceCleaner(referenceManager, taskHandle));
task.connect();
}
int getLocalConfirmationCode() {
return localConfirmationCode;
}
void remoteConfirmationCodeEntered(int code) {
localCompared = true;
if (code == remoteConfirmationCode) {
localMatched = true;
if (remoteMatched) {
setView(new ConfirmationCodeView(this, DETAILS));
} else if (remoteCompared) {
setView(new ErrorView(this, R.string.codes_do_not_match,
R.string.interfering));
} else {
setView(new ConfirmationCodeView(this, WAIT_FOR_CONTACT));
}
task.localConfirmationSucceeded();
} else {
localMatched = false;
setView(new ErrorView(this, R.string.codes_do_not_match,
R.string.interfering));
task.localConfirmationFailed();
}
}
@Override
public void connectionSucceeded() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
connected = true;
setView(new ConfirmationCodeView(AddContactActivity.this,
CONNECTED));
}
});
}
@Override
public void connectionFailed() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
connectionFailed = true;
setView(new ErrorView(AddContactActivity.this,
R.string.connection_failed,
R.string.could_not_find_contact));
}
});
}
@Override
public void keyAgreementSucceeded(final int localCode,
final int remoteCode) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
localConfirmationCode = localCode;
remoteConfirmationCode = remoteCode;
setView(new ConfirmationCodeView(AddContactActivity.this));
}
});
}
@Override
public void keyAgreementFailed() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
connectionFailed = true;
setView(new ErrorView(AddContactActivity.this,
R.string.connection_failed,
R.string.could_not_find_contact));
}
});
}
@Override
public void remoteConfirmationSucceeded() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
remoteCompared = true;
remoteMatched = true;
if (localMatched) {
setView(new ConfirmationCodeView(AddContactActivity.this,
DETAILS));
}
}
});
}
@Override
public void remoteConfirmationFailed() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
remoteCompared = true;
remoteMatched = false;
if (localMatched) {
setView(new ErrorView(AddContactActivity.this,
R.string.codes_do_not_match, R.string.interfering));
}
}
});
}
@Override
public void pseudonymExchangeSucceeded(final String remoteName) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
contactName = remoteName;
showToastAndFinish();
}
});
}
@Override
public void pseudonymExchangeFailed() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
setView(new ErrorView(AddContactActivity.this,
R.string.connection_failed,
R.string.could_not_find_contact));
}
});
}
/**
* Cleans up the reference to the invitation task when the task completes.
* This class is static to prevent memory leaks.
*/
private static class ReferenceCleaner implements InvitationListener {
private final ReferenceManager referenceManager;
private final long handle;
private ReferenceCleaner(ReferenceManager referenceManager,
long handle) {
this.referenceManager = referenceManager;
this.handle = handle;
}
@Override
public void connectionSucceeded() {
// Wait for key agreement to succeed or fail
}
@Override
public void connectionFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
@Override
public void keyAgreementSucceeded(int localCode, int remoteCode) {
// Wait for remote confirmation to succeed or fail
}
@Override
public void keyAgreementFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
@Override
public void remoteConfirmationSucceeded() {
// Wait for the pseudonym exchange to succeed or fail
}
@Override
public void remoteConfirmationFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
@Override
public void pseudonymExchangeSucceeded(String remoteName) {
referenceManager.removeReference(handle, InvitationTask.class);
}
@Override
public void pseudonymExchangeFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
}
}

View File

@@ -1,22 +0,0 @@
package org.briarproject.briar.android.invitation;
import android.content.Context;
import android.widget.LinearLayout;
abstract class AddContactView extends LinearLayout {
static final public int CODE_LEN = 6;
protected AddContactActivity container = null;
AddContactView(Context ctx) {
super(ctx);
}
void init(AddContactActivity container) {
this.container = container;
populate();
}
abstract void populate();
}

View File

@@ -1,43 +0,0 @@
package org.briarproject.briar.android.invitation;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import org.briarproject.briar.R;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
class ChooseIdentityView extends AddContactView implements OnClickListener {
ChooseIdentityView(Context ctx) {
super(ctx);
}
@Override
void populate() {
removeAllViews();
Context ctx = getContext();
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.invitation_bluetooth_start, this);
Button continueButton = (Button) view.findViewById(R.id.continueButton);
continueButton.setOnClickListener(this);
container.loadLocalAuthor();
}
@Override
public void onClick(View view) {
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
container.startActivityForResult(i, REQUEST_BLUETOOTH);
}
}

View File

@@ -1,121 +0,0 @@
package org.briarproject.briar.android.invitation;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import org.briarproject.briar.R;
import static android.content.Context.INPUT_METHOD_SERVICE;
class ConfirmationCodeView extends AddContactView {
public enum ConfirmationState { CONNECTED, ENTER_CODE, WAIT_FOR_CONTACT, DETAILS }
private ConfirmationState state;
ConfirmationCodeView(Context ctx) {
super(ctx);
this.state = ConfirmationState.ENTER_CODE;
}
ConfirmationCodeView(Context ctx, ConfirmationState state) {
super(ctx);
this.state = state;
}
@Override
void populate() {
removeAllViews();
Context ctx = getContext();
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.invitation_bluetooth_confirmation_code, this);
// local confirmation code
TextView code = (TextView) view.findViewById(R.id.codeView);
int localCode = container.getLocalConfirmationCode();
code.setText(String.format("%06d", localCode));
if (state != ConfirmationState.ENTER_CODE) {
// hide views we no longer need
view.findViewById(R.id.enterCodeTextView).setVisibility(View.GONE);
view.findViewById(R.id.codeEntryView).setVisibility(View.GONE);
view.findViewById(R.id.continueButton).setVisibility(View.GONE);
// show progress indicator
view.findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
// show what we are waiting for
TextView connecting = (TextView) view.findViewById(R.id.waitingView);
int textId;
if (state == ConfirmationState.CONNECTED) {
textId = R.string.calculating_confirmation_code;
view.findViewById(R.id.yourConfirmationCodeView).setVisibility(View.GONE);
view.findViewById(R.id.codeView).setVisibility(View.GONE);
} else if (state == ConfirmationState.WAIT_FOR_CONTACT) {
textId = R.string.waiting_for_contact;
} else {
textId = R.string.exchanging_contact_details;
}
connecting.setText(ctx.getString(textId));
connecting.setVisibility(View.VISIBLE);
}
else {
// handle click on continue button
final EditText codeEntry = (EditText) view.findViewById(R.id.codeEntryView);
final Button continueButton = (Button) view.findViewById(R.id.continueButton);
continueButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
send(codeEntry);
}
});
// activate continue button only when we have a 6 digit (CODE_LEN) code
codeEntry.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
continueButton.setEnabled(codeEntry.getText().length() == CODE_LEN);
}
@Override
public void afterTextChanged(Editable s) {
}
});
codeEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO && v.getText().length() == CODE_LEN) {
send(v);
return true;
}
return false;
}
});
}
}
private void send(TextView codeEntry) {
int code = Integer.parseInt(codeEntry.getText().toString());
container.remoteConfirmationCodeEntered(code);
// Hide the soft keyboard
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(codeEntry.getWindowToken(), 0);
}
}

View File

@@ -1,59 +0,0 @@
package org.briarproject.briar.android.invitation;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import org.briarproject.briar.R;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH;
class ErrorView extends AddContactView implements OnClickListener {
private final int error;
private final int explanation;
ErrorView(Context ctx) {
super(ctx);
this.error = R.string.connection_failed;
this.explanation = R.string.could_not_find_contact;
}
ErrorView(Context ctx, int error, int explanation) {
super(ctx);
this.error = error;
this.explanation = explanation;
}
@Override
void populate() {
removeAllViews();
Context ctx = getContext();
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.invitation_error, this);
TextView errorView = (TextView) view.findViewById(R.id.errorTextView);
errorView.setText(ctx.getString(error));
TextView explanationView = (TextView) view.findViewById(R.id.explanationTextView);
explanationView.setText(ctx.getString(explanation));
Button tryAgainButton = (Button) view.findViewById(R.id.tryAgainButton);
tryAgainButton.setOnClickListener(this);
}
@Override
public void onClick(View view) {
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
container.startActivityForResult(i, REQUEST_BLUETOOTH);
}
}

View File

@@ -1,111 +0,0 @@
package org.briarproject.briar.android.invitation;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import org.briarproject.briar.R;
import static android.content.Context.INPUT_METHOD_SERVICE;
class InvitationCodeView extends AddContactView {
private boolean waiting;
InvitationCodeView(Context ctx, boolean waiting) {
super(ctx);
this.waiting = waiting;
}
InvitationCodeView(Context ctx) {
this(ctx, false);
}
@Override
void populate() {
removeAllViews();
Context ctx = getContext();
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.invitation_bluetooth_invitation_code, this);
// local invitation code
TextView code = (TextView) view.findViewById(R.id.codeView);
int localCode = container.getLocalInvitationCode();
code.setText(String.format("%06d", localCode));
if (waiting) {
// hide views we no longer need
view.findViewById(R.id.enterCodeTextView).setVisibility(View.GONE);
view.findViewById(R.id.codeEntryView).setVisibility(View.GONE);
view.findViewById(R.id.continueButton).setVisibility(View.GONE);
// show progress indicator
view.findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
// show which code we are waiting for
TextView connecting = (TextView) view.findViewById(R.id.waitingView);
int remoteCode = container.getRemoteInvitationCode();
String format = container.getString(R.string.searching_format);
connecting.setText(String.format(format, remoteCode));
connecting.setVisibility(View.VISIBLE);
}
else {
// handle click on continue button
final EditText codeEntry = (EditText) view.findViewById(R.id.codeEntryView);
final Button continueButton = (Button) view.findViewById(R.id.continueButton);
continueButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
send(codeEntry);
}
});
// activate continue button only when we have a 6 digit (CODE_LEN) code
codeEntry.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
continueButton.setEnabled(codeEntry.getText().length() == CODE_LEN);
}
@Override
public void afterTextChanged(Editable s) {
}
});
codeEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO && v.getText().length() == CODE_LEN) {
send(v);
return true;
}
return false;
}
});
}
}
private void send(TextView codeEntry) {
int code = Integer.parseInt(codeEntry.getText().toString());
container.remoteInvitationCodeEntered(code);
// Hide the soft keyboard
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(codeEntry.getWindowToken(), 0);
}
}

View File

@@ -0,0 +1,14 @@
package org.briarproject.briar.android.keyagreement;
import java.io.IOException;
class CameraException extends IOException {
CameraException(String message) {
super(message);
}
CameraException(Throwable cause) {
super(cause);
}
}

View File

@@ -7,6 +7,7 @@ import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.Surface;
@@ -43,6 +44,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
private static final Logger LOG =
Logger.getLogger(CameraView.class.getName());
@Nullable
private Camera camera = null;
private PreviewConsumer previewConsumer = null;
private Surface surface = null;
@@ -82,14 +84,14 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
}
@UiThread
public void start() {
public void start() throws CameraException {
LOG.info("Opening camera");
try {
LOG.info("Opening camera");
camera = Camera.open();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error opening camera", e);
return;
throw new CameraException(e);
}
if (camera == null) throw new CameraException("No back-facing camera");
setDisplayOrientation(0);
// Use barcode scene mode if it's available
Parameters params = camera.getParameters();
@@ -113,60 +115,81 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
}
@UiThread
public void stop() {
public void stop() throws CameraException {
if (camera == null) return;
stopPreview();
LOG.info("Releasing camera");
try {
LOG.info("Releasing camera");
camera.release();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error releasing camera", e);
throw new CameraException(e);
}
camera = null;
}
@UiThread
private void startPreview(SurfaceHolder holder) {
private void startPreview(SurfaceHolder holder) throws CameraException {
LOG.info("Starting preview");
if (camera == null) throw new CameraException("Camera is null");
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
previewStarted = true;
startConsumer();
} catch (IOException | RuntimeException e) {
LOG.log(WARNING, "Error starting camera preview", e);
throw new CameraException(e);
}
}
@UiThread
private void stopPreview() {
private void stopPreview() throws CameraException {
LOG.info("Stopping preview");
if (camera == null) throw new CameraException("Camera is null");
try {
stopConsumer();
camera.stopPreview();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error stopping camera preview", e);
throw new CameraException(e);
}
previewStarted = false;
}
@UiThread
private void startConsumer() {
if (autoFocus) camera.autoFocus(this);
private void startConsumer() throws CameraException {
if (camera == null) throw new CameraException("Camera is null");
if (autoFocus) {
try {
camera.autoFocus(this);
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
previewConsumer.start(camera);
}
@UiThread
private void stopConsumer() {
if (autoFocus) camera.cancelAutoFocus();
private void stopConsumer() throws CameraException {
if (camera == null) throw new CameraException("Camera is null");
if (autoFocus) {
try {
camera.cancelAutoFocus();
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
previewConsumer.stop();
}
@UiThread
private void setDisplayOrientation(int rotationDegrees) {
private void setDisplayOrientation(int rotationDegrees)
throws CameraException {
int orientation;
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(0, info);
try {
Camera.getCameraInfo(0, info);
} catch (RuntimeException e) {
throw new CameraException(e);
}
if (info.facing == CAMERA_FACING_FRONT) {
orientation = (info.orientation + rotationDegrees) % 360;
orientation = (360 - orientation) % 360;
@@ -175,49 +198,70 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
}
if (LOG.isLoggable(INFO))
LOG.info("Display orientation " + orientation + " degrees");
if (camera == null) throw new CameraException("Camera is null");
try {
camera.setDisplayOrientation(orientation);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error setting display orientation", e);
throw new CameraException(e);
}
displayOrientation = orientation;
}
@UiThread
private Parameters setSceneMode(Camera camera, Parameters params) {
private Parameters setSceneMode(Camera camera, Parameters params)
throws CameraException {
List<String> sceneModes = params.getSupportedSceneModes();
if (sceneModes == null) return params;
if (LOG.isLoggable(INFO)) LOG.info("Scene modes: " + sceneModes);
if (sceneModes.contains(SCENE_MODE_BARCODE)) {
params.setSceneMode(SCENE_MODE_BARCODE);
camera.setParameters(params);
return camera.getParameters();
try {
camera.setParameters(params);
return camera.getParameters();
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
return params;
}
@UiThread
private Parameters disableFlash(Camera camera, Parameters params) {
private Parameters disableFlash(Camera camera, Parameters params)
throws CameraException {
params.setFlashMode(FLASH_MODE_OFF);
camera.setParameters(params);
return camera.getParameters();
try {
camera.setParameters(params);
return camera.getParameters();
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
@UiThread
private Parameters disableSceneMode(Camera camera, Parameters params) {
private Parameters disableSceneMode(Camera camera, Parameters params)
throws CameraException {
params.setSceneMode(SCENE_MODE_AUTO);
camera.setParameters(params);
return camera.getParameters();
try {
camera.setParameters(params);
return camera.getParameters();
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
@UiThread
private Parameters setBestParameters(Camera camera, Parameters params) {
private Parameters setBestParameters(Camera camera, Parameters params)
throws CameraException {
setVideoStabilisation(params);
setFocusMode(params);
params.setFlashMode(FLASH_MODE_OFF);
setPreviewSize(params);
camera.setParameters(params);
return camera.getParameters();
try {
camera.setParameters(params);
return camera.getParameters();
} catch (RuntimeException e) {
throw new CameraException(e);
}
}
@UiThread
@@ -286,9 +330,15 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
}
@UiThread
private void logCameraParameters() {
private void logCameraParameters() throws CameraException {
if (camera == null) throw new AssertionError();
if (LOG.isLoggable(INFO)) {
Parameters params = camera.getParameters();
Parameters params;
try {
params = camera.getParameters();
} catch (RuntimeException e) {
throw new CameraException(e);
}
if (Build.VERSION.SDK_INT >= 15) {
LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization());
@@ -306,13 +356,18 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
post(new Runnable() {
@Override
public void run() {
surfaceCreatedUi(holder);
try {
surfaceCreatedUi(holder);
} catch (CameraException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@UiThread
private void surfaceCreatedUi(SurfaceHolder holder) {
private void surfaceCreatedUi(SurfaceHolder holder) throws CameraException {
LOG.info("Surface created");
if (surface != null && surface != holder.getSurface()) {
LOG.info("Releasing old surface");
@@ -329,13 +384,19 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
post(new Runnable() {
@Override
public void run() {
surfaceChangedUi(holder, w, h);
try {
surfaceChangedUi(holder, w, h);
} catch (CameraException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@UiThread
private void surfaceChangedUi(SurfaceHolder holder, int w, int h) {
private void surfaceChangedUi(SurfaceHolder holder, int w, int h)
throws CameraException {
if (LOG.isLoggable(INFO)) LOG.info("Surface changed: " + w + "x" + h);
if (surface != null && surface != holder.getSurface()) {
LOG.info("Releasing old surface");
@@ -352,7 +413,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
camera.setParameters(params);
logCameraParameters();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error setting preview size", e);
throw new CameraException(e);
}
startPreview(holder);
}

View File

@@ -22,6 +22,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
@MethodsNotNullByDefault
@@ -60,8 +61,12 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (camera == this.camera) {
Size size = camera.getParameters().getPreviewSize();
new DecoderTask(data, size.width, size.height).execute();
try {
Size size = camera.getParameters().getPreviewSize();
new DecoderTask(data, size.width, size.height).execute();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error getting camera parameters.", e);
}
}
}
@@ -70,7 +75,7 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
private final byte[] data;
private final int width, height;
DecoderTask(byte[] data, int width, int height) {
private DecoderTask(byte[] data, int width, int height) {
this.data = data;
this.width = width;
this.height = height;

View File

@@ -56,6 +56,7 @@ import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -143,6 +144,12 @@ public class ShowQrCodeFragment extends BaseEventFragment
public void onStart() {
super.onStart();
try {
cameraView.start();
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
}
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
@@ -162,7 +169,6 @@ public class ShowQrCodeFragment extends BaseEventFragment
} else {
startListening();
}
cameraView.start();
}
@Override
@@ -170,7 +176,19 @@ public class ShowQrCodeFragment extends BaseEventFragment
super.onStop();
stopListening();
if (receiver != null) getActivity().unregisterReceiver(receiver);
cameraView.stop();
try {
cameraView.stop();
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
}
}
@UiThread
private void logCameraExceptionAndFinish(CameraException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
Toast.makeText(getActivity(), R.string.camera_error,
LENGTH_LONG).show();
finish();
}
@UiThread
@@ -218,7 +236,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
statusView.setVisibility(VISIBLE);
status.setText(R.string.connecting_to_device);
task.connectAndRunProtocol(remotePayload);
} catch (IOException e) {
} catch (IOException | IllegalArgumentException e) {
// TODO show failure
Toast.makeText(getActivity(), R.string.qr_code_invalid,
LENGTH_LONG).show();

View File

@@ -29,7 +29,6 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListAct
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.Visibility;
@@ -44,7 +43,7 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class GroupActivity extends
ThreadListActivity<PrivateGroup, GroupMessageAdapter, GroupMessageItem, GroupMessageHeader>
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageAdapter>
implements GroupListener, OnClickListener {
@Inject
@@ -60,7 +59,7 @@ public class GroupActivity extends
}
@Override
protected ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> getController() {
protected ThreadListController<PrivateGroup, GroupMessageItem> getController() {
return controller;
}
@@ -276,7 +275,7 @@ public class GroupActivity extends
public void onGroupDissolved() {
setGroupEnabled(false);
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_dissolved_dialog_title));
builder.setMessage(getString(R.string.groups_dissolved_dialog_message));
builder.setNeutralButton(R.string.ok, null);

View File

@@ -8,13 +8,11 @@ import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.Visibility;
public interface GroupController
extends
ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
extends ThreadListController<PrivateGroup, GroupMessageItem> {
void loadLocalAuthor(
ResultExceptionHandler<LocalAuthor, DbException> handler);
@@ -22,7 +20,8 @@ public interface GroupController
void isDissolved(
ResultExceptionHandler<Boolean, DbException> handler);
interface GroupListener extends ThreadListListener<GroupMessageHeader> {
interface GroupListener extends ThreadListListener<GroupMessageItem> {
@UiThread
void onContactRelationshipRevealed(AuthorId memberId,
ContactId contactId, Visibility v);

View File

@@ -80,14 +80,15 @@ class GroupControllerImpl extends
super.eventOccurred(e);
if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent gmae = (GroupMessageAddedEvent) e;
if (!gmae.isLocal() && gmae.getGroupId().equals(getGroupId())) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
if (!g.isLocal() && g.getGroupId().equals(getGroupId())) {
LOG.info("Group message received, adding...");
final GroupMessageHeader h = gmae.getHeader();
final GroupMessageItem item =
buildItem(g.getHeader(), g.getBody());
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
listener.onHeaderReceived(h);
listener.onItemReceived(item);
}
});
}
@@ -108,7 +109,7 @@ class GroupControllerImpl extends
(GroupInvitationResponseReceivedEvent) e;
final GroupInvitationResponse r =
(GroupInvitationResponse) g.getResponse();
if (getGroupId().equals(r.getGroupId()) && r.wasAccepted()) {
if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) {
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.privategroup.creation;
import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
@@ -11,6 +12,7 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
@@ -19,6 +21,8 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
public class CreateGroupFragment extends BaseFragment {
@@ -28,7 +32,8 @@ public class CreateGroupFragment extends BaseFragment {
private CreateGroupListener listener;
private EditText nameEntry;
private Button createGroupButton;
private TextView feedback;
private TextInputLayout nameLayout;
private ProgressBar progress;
@Override
public void onAttach(Context context) {
@@ -69,7 +74,7 @@ public class CreateGroupFragment extends BaseFragment {
}
});
feedback = (TextView) v.findViewById(R.id.feedback);
nameLayout = (TextInputLayout) v.findViewById(R.id.nameLayout);
createGroupButton = (Button) v.findViewById(R.id.button);
createGroupButton.setOnClickListener(new OnClickListener() {
@@ -79,6 +84,8 @@ public class CreateGroupFragment extends BaseFragment {
}
});
progress = (ProgressBar) v.findViewById(R.id.progressBar);
return v;
}
@@ -107,16 +114,18 @@ public class CreateGroupFragment extends BaseFragment {
String name = nameEntry.getText().toString();
int length = StringUtils.toUtf8(name).length;
if (length > MAX_GROUP_NAME_LENGTH) {
feedback.setText(R.string.name_too_long);
nameLayout.setError(getString(R.string.name_too_long));
return false;
}
feedback.setText("");
nameLayout.setError(null);
return length > 0;
}
private void createGroup() {
if (!validateName()) return;
listener.hideSoftKeyboard(nameEntry);
createGroupButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
listener.onGroupNameChosen(nameEntry.getText().toString());
}
}

View File

@@ -15,6 +15,7 @@ import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.Toast;
import org.acra.ACRA;
import org.briarproject.bramble.api.db.DbException;
@@ -46,6 +47,7 @@ import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE;
@@ -400,10 +402,15 @@ public class SettingsFragment extends PreferenceFragmentCompat
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
String name = r.getTitle(getContext());
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
if (r == null) {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
} else {
String name = r.getTitle(getContext());
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
}
}
storeSettings(s);
}

View File

@@ -4,9 +4,6 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.os.StrictMode.VmPolicy;
import android.support.v7.preference.PreferenceManager;
import android.transition.Fade;
@@ -23,8 +20,6 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.briar.android.BriarApplication.EXPIRY_DATE;
import static org.briarproject.briar.android.TestingConstants.DEFAULT_LOG_LEVEL;
import static org.briarproject.briar.android.TestingConstants.TESTING;
public class SplashScreenActivity extends BaseActivity {
@@ -36,11 +31,6 @@ public class SplashScreenActivity extends BaseActivity {
@Inject
protected AndroidExecutor androidExecutor;
public SplashScreenActivity() {
Logger.getLogger("").setLevel(DEFAULT_LOG_LEVEL);
enableStrictMode();
}
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
@@ -81,19 +71,6 @@ public class SplashScreenActivity extends BaseActivity {
}
}
private void enableStrictMode() {
if (TESTING) {
ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder();
threadPolicy.detectAll();
threadPolicy.penaltyLog();
StrictMode.setThreadPolicy(threadPolicy.build());
VmPolicy.Builder vmPolicy = new VmPolicy.Builder();
vmPolicy.detectAll();
vmPolicy.penaltyLog();
StrictMode.setVmPolicy(vmPolicy.build());
}
}
private void setPreferencesDefaults() {
androidExecutor.runOnBackgroundThread(new Runnable() {
@Override

View File

@@ -3,7 +3,9 @@ package org.briarproject.briar.android.threaded;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.client.MessageTree;
import org.briarproject.briar.api.client.MessageTree.MessageNode;
import org.briarproject.briar.client.MessageTreeImpl;
import java.util.ArrayList;
@@ -13,8 +15,7 @@ import java.util.List;
@UiThread
@NotNullByDefault
public class NestedTreeList<T extends MessageTree.MessageNode>
implements Iterable<T> {
public class NestedTreeList<T extends MessageNode> implements Iterable<T> {
private final MessageTree<T> tree = new MessageTreeImpl<>();
private List<T> depthFirstCollection = new ArrayList<>();
@@ -38,14 +39,14 @@ public class NestedTreeList<T extends MessageTree.MessageNode>
return depthFirstCollection.get(index);
}
public int indexOf(T elem) {
return depthFirstCollection.indexOf(elem);
}
public int size() {
return depthFirstCollection.size();
}
public boolean contains(MessageId m) {
return tree.contains(m);
}
@Override
public Iterator<T> iterator() {
return depthFirstCollection.iterator();

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.threaded;
import android.os.Handler;
import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -28,7 +27,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
protected final NestedTreeList<I> items = new NestedTreeList<>();
private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager;
private final Handler handler = new Handler();
private volatile int revision = 0;
@@ -104,6 +102,10 @@ public class ThreadItemAdapter<I extends ThreadItem>
return NO_POSITION; // Not found
}
boolean contains(MessageId m) {
return items.contains(m);
}
/**
* Highlights the item with the given {@link MessageId}
* and disables the highlight for a previously highlighted item, if any.
@@ -184,6 +186,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
}
static class UnreadCount {
final int top, bottom;
private UnreadCount(int top, int bottom) {
@@ -193,6 +196,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
}
public interface ThreadItemListener<I> {
void onUnreadItemVisible(I item);
void onReplyClick(I item);

View File

@@ -16,6 +16,7 @@ public class ThreadItemListImpl<I extends ThreadItem> extends ArrayList<I>
return bottomVisibleItemId;
}
@Override
public void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId) {
this.bottomVisibleItemId = bottomVisibleItemId;
}

View File

@@ -32,7 +32,6 @@ import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.client.PostHeader;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import java.util.Collection;
@@ -49,9 +48,9 @@ import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCo
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadItemAdapter<I>, I extends ThreadItem, H extends PostHeader>
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
extends BriarActivity
implements ThreadListListener<H>, TextInputListener, SharingListener,
implements ThreadListListener<I>, TextInputListener, SharingListener,
ThreadItemListener<I>, ThreadListDataSource {
protected static final String KEY_REPLY_ID = "replyId";
@@ -68,7 +67,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Nullable
private MessageId replyId;
protected abstract ThreadListController<G, I, H> getController();
protected abstract ThreadListController<G, I> getController();
@Inject
protected SharingController sharingController;
@@ -190,8 +189,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
if (items.isEmpty()) {
list.showData();
} else {
initList(items);
updateTextInput(replyId);
displayItems(items);
updateTextInput();
}
} else {
LOG.info("Concurrent update, reloading");
@@ -206,7 +205,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
});
}
private void initList(final ThreadItemList<I> items) {
private void displayItems(final ThreadItemList<I> items) {
adapter.setItems(items);
MessageId messageId = items.getFirstVisibleItemId();
if (messageId != null)
@@ -253,9 +252,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ThreadItem replyItem = adapter.getHighlightedItem();
if (replyItem != null) {
outState.putByteArray(KEY_REPLY_ID, replyItem.getId().getBytes());
if (replyId != null) {
outState.putByteArray(KEY_REPLY_ID, replyId.getBytes());
}
}
@@ -273,7 +271,9 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onBackPressed() {
if (adapter.getHighlightedItem() != null) {
updateTextInput(null);
textInput.setText("");
replyId = null;
updateTextInput();
} else {
super.onBackPressed();
}
@@ -289,7 +289,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
@Override
public void onReplyClick(final I item) {
updateTextInput(item.getId());
replyId = item.getId();
updateTextInput();
if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item);
} else {
@@ -339,15 +340,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
snackbar.show();
}
private void updateTextInput(@Nullable MessageId replyItemId) {
if (replyItemId != null) {
private void updateTextInput() {
if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint);
textInput.requestFocus();
textInput.showSoftKeyboard();
} else {
textInput.setHint(R.string.forum_new_message_hint);
}
adapter.setHighlightedItem(replyItemId);
adapter.setHighlightedItem(replyId);
}
@Override
@@ -374,25 +375,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
getController().createAndStoreMessage(text, replyItem, handler);
textInput.hideSoftKeyboard();
textInput.setText("");
updateTextInput(null);
replyId = null;
updateTextInput();
}
protected abstract int getMaxBodyLength();
@Override
public void onHeaderReceived(H header) {
getController().loadItem(header,
new UiResultExceptionHandler<I, DbException>(this) {
@Override
public void onResultUi(final I result) {
addItem(result, false);
}
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
public void onItemReceived(I item) {
addItem(item, false);
}
@Override
@@ -400,8 +391,15 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
supportFinishAfterTransition();
}
protected void addItem(I item, boolean isLocal) {
private void addItem(I item, boolean isLocal) {
adapter.incrementRevision();
MessageId parent = item.getParentId();
if (parent != null && !adapter.contains(parent)) {
// We've incremented the adapter's revision, so the item will be
// loaded when its parent has been loaded
LOG.info("Ignoring item with missing parent");
return;
}
adapter.add(item);
if (isLocal) {

View File

@@ -12,14 +12,13 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.client.PostHeader;
import java.util.Collection;
import javax.annotation.Nullable;
@NotNullByDefault
public interface ThreadListController<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
public interface ThreadListController<G extends NamedGroup, I extends ThreadItem>
extends ActivityLifecycleController {
void setGroupId(GroupId groupId);
@@ -29,9 +28,8 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
void loadItems(ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
void loadItems(
ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
void markItemRead(I item);
@@ -42,9 +40,10 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
void deleteNamedGroup(ExceptionHandler<DbException> handler);
interface ThreadListListener<H> extends ThreadListDataSource {
interface ThreadListListener<I> extends ThreadListDataSource {
@UiThread
void onHeaderReceived(H header);
void onItemReceived(I item);
@UiThread
void onGroupRemoved();

View File

@@ -39,9 +39,9 @@ import static java.util.logging.Level.WARNING;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<H>>
public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<I>>
extends DbControllerImpl
implements ThreadListController<G, I, H>, EventListener {
implements ThreadListController<G, I>, EventListener {
private static final Logger LOG =
Logger.getLogger(ThreadListControllerImpl.class.getName());
@@ -203,35 +203,6 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
@DatabaseExecutor
protected abstract String loadMessageBody(H header) throws DbException;
@Override
public void loadItem(final H header,
final ResultExceptionHandler<I, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
String body;
if (!bodyCache.containsKey(header.getId())) {
body = loadMessageBody(header);
bodyCache.put(header.getId(), body);
} else {
body = bodyCache.get(header.getId());
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading item took " + duration + " ms");
I item = buildItem(header, body);
handler.onResult(item);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
public void markItemRead(I item) {
markItemsRead(Collections.singletonList(item));

View File

@@ -0,0 +1,38 @@
package org.briarproject.briar.android.util;
import android.content.Context;
import android.os.Build;
import android.support.annotation.ColorRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.NotificationCompat;
import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE;
import static android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
public class BriarNotificationBuilder extends NotificationCompat.Builder {
public BriarNotificationBuilder(Context context) {
super(context);
setAutoCancel(true);
}
public BriarNotificationBuilder setColorRes(@ColorRes int res) {
setColor(ContextCompat.getColor(mContext, res));
return this;
}
public BriarNotificationBuilder setLockscreenVisibility(String category,
boolean show) {
if (Build.VERSION.SDK_INT >= 21) {
setCategory(category);
if (show)
setVisibility(VISIBILITY_PRIVATE);
else
setVisibility(VISIBILITY_SECRET);
}
return this;
}
}

View File

@@ -104,8 +104,9 @@ public class EmojiPageView extends FrameLayout {
emojiSize + 2 * pad));
view = emojiView;
}
String emoji = model.getEmoji()[position];
view.setEmoji(emoji);
view.setEmoji(model.getEmoji()[position]);
return view;
}
}

View File

@@ -32,7 +32,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
private static final Logger LOG =
Logger.getLogger(RecentEmojiPageModel.class.getName());
private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent";
private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent2";
private static final int EMOJI_LRU_SIZE = 50;
private final LinkedHashSet<String> recentlyUsed; // UI thread
@@ -98,12 +98,12 @@ public class RecentEmojiPageModel implements EmojiPageModel {
}
private String serialize(LinkedHashSet<String> emojis) {
return StringUtils.join(emojis, ";");
return StringUtils.join(emojis, "\t");
}
private LinkedHashSet<String> deserialize(@Nullable String serialized) {
if (serialized == null) return new LinkedHashSet<>();
String[] list = serialized.split(";");
String[] list = serialized.split("\t");
LinkedHashSet<String> result = new LinkedHashSet<>(list.length);
Collections.addAll(result, list);
return result;

View File

@@ -1,37 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:layout_margin="@dimen/margin_large">
android:padding="@dimen/margin_large">
<EditText
<android.support.design.widget.TextInputLayout
android:id="@+id/createForumNameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/createForumNameEntry"
android:maxLines="1"
android:inputType="text|textCapSentences"
android:hint="@string/choose_forum_hint" />
app:errorEnabled="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/createForumFeedback"
android:gravity="center" />
<EditText
android:id="@+id/createForumNameEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/choose_forum_hint"
android:inputType="text|textCapSentences"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<Button
style="@style/BriarButton"
android:id="@+id/createForumButton"
style="@style/BriarButton"
android:enabled="false"
android:text="@string/create_forum_button" />
android:text="@string/create_forum_button"/>
<ProgressBar
android:id="@+id/createForumProgressBar"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>

View File

@@ -30,7 +30,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/expiryWarningClose"
android:text="@string/expiry_warning"
android:text="@plurals/expiry_warning"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="@dimen/text_size_small"/>

View File

@@ -1,30 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:layout_margin="@dimen/margin_large">
android:padding="@dimen/margin_large">
<EditText
android:id="@+id/name"
<android.support.design.widget.TextInputLayout
android:id="@+id/nameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:inputType="text|textCapSentences"
android:hint="@string/groups_create_group_hint"/>
app:errorEnabled="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/feedback"
android:gravity="center" />
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/groups_create_group_hint"
android:inputType="text|textCapSentences"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<Button
style="@style/BriarButton"
android:id="@+id/button"
style="@style/BriarButton"
android:enabled="false"
android:text="@string/groups_create_group_button"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>

View File

@@ -1,109 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_activity_vertical"
android:paddingEnd="@dimen/margin_activity_horizontal"
android:paddingRight="@dimen/margin_activity_horizontal"
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingLeft="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<TextView
android:id="@+id/connectedView"
style="@style/BriarTextTitle"
android:textSize="@dimen/text_size_large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connected_to_contact"
android:padding="@dimen/margin_medium"
android:layout_centerHorizontal="true"
android:drawableLeft="@drawable/navigation_accept"
android:drawableStart="@drawable/navigation_accept"
android:gravity="center_vertical"/>
<TextView
android:id="@+id/yourConfirmationCodeView"
style="@style/BriarTextBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/your_confirmation_code"
android:padding="@dimen/margin_medium"
android:layout_below="@+id/connectedView"
android:layout_centerHorizontal="true"/>
<TextView
android:id="@+id/codeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/margin_medium"
android:textSize="50sp"
android:textColor="@color/briar_text_secondary"
android:layout_below="@+id/yourConfirmationCodeView"
android:layout_centerHorizontal="true"
tools:text="1337"/>
<TextView
android:id="@+id/waitingView"
style="@style/BriarTextBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/searching_format"
android:layout_gravity="center_horizontal"
android:padding="@dimen/margin_medium"
android:layout_below="@+id/codeView"
android:layout_centerHorizontal="true"
android:visibility="gone"
android:gravity="center_horizontal"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_below="@+id/waitingView"
android:layout_centerHorizontal="true"
android:visibility="gone"/>
<TextView
android:id="@+id/enterCodeTextView"
style="@style/BriarTextBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enter_confirmation_code"
android:layout_gravity="center_horizontal"
android:padding="@dimen/margin_medium"
android:layout_below="@+id/codeView"
android:layout_centerHorizontal="true"/>
<include
android:id="@+id/codeEntryView"
layout="@layout/view_code_entry"
android:layout_below="@+id/enterCodeTextView"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_medium"/>
<Button
android:id="@+id/continueButton"
style="@style/BriarButton.Default"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/continue_button"
android:layout_gravity="center_horizontal"
android:enabled="false"
android:layout_below="@+id/codeEntryView"
android:layout_centerHorizontal="true"
android:layout_margin="@dimen/margin_medium"/>
</RelativeLayout>
</ScrollView>

View File

@@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_activity_vertical"
android:paddingEnd="@dimen/margin_activity_horizontal"
android:paddingRight="@dimen/margin_activity_horizontal"
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingLeft="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<TextView
android:id="@+id/yourCodeView"
style="@style/BriarTextBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/your_invitation_code"
android:layout_marginTop="@dimen/margin_medium"
android:layout_centerHorizontal="true"/>
<TextView
android:id="@+id/codeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:textSize="50sp"
android:textColor="@color/briar_text_secondary"
android:layout_below="@+id/yourCodeView"
android:layout_centerHorizontal="true"
tools:text="1337"/>
<TextView
android:id="@+id/waitingView"
style="@style/BriarTextBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/searching_format"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/margin_medium"
android:layout_below="@+id/codeView"
android:layout_centerHorizontal="true"
android:visibility="gone"
android:gravity="center_horizontal"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:indeterminate="true"
android:layout_below="@+id/waitingView"
android:layout_centerHorizontal="true"
android:visibility="gone"/>
<TextView
android:id="@+id/enterCodeTextView"
style="@style/BriarTextBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enter_invitation_code"
android:layout_gravity="center_horizontal"
android:padding="@dimen/margin_medium"
android:layout_below="@+id/codeView"
android:layout_centerHorizontal="true"/>
<include
android:id="@+id/codeEntryView"
layout="@layout/view_code_entry"
android:layout_below="@+id/enterCodeTextView"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_medium"/>
<Button
android:id="@+id/continueButton"
style="@style/BriarButton.Default"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/continue_button"
android:layout_gravity="center_horizontal"
android:enabled="false"
android:layout_below="@+id/codeEntryView"
android:layout_centerHorizontal="true"
android:layout_margin="@dimen/margin_medium"/>
</RelativeLayout>
</ScrollView>

View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/margin_activity_vertical"
android:paddingEnd="@dimen/margin_activity_horizontal"
android:paddingLeft="@dimen/margin_activity_horizontal"
android:paddingRight="@dimen/margin_activity_horizontal"
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<TextView
style="@style/BriarTextBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/your_nickname"
android:visibility="gone"/>
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/border_spinner"
android:layout_marginTop="@dimen/margin_medium"
android:spinnerMode="dropdown"
android:visibility="gone"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/bluetooth"/>
<TextView
style="@style/BriarTextBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:text="@string/face_to_face"/>
<Button
android:id="@+id/continueButton"
style="@style/BriarButton.Default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/margin_medium"
android:text="@string/continue_button"/>
</LinearLayout>
</ScrollView>

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/margin_activity_vertical"
android:paddingEnd="@dimen/margin_activity_horizontal"
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<TextView
android:id="@+id/errorTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_failed"
android:layout_gravity="center_horizontal"
android:textSize="@dimen/text_size_large"
android:textColor="@color/briar_text_primary"
android:drawableStart="@drawable/alerts_and_states_error"
android:drawableLeft="@drawable/alerts_and_states_error"
android:gravity="center_vertical"
android:padding="@dimen/margin_medium"/>
<TextView
android:id="@+id/explanationTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/interfering"
android:textColor="@color/briar_text_primary"
android:layout_gravity="center_horizontal"
android:padding="@dimen/margin_medium"/>
<Button
android:id="@+id/tryAgainButton"
style="@style/BriarButton.Default"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/try_again_button"
android:layout_gravity="center_horizontal"
android:layout_margin="@dimen/margin_medium"/>
</LinearLayout>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<EditText
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/codeEntryView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="number"
android:layout_gravity="center_horizontal"
android:textSize="@dimen/text_size_xlarge"
android:ems="4"
android:maxLines="1"
android:maxLength="6"
android:layout_margin="@dimen/margin_medium"
android:imeOptions="actionGo"
tools:text="123456"/>

View File

@@ -3,18 +3,18 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_group_member_list"
android:icon="@drawable/ic_group_white"
android:title="@string/groups_member_list"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_group_invite"
android:icon="@drawable/social_share_white"
android:title="@string/groups_invite_members"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_group_member_list"
android:icon="@drawable/ic_group_white"
android:title="@string/groups_member_list"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_group_reveal"
android:icon="@drawable/ic_visibility_white"

View File

@@ -2,9 +2,9 @@
<resources>
<!--Setup-->
<string name="setup_title">Briar einrichten</string>
<string name="setup_explanation">Dein Briar-Konto wird verschlüsselt auf Deinem Gerät gespeichert und nicht mit der \"Cloud\" synchronisiert. Wenn Du Briar deinstallierst oder Dein Passwort vergisst, können das Konto und Deine Daten nicht wiederhergestellt werden.</string>
<string name="choose_nickname">Wähle Deinen Benutzernamen</string>
<string name="choose_password">Wähle Dein Passwort</string>
<string name="setup_explanation">Dein Briar-Konto wird verschlüsselt auf deinem Gerät gespeichert und nicht mit der \"Cloud\" synchronisiert. Wenn du Briar deinstallierst oder dein Passwort vergisst, können das Konto und deine Daten nicht wiederhergestellt werden.</string>
<string name="choose_nickname">Wähle deinen Benutzernamen</string>
<string name="choose_password">Wähle dein Passwort</string>
<string name="confirm_password">Passwort bestätigen</string>
<string name="name_too_long">Name zu lang</string>
<string name="password_too_weak">Passwort zu schwach</string>
@@ -22,7 +22,11 @@
<string name="startup_failed_activity_title">Fehler beim Starten von Briar</string>
<string name="startup_failed_db_error">Deine Briar-Datenbank ist korrupt. Briar-Konto, Daten und alle Verbindungen zu Kontakten können nicht mehr wiederhergestellt werden. Deinstalliere Briar und erstelle nach Installation der aktuellen Briar-Version ein neues Konto.</string>
<string name="startup_failed_service_error">Briar konnte ein benötigtes Plugin nicht starten. Normalerweise kann das Problem durch eine Neuinstallation von Briar gelöst werden. Eine Neuinstallation führt jedoch zum Verlust des Kontos und aller dazugehörigen Daten, da Briar deine Daten nicht auf zentralen Servern speichert</string>
<string name="expiry_warning">Diese Version von Briar ist nicht mehr aktuell.\nBitte installiere eine neuere Version.</string>
<plurals name="expiry_warning">
<item quantity="one">Dies ist eine Beta-Version von Briar. Dein Konto läuft in %d Tag ab und kann nicht verlängert werden.</item>
<item quantity="other">Dies ist eine Beta-Version von Briar. Dein Konto läuft in %d Tagen ab und kann nicht verlängert werden.</item>
</plurals>
<string name="expiry_date_reached">Diese Software ist abgelaufen.\nDanke dass Du Briar getestet hast!</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Navigationsleiste öffnen</string>
<string name="nav_drawer_close_description">Navigationsleiste schliessen</string>
@@ -87,7 +91,7 @@
<!--Adding Contacts-->
<string name="add_contact_title">Kontakt hinzufügen</string>
<string name="your_nickname">Wähle die zu verwendende Identität:</string>
<string name="face_to_face">Um einen neuen Kontakt hinzufügen zu können, ist eine reale Begegnung erforderlich.\n\nDadurch wird eine betrügerische Impersonation und der unautorisierte Zugriff auf die Kommunikation verhindert.</string>
<string name="face_to_face">Um einen neuen Kontakt hinzuzufügen, ist es notwendig, dass sich beide Kontakte an einem Ort treffen.\n\nDadurch wird betrügerische Identitätsvortäuschung und unautorisierter Kommunikationszugriff verhindert.</string>
<string name="continue_button">Weiter</string>
<string name="your_invitation_code">Dein Einladungs-Code ist</string>
<string name="enter_invitation_code">Bitte gib den Einladungs-Code Deines Kontakts an:</string>
@@ -117,6 +121,7 @@
<string name="introduction_onboarding_text">Du kannst Deine Kontakte untereinander bekannt machen. So können sie sich über Briar verbinden, ohne sich persönlich treffen zu müssen.</string>
<string name="introduction_activity_title">Kontakt auswählen</string>
<string name="introduction_message_title">Kontakte untereinander bekannt machen</string>
<string name="introduction_message_hint">Nachricht hinzufügen (optional)</string>
<string name="introduction_button">Kontaktempfehlung abgeben</string>
<string name="introduction_sent">Deine Kontaktempfehlung wurde verschickt</string>
<string name="introduction_error">Es gab einen Fehler beim Versuch, die Kontaktempfehlung zu verschicken</string>
@@ -147,6 +152,7 @@
<string name="groups_create_group_title">Private Gruppe erstellen</string>
<string name="groups_create_group_button">Gruppe erstellen</string>
<string name="groups_create_group_invitation_button">Einladung schicken</string>
<string name="groups_create_group_hint">Wähle einen Namen für deine private Gruppe</string>
<string name="groups_invitation_sent">Gruppeneinladung versendet</string>
<string name="groups_message_sent">Nachricht gesendet</string>
<string name="groups_member_list">Mitglieder</string>
@@ -188,6 +194,8 @@
<string name="groups_reveal_invisible">Beziehung zum Kontakt ist für diese Gruppe nicht sichtbar</string>
<!--Forums-->
<string name="no_forums">Du hast noch keine Foren.\n\nWarum erstellst du nicht einfach selbst ein neues Forum, indem du auf das \"+\"-Symbol am oberen Bildschirmrand tippst?\n\nDu kannst auch deine Kontakte auffordern, Foren mit dir zu teilen.</string>
<string name="create_forum_title">Forum erstellen</string>
<string name="choose_forum_hint">Wähle einen Namen für Dein Forum</string>
<string name="create_forum_button">Forum erstellen</string>
<string name="forum_created_toast">Forum wurde erstellt</string>
<string name="no_forum_posts">Dieses Forum ist leer.\n\nBenutze das Stift-Icon am oberen Bildschirmrand um den ersten Beitrag zu verfassen.\n\nFühlst du dich einsam hier? Dann teile das Forum mit weiteren Kontakten!</string>
@@ -211,6 +219,7 @@
<string name="activity_share_toolbar_header">Kontakte auswählen</string>
<string name="no_contacts_selector">Du scheinst hier neu zu sein und noch keine Kontakte zu haben.\n\nBitte komm zurück, wenn du deinen ersten Kontakt hinzugefügt hast.</string>
<string name="forum_shared_snackbar">Forum mit gewählten Kontakten geteilt</string>
<string name="forum_share_message">Nachricht hinzufügen (optional)</string>
<string name="forum_share_error">Es gab einen Fehler beim Versuch, dieses Forum zu teilen.</string>
<string name="forum_invitation_received">%1$s hat das Forum \"%2$s\" mit dir geteilt.</string>
<string name="forum_invitation_sent">Du hast das Forum \"%1$s\" mit %2$s geteilt.</string>
@@ -241,12 +250,12 @@
<string name="blogs_blog_post_created">Blogbeitrag erstellt</string>
<string name="blogs_blog_post_received">Neuen Blogbeitrag empfangen</string>
<string name="blogs_blog_post_scroll_to">Scrolle zu</string>
<string name="blogs_feed_empty_state">Dies ist die globale Blog-Zeitleiste.\n\nOffensichtlich hat noch niemand etwas veröffentlicht.\n\nSei die oder der erste und tipp auf das Stift-Icon, um einen neuen Blogbeitrag zu verfassen.</string>
<string name="blogs_personal_blog">%ss persönliches Blog</string>
<string name="blogs_feed_empty_state">Dies ist die globale Blog-Zeitleiste.\n\nOffensichtlich hat noch niemand etwas veröffentlicht.\n\nSei die oder der Erste und tippe auf das Stift-Icon, um einen neuen Blogbeitrag zu verfassen.</string>
<string name="blogs_remove_blog">Blog entfernen</string>
<string name="blogs_remove_blog_dialog_message">Bist Du sicher, dass Du diesen Blog und alle dazugehörigen Beiträge löschen möchtest?\nBeachte, dass dies nicht den Blog auf Geräten anderer Leute löscht.</string>
<string name="blogs_remove_blog_ok">Blog entfernen</string>
<string name="blogs_blog_removed">Blog wurde entfernt</string>
<string name="blogs_reblog_comment_hint">Kommentar hinzufügen (optional)</string>
<string name="blogs_reblog_button">Rebloggen</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Blog teilen</string>
@@ -257,8 +266,8 @@
<string name="blogs_sharing_response_declined_sent">Du hast die Blogeinladung von %s abgelehnt.</string>
<string name="blogs_sharing_response_accepted_received">%s hat die Blogeinladung akzeptiert.</string>
<string name="blogs_sharing_response_declined_received">%s hat die Blogeinladung abgelehnt.</string>
<string name="blogs_sharing_invitation_received">%1$s hat das persönliche Blog von %2$s mit dir geteilt.</string>
<string name="blogs_sharing_invitation_sent">Du hast das persönliche Blog von %1$s mit %2$s geteilt.</string>
<string name="blogs_sharing_invitation_received">%1$shat den Blog \"%2$s\" mit Dir geteilt.</string>
<string name="blogs_sharing_invitation_sent">Du teilst den Blog \"%1$s\" mit %2$s.</string>
<string name="blogs_sharing_invitations_title">Blogeinladungen</string>
<string name="blogs_sharing_joined_toast">Blog abonniert</string>
<string name="blogs_sharing_declined_toast">Blogeinladung abgelehnt</string>
@@ -276,7 +285,7 @@
<string name="blogs_rss_remove_feed_dialog_message">Soll der Feed mit allen Posts wirklich gelöscht werden?\nGeteilte Posts werden dabei nicht von anderen Geräten gelöscht.</string>
<string name="blogs_rss_remove_feed_ok">Feed entfernen</string>
<string name="blogs_rss_feeds_manage_delete_error">Der Feed konnte nicht gelöscht werden!</string>
<string name="blogs_rss_feeds_manage_empty_state">Du hast bisher noch keine RSS Feeds importiert. Tippe auf das \"+\"-Icon am oberen Bildschirmrand um einen neuen Feed hinzuzufügen.</string>
<string name="blogs_rss_feeds_manage_empty_state">Du hast bisher noch keine RSS-Feeds importiert. Tippe auf das \"+\"-Icon am oberen Bildschirmrand, um einen neuen Feed hinzuzufügen.</string>
<string name="blogs_rss_feeds_manage_error">Es gab ein Problem beim Laden deiner Feeds. Bitte versuche es später erneut.</string>
<!--Settings Network-->
<string name="network_settings_title">Netzwerke</string>
@@ -311,7 +320,17 @@
<string name="uninstall_setting_summary">Diese Aktion benötigt manuelle Bestätigung im Falle eines Panik-Ereignisses</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Benachrichtigungen</string>
<string name="notify_private_messages_setting_title">Private Nachrichten</string>
<string name="notify_private_messages_setting_summary">Zeige Benachrichtigungen für private Nachrichten</string>
<string name="notify_group_messages_setting_title">Gruppennachrichten</string>
<string name="notify_group_messages_setting_summary">Benachrichtigungen für Gruppennachrichten anzeigen</string>
<string name="notify_forum_posts_setting_title">Forenbeiträge</string>
<string name="notify_forum_posts_setting_summary">Benachrichtigungen für Forenbeiträge anzeigen</string>
<string name="notify_blog_posts_setting_title">Blogbeiträge</string>
<string name="notify_blog_posts_setting_summary">Benachrichtigungen für Blogbeiträge anzeigen</string>
<string name="notify_vibration_setting">Vibration</string>
<string name="notify_lock_screen_setting_title">Sperrbildschirm</string>
<string name="notify_lock_screen_setting_summary">Zeigt Benachrichtigungen auf dem Sperrbildschirm an</string>
<string name="notify_sound_setting">Tonsignal</string>
<string name="notify_sound_setting_default">Standardklingelton</string>
<string name="notify_sound_setting_disabled">Keine</string>
@@ -327,7 +346,7 @@
<!--Crash Reporter-->
<string name="crash_report_title">Briar-Absturzbericht</string>
<string name="briar_crashed">Es tut uns leid, Briar ist abgestürzt.</string>
<string name="not_your_fault">Das ist nicht Deine Schuld.</string>
<string name="not_your_fault">Das ist nicht deine Schuld.</string>
<string name="please_send_report">Bitte hilf uns, Briar zu verbessern, indem Du einen Absturzbericht sendest.</string>
<string name="report_is_encrypted">Wir versprechen, dass der Bericht verschlüsselt ist und über eine sichere Verbindung geschickt wird.</string>
<string name="feedback_title">Feedback</string>
@@ -343,4 +362,6 @@
<!--Sign Out-->
<string name="progress_title_logout">Von Briar abmelden ...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Bildschirmüberlagerung erkannt</string>
<string name="screen_filter_body">Eine andere App überlagert Briar. Um deine Sicherheit zu gewährleisten, reagiert Briar in diesem Fall nicht auf deine Eingaben.\n\nBeende deswegen die folgenden Apps während der Verwendung von Briar:\n\n%1$s</string>
</resources>

View File

@@ -22,7 +22,11 @@
<string name="startup_failed_activity_title">Fallo al iniciar Briar</string>
<string name="startup_failed_db_error">Por alguna razón, la base de datos de Briar ha sufrido daños irreparables. Tu cuenta, tus datos y todos tus contactos se han perdido. Desafortunadamente, tendrás que reinstalar Briar y registrar una nueva cuenta.</string>
<string name="startup_failed_service_error">Briar no pudo iniciar un complemento necesario. Reinstalar Briar suele solucionar el problema. Sin embargo, ten en cuenta que perderás tu cuenta y todos los datos asociados ya que Briar no almacena esta información en ningún servidor central.</string>
<string name="expiry_warning">Esta versión ha caducado.\nInstala una más reciente, por favor.</string>
<plurals name="expiry_warning">
<item quantity="one">Esta es una versión preliminar de Briar. Tu cuenta caducará en %d día y no podrá renovarse.</item>
<item quantity="other">Esta es una versión preliminar de Briar. Tu cuenta caducará en %d días y no podrá renovarse.</item>
</plurals>
<string name="expiry_date_reached">Esta versión ha caducado.\n¡Gracias por probarla!</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Abrir el panel de navegación</string>
<string name="nav_drawer_close_description">Cierra el panel de navegación</string>
@@ -76,7 +80,7 @@
<string name="text_too_long">El texto es demasiado largo</string>
<string name="show_onboarding">Mostrar diálogo de ayuda</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Parece que eres nuevo por aquí y no tienes aún contactos.\n\nPulsa el signo + en la parte superior y sigue las instrucciones para añadir amigos a tu lista.\n\nPor favor, recuerda: sólo puedes añadir nuevos contactos cara a cara para evitar que nadie suplante tu identidad o lea tus mensajes en el futuro. </string>
<string name="no_contacts">Parece que eres nuevo por aquí y no tienes aún contactos.\n\nPulsa el signo + en la parte superior y sigue las instrucciones para añadir amigos a tu lista.\n\nPor favor, recuerda: sólo puedes añadir nuevos contactos cara a cara para evitar que nadie suplante tu identidad o lea tus mensajes en el futuro. </string>
<string name="date_no_private_messages">Sin mensajes.</string>
<string name="no_private_messages">Esta es la vista de conversación.\n\nParece que aún no hay ninguna.\n\nPulsa el campo de texto en la parte inferior para empezar la conversación.</string>
<string name="message_hint">Escribe un mensaje</string>
@@ -118,7 +122,7 @@
<string name="introduction_activity_title">Seleccionar contacto</string>
<string name="introduction_message_title">Presentar a dos contactos</string>
<string name="introduction_message_hint">Añade un mensaje (opcional)</string>
<string name="introduction_button">Realizar la presentación</string>
<string name="introduction_button">Presentar contactos</string>
<string name="introduction_sent">Tu presentación se ha mandado.</string>
<string name="introduction_error">Ocurrió un error realizando la presentación.</string>
<string name="introduction_response_error">Error al responder a la presentación</string>
@@ -136,7 +140,7 @@
<item quantity="other">%d nuevos contactos añadido</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">Aún no participas en ningún grupo.\n\nCrea un grupo pulsando en el signo + arriba o pide a tus contactos que te inviten a uno de sus grupos.</string>
<string name="groups_list_empty">Aún no participas en ningún grupo.\n\nCrea un grupo pulsando en el signo + arriba o pide a tus contactos que te inviten a uno de sus grupos.</string>
<string name="groups_created_by">Creado por %s</string>
<plurals name="messages">
<item quantity="one">%d mensaje</item>
@@ -189,7 +193,9 @@
<string name="groups_reveal_visible_revealed_by_contact">Las relaciones entre los contactos son visibles al grupo (las reveló %s)</string>
<string name="groups_reveal_invisible">Las relaciones entre los contactos no son visibles al grupo</string>
<!--Forums-->
<string name="no_forums">No tienes ningún foro aún.\n\n¿Por qué no creas uno pulsando el signo + de la parte superior?\n\nTambién puedes pedirle a tus contactos que compartan foros contigo.</string>
<string name="no_forums">No tienes ningún foro aún.\n\n¿Por qué no creas uno pulsando el signo + de la parte superior?\n\nTambién puedes pedirle a tus contactos que compartan foros contigo.</string>
<string name="create_forum_title">Crear foro</string>
<string name="choose_forum_hint">Elige un nombre para el foro</string>
<string name="create_forum_button">Crear foro</string>
<string name="forum_created_toast">Foro creado</string>
<string name="no_forum_posts">Este foro está vacío.\n\nUsa el signo del lápiz de la parte superior para redactar una primera publicación.\n\n¿No te sientes un poco solo aquí? ¡Comparte este foro con más contactos!</string>
@@ -245,7 +251,6 @@
<string name="blogs_blog_post_received">Recibido nuevo artículo del blog</string>
<string name="blogs_blog_post_scroll_to">Desplazarse hasta</string>
<string name="blogs_feed_empty_state">Esta es la lista global de entradas de blogs.\n\nParece que nadie ha publicado nada todavía.\n\nSé el primero: pulsa el signo del lápiz para escribir una nueva entrada de blog.</string>
<string name="blogs_personal_blog">Blog personal de %s</string>
<string name="blogs_remove_blog">Eliminar blog</string>
<string name="blogs_remove_blog_dialog_message">¿Seguro que quieres eliminar este blog y todos sus artículos?\nTen en cuenta que no se eliminará el blog de los dispositivos de otras personas.</string>
<string name="blogs_remove_blog_ok">Eliminar blog</string>
@@ -261,8 +266,8 @@
<string name="blogs_sharing_response_declined_sent">Rechazaste la invitación al blog de %s.</string>
<string name="blogs_sharing_response_accepted_received">%s aceptó la invitación al blog.</string>
<string name="blogs_sharing_response_declined_received">%s rechazó la invitación al blog.</string>
<string name="blogs_sharing_invitation_received">%1$s ha compartido el blog personal de %2$s contigo.</string>
<string name="blogs_sharing_invitation_sent">Has compartido el blog personal de %1$s con %2$s.</string>
<string name="blogs_sharing_invitation_received">%1$s ha compartido el blog \"%2$s\" contigo.</string>
<string name="blogs_sharing_invitation_sent">Has compartido el blog \"%1$s\" con %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitaciones a blogs</string>
<string name="blogs_sharing_joined_toast">Suscrito al blog</string>
<string name="blogs_sharing_declined_toast">Rechazada invitación al blog</string>
@@ -284,7 +289,7 @@
<string name="blogs_rss_feeds_manage_error">Hubo un problema cargando tus canales RSS. Por favor, prueba más tarde.</string>
<!--Settings Network-->
<string name="network_settings_title">Redes</string>
<string name="bluetooth_setting">Connectar mediante Bluetooth</string>
<string name="bluetooth_setting">Conectar mediante Bluetooth</string>
<string name="bluetooth_setting_enabled">Cuando haya contactos cerca</string>
<string name="bluetooth_setting_disabled">Solo al añadir contactos</string>
<string name="tor_network_setting">Conectar a través de Tor</string>
@@ -324,6 +329,8 @@
<string name="notify_blog_posts_setting_title">Entradas de blog</string>
<string name="notify_blog_posts_setting_summary">Notificar entradas de blog</string>
<string name="notify_vibration_setting">Vibrar</string>
<string name="notify_lock_screen_setting_title">Pantalla de bloqueo</string>
<string name="notify_lock_screen_setting_summary">Notificaciones en la pantalla de bloqueo</string>
<string name="notify_sound_setting">Sonar</string>
<string name="notify_sound_setting_default">Tono de notificación predeterminado</string>
<string name="notify_sound_setting_disabled">Ninguna</string>

View File

@@ -2,66 +2,70 @@
<resources>
<!--Setup-->
<string name="setup_title">Configuration de Briar</string>
<string name="setup_explanation">Votre compte Briar est enregistré chiffré sur votre appareil et non sur le \"cloud\". Si vous désinstallez Briar ou si vous oubliez votre mot de passe, votre compte et vos données sont irrécupérables.</string>
<string name="choose_nickname">Choisissez votre surnom</string>
<string name="choose_password">Choisissez votre mot de passe</string>
<string name="confirm_password">Confirmez votre mot de passe</string>
<string name="setup_explanation">Votre compte Briar est enregistré chiffré sur votre appareil et non sur le nuage. Si vous désinstallez Briar ou oubliez votre mot de passe, votre compte et vos données sont irrécupérables.</string>
<string name="choose_nickname">Choisir votre pseudonyme</string>
<string name="choose_password">Choisir votre mot de passe</string>
<string name="confirm_password">Confirmer votre mot de passe</string>
<string name="name_too_long">Le nom est trop long</string>
<string name="password_too_weak">Le mot de passe est trop faible</string>
<string name="passwords_do_not_match">Les mots de passes ne correspondent pas</string>
<string name="passwords_do_not_match">Les mots de passe ne correspondent pas</string>
<string name="create_account_button">Créer un compte</string>
<!--Login-->
<string name="enter_password">Tapez votre mot de passe :</string>
<string name="try_again">Mot de passe incorrecte, essayer à nouveau</string>
<string name="sign_in_button">Se connecter</string>
<string name="enter_password">Saisir votre mot de passe :</string>
<string name="try_again">Le mot de passe est erroné, ressayez</string>
<string name="sign_in_button">Connexion</string>
<string name="forgotten_password">J\'ai oublié mon mot de passe</string>
<string name="dialog_title_lost_password">Mot de passe oublié</string>
<string name="dialog_message_lost_password">Votre compte Briar est enregistré chiffré sur votre appareil et non sur le \"cloud\", par conséquent, votre mot de passe ne peut être réinitialisé. Voulez-vous supprimer votre compte et démarrer à nouveau ?\n\nAttention : vos identités, contacts et messages seront définitivement perdus.</string>
<string name="startup_failed_notification_title">Briar n\'a pas pu démarrer</string>
<string name="startup_failed_notification_text">Vous devrez peut-être réinstaller Briar.</string>
<string name="dialog_message_lost_password">Votre compte Briar est enregistré chiffré sur votre appareil et non sur le nuage et nous ne pouvons donc pas réinitialiser votre mot de passe. Voulez-vous supprimer votre compte et recommencer?\n\nAttention : vos identités, contacts et messages seront perdus irrémédiablement.</string>
<string name="startup_failed_notification_title">Impossible de démarrer Briar</string>
<string name="startup_failed_notification_text">Il vous faudra peut-être réinstaller Briar.</string>
<string name="startup_failed_activity_title">Échec de démarrage de Briar</string>
<string name="startup_failed_db_error">Pour une raison indéterminée, votre base de donnée Briar est corrompue et irrécupérable. Vos comptes, données et contacts sont perdus. Vous devez malheureusement réinstaller Briar et configurer un nouveau compte.</string>
<string name="startup_failed_service_error">Briar n\'a pas pu démarrer un module nécessaire. Réinstaller Briar résout généralement ce problème. Veuillez noter que vous perdrez votre compte et toutes les données associées puisque Briar n\'utilise pas de serveurs centralisés pour enregistrer les données.</string>
<string name="expiry_warning">Ce logiciel est arrivé à expiration.\nVeuillez installer une version plus récente.</string>
<string name="startup_failed_db_error">Pour une raison indéterminée, votre base de données Briar est corrompue sans espoir de récupération. Vos comptes, données et contacts sont perdus. Vous devez malheureusement réinstaller Briar et configurer un nouveau compte.</string>
<string name="startup_failed_service_error">Briar n\'a pas pu démarrer un greffon exigé. Réinstaller Briar résout généralement ce problème. Veuillez cependant noter que vous perdrez votre compte et toutes données relatives puisque Briar n\'utilise pas de serveurs centralisés sur lesquels enregistrer vos données.</string>
<plurals name="expiry_warning">
<item quantity="one">Ceci est une version bêta de Briar. Votre compte arrivera à expiration dans %d jour et ne peut pas être renouvelé.</item>
<item quantity="other">Ceci est une version bêta de Briar. Votre compte arrivera à expiration dans %d jours et ne peut pas être renouvelé.</item>
</plurals>
<string name="expiry_date_reached">Ce logiciel est arrivé à expiration.\nMerci de l\'avoir testé!</string>
<!--Navigation Drawer-->
<string name="nav_drawer_open_description">Ouvrir le panneau de navigation</string>
<string name="nav_drawer_close_description">Fermer le panneau de navigation</string>
<string name="nav_drawer_open_description">Ouvrir le tiroir de navigation</string>
<string name="nav_drawer_close_description">Fermer le tiroir de navigation</string>
<string name="contact_list_button">Contacts</string>
<string name="groups_button">Groupes privés</string>
<string name="forums_button">Forums</string>
<string name="blogs_button">Blogs</string>
<string name="blogs_button">Blogues</string>
<string name="settings_button">Paramètres</string>
<string name="sign_out_button">Se déconnecter</string>
<string name="sign_out_button">Déconnexion</string>
<!--Transports-->
<string name="transport_tor">Internet</string>
<string name="transport_bt">Bluetooth</string>
<string name="transport_lan">Wifi</string>
<string name="transport_lan">Wi-Fi</string>
<!--Notifications-->
<string name="ongoing_notification_title">Connecté à Briar</string>
<string name="ongoing_notification_text">Toucher pour ouvrir Briar.</string>
<plurals name="private_message_notification_text">
<item quantity="one">Nouveau message privé.</item>
<item quantity="other">%d Nouveaux messages privés.</item>
<item quantity="one">Un nouveau message privé.</item>
<item quantity="other">%d nouveaux messages privés.</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="one">Nouveau message de groupe.</item>
<item quantity="one">Un nouveau message de groupe.</item>
<item quantity="other">%d nouveaux messages de groupe.</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="one">Nouveau post de forum.</item>
<item quantity="other">%d nouveaux messages de forum.</item>
<item quantity="one">Un nouvel article de forum.</item>
<item quantity="other">%d nouveaux articles de forum.</item>
</plurals>
<plurals name="blog_post_notification_text">
<item quantity="one">Nouveau post de blog.</item>
<item quantity="other">%d Nouveaux messages de blog.</item>
<item quantity="one">Un nouveau billet de blogue.</item>
<item quantity="other">%d nouveaux billets de blogue.</item>
</plurals>
<!--Misc-->
<string name="now">Maintenant</string>
<string name="now">maintenant</string>
<string name="show">Afficher</string>
<string name="hide">Cacher</string>
<string name="ok">OK</string>
<string name="cancel">Annuler</string>
<string name="got_it">Je l\'ai</string>
<string name="got_it">Compris</string>
<string name="delete">Supprimer</string>
<string name="accept">Accepter</string>
<string name="decline">Refuser</string>
@@ -74,161 +78,163 @@
<string name="no_data">Aucune donnée</string>
<string name="ellipsis">...</string>
<string name="text_too_long">Le texte saisi est trop long</string>
<string name="show_onboarding">Afficher la boite de dialogue d\'Aide</string>
<string name="show_onboarding">Afficher la fenêtre d\'aide</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Vous semblez être nouveau ici et n\'avez aucun contact.\n\nTouchez l\'icône + en haut et suivez les instructions pour en ajouter.\n\nRappel : vous pouvez ajouter des contacts uniquement en les rencontrant face à face, ceci pour éviter le vol d\'identité et que vos messages ne soient lus par d\'autres.</string>
<string name="no_contacts">Il semble que soyez nouveau ici, sans encore aucun contact.\n\nTouchez l\'icône + en haut et suivez les instructions pour ajouter des amis à votre liste.\n\nVeuillez ne pas oublier que vous pouvez seulement ajouter des contacts en les rencontrant directement, afin d\'éviter que quelqu\'un se fasse passer pour vous et puisse lire vos messages à l\'avenir.</string>
<string name="date_no_private_messages">Aucun message.</string>
<string name="no_private_messages">Ceci est la vue de conversation.\n\nIl semble y avoir un manque de conversation.\n\nAppuyez sur le champ de saisie en bas pour en démarrer une. </string>
<string name="message_hint">Taper le message</string>
<string name="no_private_messages">Ceci est la vue des conversations.\n\nIl semble ne pas y en avoir.\n\nIl suffit de toucher le champ de saisie, en bas, pour en démarrer une.</string>
<string name="message_hint">Rédiger le message</string>
<string name="delete_contact">Supprimer le contact</string>
<string name="dialog_title_delete_contact">Confirmer la suppression du contact</string>
<string name="dialog_message_delete_contact">Êtes-vous sûr de vouloir supprimer ce contact et tous les messages échangés avec celui-ci ?</string>
<string name="contact_deleted_toast">Contact supprimé</string>
<string name="dialog_message_delete_contact">Voulez-vous vraiment supprimer ce contact et tous les messages associés?</string>
<string name="contact_deleted_toast">Le contact a été supprimé</string>
<!--Adding Contacts-->
<string name="add_contact_title">Ajouter un contact</string>
<string name="your_nickname">Choisissez l\'identité que vous souhaitez utiliser :</string>
<string name="face_to_face">Vous devez rencontrer la personne que vous voulez ajouter comme contact.\n\nCeci pour éviter le vol d\'identité et que vos messages ne soient lus par d\'autres.</string>
<string name="your_nickname">Choisir l\'identité que vous souhaitez utiliser :</string>
<string name="face_to_face">Vous devez rencontrer la personne que vous voulez ajouter comme contact, afin d\'éviter que quelqu\'un se fasse passer pour vous et puisse lire vos messages à l\'avenir.</string>
<string name="continue_button">Continuer</string>
<string name="your_invitation_code">Votre code d\'invitation est </string>
<string name="your_invitation_code">Votre code d\'invitation est</string>
<string name="enter_invitation_code">Veuillez saisir le code d\'invitation de votre contact :</string>
<string name="searching_format">Recherche de contacts avec le code d\'invitation %06d\u2026</string>
<string name="connection_failed">Échec de connexion</string>
<string name="could_not_find_contact">Briar n\'a pas trouvé de contacts à proximité</string>
<string name="try_again_button">Essayer à nouveau</string>
<string name="could_not_find_contact">Briar n\'a pas trouvé votre contact à proximité</string>
<string name="try_again_button">Ressayer</string>
<string name="connected_to_contact">Connecté au contact</string>
<string name="calculating_confirmation_code">Calcul du code de confirmation\u2026</string>
<string name="your_confirmation_code">Votre code de confirmation est </string>
<string name="your_confirmation_code">Votre code de confirmation est</string>
<string name="enter_confirmation_code">Veuillez saisir le code de confirmation de votre contact :</string>
<string name="waiting_for_contact">Attente du contact\u2026</string>
<string name="waiting_for_contact_to_scan">En attente de scan et de la connexion du contact\u2026</string>
<string name="exchanging_contact_details">Échange des détails du contact\u2026</string>
<string name="waiting_for_contact">En attente du contact\u2026</string>
<string name="waiting_for_contact_to_scan">En attente de recherche et de connexion du contact\u2026</string>
<string name="exchanging_contact_details">Échange des renseignements de contact\u2026</string>
<string name="codes_do_not_match">Les codes ne correspondent pas</string>
<string name="interfering">Il se pourrait que quelqu\'un essaye d\'interférer avec votre connexion</string>
<string name="interfering">Cela pourrait signer que quelqu\'un tente d\'interférer avec votre connexion</string>
<string name="contact_added_toast">Contact ajouté : %s</string>
<string name="contact_already_exists">Le contact %s existe déjà</string>
<string name="contact_exchange_failed">L\'échange de contact a échoué</string>
<string name="qr_code_invalid">Le code QR n\'est pas valide</string>
<string name="contact_exchange_failed">Échec d\'échange de contacts</string>
<string name="qr_code_invalid">Le code QR est invalide</string>
<string name="connecting_to_device">Connexion à l\'appareil\u2026</string>
<string name="authenticating_with_device">Autentification avec l\'appareil\u2026</string>
<string name="connection_aborted_local">Nous avons interrompu la connexion ! Il est possible que quelqu\'un tente d\'interférer avec celle-ci</string>
<string name="connection_aborted_remote">Votre contact a interrompu la connexion ! Il est possible que quelqu\'un tente d\'interférer avec celle-ci</string>
<string name="connection_aborted_local">Nous avons interrompu la connexion! Cela pourrait signer que quelqu\'un tente d\'interférer avec votre connexion</string>
<string name="connection_aborted_remote">Votre contact a interrompu la connexion! Cela pourrait signer que quelqu\'un tente d\'interférer avec votre connexion</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Introduire vos contacts</string>
<string name="introduction_onboarding_text">Vous pouvez introduire vos contacts les uns les autres, ils n\'ont donc pas besoin de se rencontrer pour se connecter avec Briar.</string>
<string name="introduction_activity_title">Sélectionner contact </string>
<string name="introduction_message_title">Introduire des contacts</string>
<string name="introduction_message_hint">Ajouter un message (optionnel)</string>
<string name="introduction_button">Effectuer l\'introduction</string>
<string name="introduction_sent">Votre admission a été envoyée.</string>
<string name="introduction_error">Une erreur s\'est produite lors de l\'admission.</string>
<string name="introduction_response_error">Erreur lors de la réponse à l\'admission</string>
<string name="introduction_request_sent">Vous avez demandé à %2$s d\'admettre %1$s.</string>
<string name="introduction_request_received">%1$s vous a demandé l\'admission de %2$s. Souhaitez-vous ajouter %2$s à votre liste de contacts ?</string>
<string name="introduction_request_exists_received">%1$s vous a demandé l\'admission de %2$s mais %2$s est déjà dans votre liste de contacts. Puisque %1$s ne le sait pas, vous pouvez tout de même répondre : </string>
<string name="introduction_request_answered_received">%1$s a demandé l\'admission de %2$s.</string>
<string name="introduction_response_accepted_sent">Vous avez accepté l\'admission de %1$s.</string>
<string name="introduction_response_declined_sent">Vous avez refusé l\'admission de %1$s.</string>
<string name="introduction_response_accepted_received">%1$s a accepté l\'admission de %2$s.</string>
<string name="introduction_response_declined_received">%1$s a refusé l\'admission de %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s annonce que %2$s a refusé l\'admission.</string>
<string name="introduction_onboarding_title">Présenter vos contacts</string>
<string name="introduction_onboarding_text">Vous pouvez présenter vos contacts mutuellement, afin qu\'ils n\'aient pas à se rencontrer en personne pour se connecter les uns aux autres avec Briar.</string>
<string name="introduction_activity_title">Sélectionner un contact </string>
<string name="introduction_message_title">Présenter des contacts</string>
<string name="introduction_message_hint">Ajouter un message (facultatif)</string>
<string name="introduction_button">Faire les présentations</string>
<string name="introduction_sent">Votre présentation a été envoyée.</string>
<string name="introduction_error">Une erreur est survenue lors de la présentation.</string>
<string name="introduction_response_error">Erreur de réponse à la présentation</string>
<string name="introduction_request_sent">Vous avez demandé de présenter %1$s à %2$s.</string>
<string name="introduction_request_received">%1$s a demandé de vous présenter à %2$s. Souhaitez-vous ajouter %2$s à votre liste de contacts?</string>
<string name="introduction_request_exists_received">%1$s a demandé de vous présenter à %2$s, mais %2$s est déjà dans votre liste de contacts. Puisque %1$s pourrait ne pas le savoir, vous pouvez tout de même répondre :</string>
<string name="introduction_request_answered_received">%1$s a demandé de vous présenter à %2$s.</string>
<string name="introduction_response_accepted_sent">Vous avez accepté d\'être présenté à %1$s.</string>
<string name="introduction_response_declined_sent">Vous avez refusé d\'être présenté à %1$s.</string>
<string name="introduction_response_accepted_received">%1$s a accepté d\'être présenté à %2$s.</string>
<string name="introduction_response_declined_received">%1$s a refusé d\'être présenté à %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s annonce que %2$s a refusé la présentation.</string>
<plurals name="introduction_notification_text">
<item quantity="one">Nouveau contact ajouté.</item>
<item quantity="other">%d nouveaux contacts ajoutés.</item>
<item quantity="one">Un nouveau contact a été ajouté.</item>
<item quantity="other">%d nouveaux contacts ont été ajoutés.</item>
</plurals>
<!--Private Groups-->
<string name="groups_list_empty">Vous n\'êtes membres d\'aucun groupe.\n\nTouchez l\'icône + en haut pour en créer un ou demandez à vos contacts de vous inviter à l\'un de leurs groupes.</string>
<string name="groups_list_empty">Vous ne participez à aucun groupe.\n\nTouchez l\'icône +, en haut, pour en créer un ou demandez à vos contacts de vous inviter dans l\'un des leurs.</string>
<string name="groups_created_by">Créé par %s</string>
<plurals name="messages">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>
</plurals>
<string name="groups_group_is_empty">Ce groupe est vide</string>
<string name="groups_group_is_dissolved">Ce groupe a été supprimé</string>
<string name="groups_group_is_dissolved">Ce groupe a été dissous</string>
<string name="groups_remove">Supprimer</string>
<string name="groups_create_group_title">Créer un groupe privé</string>
<string name="groups_create_group_button">Créer un groupe</string>
<string name="groups_create_group_invitation_button">Envoyer invitation</string>
<string name="groups_create_group_hint">Choisissez un nom pour votre groupe privé</string>
<string name="groups_invitation_sent">Invitation envoyée au groupe</string>
<string name="groups_message_sent">Message envoyé</string>
<string name="groups_member_list">Liste de participants</string>
<string name="groups_create_group_invitation_button">Envoyer une invitation</string>
<string name="groups_create_group_hint">Choisir un nom pour votre groupe privé</string>
<string name="groups_invitation_sent">L\'invitation de groupe a été envoyée</string>
<string name="groups_message_sent">Le message a été envoyé</string>
<string name="groups_member_list">Liste des participants</string>
<string name="groups_invite_members">Inviter des participants</string>
<string name="groups_member_created_you">Vous avez créé le groupe</string>
<string name="groups_member_created">%s a créé le groupe</string>
<string name="groups_member_joined_you">Vous avez rejoint le groupe</string>
<string name="groups_member_joined">%s a rejoint le groupe</string>
<string name="groups_member_joined_you">Vous vous êtes joint au groupe</string>
<string name="groups_member_joined">%s s\'est joint au groupe</string>
<string name="groups_leave">Quitter le groupe</string>
<string name="groups_leave_dialog_title">Confirmer la sortie du groupe</string>
<string name="groups_leave_dialog_message">Êtes-vous sûr de vouloir quitter ce groupe ?</string>
<string name="groups_dissolve">Supprimer le groupe</string>
<string name="groups_dissolve_dialog_title">Confirmer la suppression du groupe</string>
<string name="groups_dissolve_dialog_message">Êtes-vous sûr de vouloir supprimer ce groupe ?\n\nLes autres participants ne pourront plus continuer leur conversation et pourraient ne pas recevoir les derniers messages.</string>
<string name="groups_dissolve_button">Supprimer</string>
<string name="groups_dissolved_dialog_title">Le Groupe A Été Supprimé</string>
<string name="groups_dissolved_dialog_message">L\'initiateur de ce groupe l\'a supprimé.\n\nVous ne pouvez plus y écrire de messages et il se peut que vous n\'ayez pas reçu tout ceux qui y ont été publiés.</string>
<string name="groups_leave_dialog_message">Voulez-vous vraiment quitter ce groupe?</string>
<string name="groups_dissolve">Dissoudre le groupe</string>
<string name="groups_dissolve_dialog_title">Confirmer la dissolution du groupe</string>
<string name="groups_dissolve_dialog_message">Voulez-vous vraiment dissoudre ce groupe?\n\nLes autres participants ne pourront pas poursuivre leur conversation et ne recevront peut-être pas les derniers messages.</string>
<string name="groups_dissolve_button">Dissoudre</string>
<string name="groups_dissolved_dialog_title">Le groupe a été dissous</string>
<string name="groups_dissolved_dialog_message">Le créateur de ce groupe l\'a dissous.\n\nVous ne pouvez plus écrire de messages au groupe et ne recevrez peut-être pas tous ceux qui y ont été publiés.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Invitations du groupe</string>
<string name="groups_invitations_invitation_sent">Vous avez invité %1$s a rejoindre le groupe \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s vous a invité a rejoindre le groupe \"%2$s\".</string>
<string name="groups_invitations_joined">Groupe rejoint</string>
<string name="groups_invitations_declined">Invitation du groupe déclinée</string>
<string name="groups_invitations_title">Invitations de groupe</string>
<string name="groups_invitations_invitation_sent">Vous avez invité %1$s à se joindre au groupe « %2$s ».</string>
<string name="groups_invitations_invitation_received">%1$s vous a invité à vous joindre au groupe « %2$s ».</string>
<string name="groups_invitations_joined">S\'est joint au groupe</string>
<string name="groups_invitations_declined">L\'invitation de groupe a été refusée</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d invitation de groupe en attente</item>
<item quantity="other">%d invitations de groupe en attente</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">Vous avez accepté l\'invitation du groupe de %s.</string>
<string name="groups_invitations_response_declined_sent">Vous avez refusé l\'invitation du groupe de %s.</string>
<string name="groups_invitations_response_accepted_received">%s a accepté l\'invitation du groupe.</string>
<string name="groups_invitations_response_declined_received">%s a décliné l\'invitation du groupe.</string>
<string name="sharing_status_groups">Seul l\'initiateur du groupe peut inviter de nouveaux membres. Voici la liste des membres actuels.</string>
<string name="groups_invitations_response_accepted_sent">Vous avez accepté l\'invitation de groupe de %s.</string>
<string name="groups_invitations_response_declined_sent">Vous avez refusé l\'invitation de groupe de %s.</string>
<string name="groups_invitations_response_accepted_received">%s a accepté l\'invitation de groupe.</string>
<string name="groups_invitations_response_declined_received">%s a refusé l\'invitation de groupe.</string>
<string name="sharing_status_groups">Seul le créateur peut inviter de nouveaux participants. Voici la liste des membres actuels du groupe.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Dévoiler les contacts</string>
<string name="groups_reveal_dialog_message">Vous pouvez choisir de dévoiler les contacts à tous les membres actuels et futurs de ce groupe.\n\nDévoiler les contacts vous assure une connexion au groupe plus rapide et plus fiable car vous pouvez communiquer avec les contacts dévoilés même si l\'initiateur du groupe est hors ligne.</string>
<string name="groups_reveal_dialog_message">Vous pouvez choisir de dévoiler les contacts à tous les participants actuels et futurs de ce groupe.\n\nDévoiler les contacts accélère et fiabilise votre connexion au groupe, car vous pouvez communiquer avec les contacts dévoilés même si le créateur du groupe est hors ligne.</string>
<string name="groups_reveal_visible">Votre lien avec le contact est visible par le groupe</string>
<string name="groups_reveal_visible_revealed_by_us">Votre lien avec le contact est visible par le groupe (dévoilé par vous-même)</string>
<string name="groups_reveal_visible_revealed_by_contact">Votre lien avec le contact est invisible par le groupe (dévoilé par %s)</string>
<string name="groups_reveal_invisible">Votre lien avec le contact est invisible par le groupe</string>
<string name="groups_reveal_visible_revealed_by_us">Votre lien avec le contact est visible par le groupe (dévoilé par vous)</string>
<string name="groups_reveal_visible_revealed_by_contact">Votre lien avec le contact est visible par le groupe (dévoilé par %s)</string>
<string name="groups_reveal_invisible">Votre lien avec le contact n\'est pas visible par le groupe</string>
<!--Forums-->
<string name="no_forums">Vous n\'avez pas de forums.\n\nPourquoi ne pas en créer un en touchant l\'icône + en haut ?\n\nVous pouvez aussi demander à vos contacts d\'en partager avec vous.</string>
<string name="no_forums">Vous n\'avez pas encore de forums.\n\nPourquoi ne pas en créer un en touchant l\'icône + située en haut?\n\nVous pouvez aussi demander à vos contacts d\'en partager avec vous.</string>
<string name="create_forum_title">Créer un forum</string>
<string name="choose_forum_hint">Choisir un nom pour votre forum </string>
<string name="create_forum_button">Créer un forum</string>
<string name="forum_created_toast">Forum créé</string>
<string name="no_forum_posts">Ce forum est vide.\n\nUtilisez l\'icône crayon, en haut pour écrire le premier message.\n\nVous sentez-vous seul, ici? Partager ce forum avec d\'autres contacts !</string>
<string name="no_posts">Aucun message</string>
<string name="forum_created_toast">Le forum a été créé</string>
<string name="no_forum_posts">Ce forum est vide.\n\nUtilisez l\'icône de crayon, en haut, pour rédiger le premier article.\n\nVous sentez-vous seul ici? Partagez ce forum avec vos contacts!</string>
<string name="no_posts">Aucun article</string>
<plurals name="posts">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>
<item quantity="one">%d article</item>
<item quantity="other">%d articles</item>
</plurals>
<string name="forum_new_entry_posted">Message de forum saisi</string>
<string name="forum_new_entry_posted">L\'entrée de forum a été publiée</string>
<string name="forum_new_message_hint">Nouvelle entrée</string>
<string name="forum_message_reply_hint">Nouvelle réponse</string>
<string name="btn_reply">Répondre</string>
<string name="forum_leave">Quitter le forum</string>
<string name="dialog_title_leave_forum">Confirmer la sortie du forum</string>
<string name="dialog_message_leave_forum">Êtes-vous sûr de vouloir quitter ce forum ? Les contacts avec qui vous l\'avez partagé pourraient ne plus recevoir ses mises à jour.</string>
<string name="dialog_message_leave_forum">Voulez-vous vraiment quitter ce forum? Les contacts avec qui vous l\'avez partagé pourraient ne plus en recevoir les mises à jour.</string>
<string name="dialog_button_leave">Quitter</string>
<string name="forum_left_toast">Quitté le forum</string>
<string name="forum_left_toast">A quitté le forum</string>
<!--Forum Sharing-->
<string name="forum_share_button">Partagé le forum</string>
<string name="contacts_selected">Contacts sélectionnés</string>
<string name="activity_share_toolbar_header">Choisissez des contacts</string>
<string name="no_contacts_selector">Vous semblez être nouveau ici et n\'avez pas encore de contacts.\n\nRevenez lorsque vous aurez ajouté votre premier contact.</string>
<string name="forum_shared_snackbar">Forum partagé avec les contacts choisis</string>
<string name="forum_share_message">Ajouter un message (optionnel)</string>
<string name="forum_share_error">Une erreur s\'est produite lors du partage du forum.</string>
<string name="forum_invitation_received">%1$s a partagé le forum \"%2$s\" avec vous.</string>
<string name="forum_invitation_sent">Vous avez partagé le forum \"%1$s\" avec %2$s.</string>
<string name="forum_share_button">Partager le forum</string>
<string name="contacts_selected">Des contacts ont été sélectionnés</string>
<string name="activity_share_toolbar_header">Choisir des contacts</string>
<string name="no_contacts_selector">Il semble que soyez nouveau ici, sans encore aucun contact.\n\nRevenez ici après avoir ajouté votre premier contact.</string>
<string name="forum_shared_snackbar">Le forum a été partagé avec les contacts choisis</string>
<string name="forum_share_message">Ajouter un message (facultatif)</string>
<string name="forum_share_error">Une erreur est survenue lors du partage de ce forum.</string>
<string name="forum_invitation_received">%1$s a partagé le forum « %2$s » avec vous.</string>
<string name="forum_invitation_sent">Vous avez partagé le forum « %1$s » avec %2$s.</string>
<string name="forum_invitations_title">Invitations au forum</string>
<string name="forum_invitation_exists">Vous avez déjà accepté une invitation à ce forum. En acceptant d\'autre invitations, vous augmentez et renforcez la communication de ce forum.</string>
<string name="forum_joined_toast">Forum rejoint</string>
<string name="forum_declined_toast">Invitation au forum refusée</string>
<string name="forum_invitation_exists">Vous avez déjà accepté une invitation à ce forum. En acceptant d\'autres invitations, vous augmenterez et renforcerez la communication de ce forum.</string>
<string name="forum_joined_toast">Vous vous êtes joint au forum</string>
<string name="forum_declined_toast">L\'invitation au forum a été refusée</string>
<string name="shared_by_format">Partagé par %s</string>
<string name="forum_invitation_already_sharing">Déjà en cours de partage</string>
<string name="forum_invitation_response_accepted_sent">Vous avez accepté l\'invitation au forum de %s.</string>
<string name="forum_invitation_response_declined_sent">Vous avez refusé l\'invitation au forum de %s.</string>
<string name="forum_invitation_already_sharing">Le forum est déjà partagé</string>
<string name="forum_invitation_response_accepted_sent">Vous avez accepté l\'invitation de %s au forum.</string>
<string name="forum_invitation_response_declined_sent">Vous avez refusé l\'invitation de %s au forum.</string>
<string name="forum_invitation_response_accepted_received">%s a accepté l\'invitation au forum.</string>
<string name="forum_invitation_response_declined_received">%s a décliné l\'invitation au forum.</string>
<string name="sharing_status">Etat de partage</string>
<string name="sharing_status_forum">Tous les participants d\'un forum peuvent le partager avec leurs contacts. Vous partagez ce forum avec les contacts suivants. Il y a peut-être d\'autres participants que vous ne pouvez voir.</string>
<string name="forum_invitation_response_declined_received">%s a refusé l\'invitation au forum.</string>
<string name="sharing_status">État de partage</string>
<string name="sharing_status_forum">Tous les participants d\'un forum peuvent le partager avec leurs contacts. Vous partagez ce forum avec les contacts suivants. Il peut aussi y avoir d\'autres participants que vous ne pouvez pas voir.</string>
<string name="shared_with">Partagé avec %1$d (%2$d en ligne)</string>
<plurals name="forums_shared">
<item quantity="one">%d forum partagé par des contacts</item>
@@ -236,125 +242,126 @@
</plurals>
<string name="nobody">Personne</string>
<!--Blogs-->
<string name="blogs_other_blog_empty_state">Ce blog est actuellement vide.\n\nSoit l\'auteur n\'a encore rien écrit, soit la personne qui l\'a partagé avec vous doit venir en ligne pour que les messages soient synchronisés.</string>
<string name="read_more">Lire la suite</string>
<string name="blogs_write_blog_post">Écrire un message de blog</string>
<string name="blogs_write_blog_post_body_hint">Saisir ici votre message de blog</string>
<string name="blogs_other_blog_empty_state">Ce blogue est actuellement vide.\n\nSoit l\'auteur n\'a encore rien écrit, soit la personne qui l\'a partagé avec vous doit se connecter pour que les articles soient synchronisés.</string>
<string name="read_more">en lire davantage</string>
<string name="blogs_write_blog_post">Écrire un article de blogue</string>
<string name="blogs_write_blog_post_body_hint">Saisir votre message de blogue ici</string>
<string name="blogs_publish_blog_post">Publier</string>
<string name="blogs_blog_post_created">Message de blog créé</string>
<string name="blogs_blog_post_received">Nouveau message de blog reçu</string>
<string name="blogs_blog_post_scroll_to">Défiler jusqu\'à</string>
<string name="blogs_feed_empty_state">Ceci est le flux global des blogs.\n\nIl semble que personne n\'ait encore rien écrit.\n\nSoyez le premier et touchez l\'icône stylet pour écrire un message de blog.</string>
<string name="blogs_personal_blog">Blog personnel de %s</string>
<string name="blogs_remove_blog">Supprimer le blog</string>
<string name="blogs_remove_blog_dialog_message">Êtes-vous sûr de vouloir supprimer ce blog et tous ses messages ?\nNotez que ceci ne supprimera pas le blog sur les appareils des autres personnes.</string>
<string name="blogs_remove_blog_ok">Supprimer le blog</string>
<string name="blogs_blog_removed">Blog supprimé</string>
<string name="blogs_reblog_comment_hint">Ajouter un commentaire (optionnel)</string>
<string name="blogs_reblog_button">Reblog</string>
<string name="blogs_blog_post_created">L\'article de blogue a été créé</string>
<string name="blogs_blog_post_received">Un nouvel article de blogue a été reçu</string>
<string name="blogs_blog_post_scroll_to">Faire défiler jusqu\'à</string>
<string name="blogs_feed_empty_state">Ceci est le flux global des blogues.\n\nIl semble que personne n\'ait encore rien écrit.\n\nSoyez le premier et touchez l\'icône de crayon pour rédiger un nouvel article de blogue.</string>
<string name="blogs_remove_blog">Supprimer le blogue</string>
<string name="blogs_remove_blog_dialog_message">Voulez-vous vraiment supprimer ce blogue et tous ses messages?\nNotez que cela ne le supprimera pas des appareils d\'autrui.</string>
<string name="blogs_remove_blog_ok">Supprimer le blogue</string>
<string name="blogs_blog_removed">Le blogue a été supprimé</string>
<string name="blogs_reblog_comment_hint">Ajouter un commentaire (facultatif)</string>
<string name="blogs_reblog_button">Rebloguer</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">Partager le blog</string>
<string name="blogs_sharing_error">Une erreur s\'est produite lors du partage du blog.</string>
<string name="blogs_sharing_button">Partager le blog</string>
<string name="blogs_sharing_snackbar">Blog partagé avec les contacts choisis</string>
<string name="blogs_sharing_response_accepted_sent">Vous avez accepté l\'invitation au blog de %s.</string>
<string name="blogs_sharing_response_declined_sent">Vous avez refusé l\'invitation au blog de %s.</string>
<string name="blogs_sharing_response_accepted_received">%s a accepté l\'invitation au blog.</string>
<string name="blogs_sharing_response_declined_received">%s a décliné l\'invitation au blog.</string>
<string name="blogs_sharing_invitation_received">%1$s a partagé le blog personnel de %2$s avec vous.</string>
<string name="blogs_sharing_invitation_sent">Vous avez partagé le blog personnel de %1$s avec %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitations au blog</string>
<string name="blogs_sharing_joined_toast">Abonné au Blog</string>
<string name="blogs_sharing_declined_toast">Invitation au blog refusée</string>
<string name="sharing_status_blog">Quiconque est abonné à un blog peut le partager avec ses contacts. Vous partagez ce blog avec les contacts suivants. Il y a peut-être d\'autres abonnés que vous ne pouvez voir.</string>
<string name="blogs_sharing_share">Partager le blogue</string>
<string name="blogs_sharing_error">Une erreur est survenue lors du partage du blogue.</string>
<string name="blogs_sharing_button">Partager le blogue</string>
<string name="blogs_sharing_snackbar">Le blogue a été partagé avec les contacts choisis</string>
<string name="blogs_sharing_response_accepted_sent">Vous avez accepté l\'invitation de %s au blogue.</string>
<string name="blogs_sharing_response_declined_sent">Vous avez refusé l\'invitation de %s au blogue.</string>
<string name="blogs_sharing_response_accepted_received">%s a accepté l\'invitation au blogue.</string>
<string name="blogs_sharing_response_declined_received">%s a refusé l\'invitation au blogue.</string>
<string name="blogs_sharing_invitation_received">%1$s a partagé le blogue « %2$s » avec vous.</string>
<string name="blogs_sharing_invitation_sent">Vous avez partagé le blogue « %1$s » avec %2$s.</string>
<string name="blogs_sharing_invitations_title">Invitations au blogue</string>
<string name="blogs_sharing_joined_toast">Est abonné au blogue</string>
<string name="blogs_sharing_declined_toast">L\'invitation au blogue a été refusée</string>
<string name="sharing_status_blog">Quiconque est abonné à un blogue peut le partager avec ses contacts. Vous partagez ce blogue avec les contacts suivants. Il peut aussi y avoir d\'autres abonnés que vous ne pouvez pas voir.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Import de flux RSS</string>
<string name="blogs_rss_feeds_import">Importer un flux RSS</string>
<string name="blogs_rss_feeds_import_button">Importer</string>
<string name="blogs_rss_feeds_import_hint">Saisir l\'URL du Flux RSS</string>
<string name="blogs_rss_feeds_import_error">Désolé, une erreur s\'est produite lors de limportation du flux.</string>
<string name="blogs_rss_feeds_manage">Gérer le flux RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importé :</string>
<string name="blogs_rss_feeds_import_hint">Saisir l\'URL du flux RSS</string>
<string name="blogs_rss_feeds_import_error">Nous sommes désolé! Une erreur est survenue lors de limportation de votre flux.</string>
<string name="blogs_rss_feeds_manage">Gérer les flux RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importés :</string>
<string name="blogs_rss_feeds_manage_author">Auteur :</string>
<string name="blogs_rss_feeds_manage_updated">Dernière mise à jour :</string>
<string name="blogs_rss_remove_feed">Supprimer le flux</string>
<string name="blogs_rss_remove_feed_dialog_message">Êtes-vous sûr de vouloir supprimer ce flux et tous ses messages ?\nLes messages que vous avez partagés ne seront pas supprimés des appareils des autres personnes.</string>
<string name="blogs_rss_remove_feed_dialog_message">Voulez-vous vraiment supprimer ce flux et tous ses messages ?\nLes articles que vous avez partagés ne seront pas supprimés des appareils d\'autrui.</string>
<string name="blogs_rss_remove_feed_ok">Supprimer le flux</string>
<string name="blogs_rss_feeds_manage_delete_error">Le flux n\'a pas pu être supprimé !</string>
<string name="blogs_rss_feeds_manage_empty_state">Vous n\'avez importé aucun flux RSS.\n\nPourquoi ne pas cliquer le plus dans le coin en haut à droite pour ajouter votre premier ?</string>
<string name="blogs_rss_feeds_manage_error">Problème lors du chargement de vos flux. essayer ultérieurement.</string>
<string name="blogs_rss_feeds_manage_delete_error">Impossible de supprimer le flux!</string>
<string name="blogs_rss_feeds_manage_empty_state">Vous n\'avez pas encore importé de flux RSS.\n\nPourquoi ne pas cliquer sur l\'icône +, en haut à droite, pour ajouter votre premier flux?</string>
<string name="blogs_rss_feeds_manage_error">Un problème est survenu lors du chargement de vos flux. Veuillez ressayer ultérieurement.</string>
<!--Settings Network-->
<string name="network_settings_title">Réseaux </string>
<string name="bluetooth_setting">Connecter par Bluetooth</string>
<string name="network_settings_title">Réseaux</string>
<string name="bluetooth_setting">Se connecter par Bluetooth</string>
<string name="bluetooth_setting_enabled">Lorsque des contacts sont à proximité</string>
<string name="bluetooth_setting_disabled">Uniquement lors de l\'ajout de contacts</string>
<string name="tor_network_setting">Connecter via Tor</string>
<string name="tor_network_setting">Se connecter avec Tor</string>
<string name="tor_network_setting_never">Jamais</string>
<string name="tor_network_setting_wifi">Seulement par Wi-Fi</string>
<string name="tor_network_setting_always">Par Wi-Fi ou données mobiles</string>
<string name="tor_network_setting_always">En utilisant le Wi-Fi ou le service mobile de données</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Sécurité</string>
<string name="change_password">Modifier le mot de passe</string>
<string name="current_password">Tapez le mot de passe actuel :</string>
<string name="choose_new_password">Choisir le nouveau mot de passe :</string>
<string name="confirm_new_password">Confirmer le nouveau mot de passe :</string>
<string name="change_password">Changer le mot de passe</string>
<string name="current_password">Saisir votre mot de passe actuel :</string>
<string name="choose_new_password">Choisir votre nouveau mot de passe :</string>
<string name="confirm_new_password">Confirmer votre nouveau mot de passe :</string>
<string name="password_changed">Le mot de passe a été changé.</string>
<string name="panic_setting">Paramètres du bouton Panique</string>
<string name="panic_setting_title">Bouton Panique</string>
<string name="panic_setting_hint">Configurer Briar pour l\'utilisation de l\'application du bouton Panique</string>
<string name="panic_app_setting_title">Application bouton Panique</string>
<string name="unknown_app">une application inconnue</string>
<string name="panic_app_setting_summary">Aucune application définie</string>
<string name="panic_setting">Paramètres du bouton d\'urgence</string>
<string name="panic_setting_title">Bouton d\'urgence</string>
<string name="panic_setting_hint">Configurer la réaction de Briar lorsque vous utilisez une application de bouton d\'urgence</string>
<string name="panic_app_setting_title">Application de bouton d\'urgence</string>
<string name="unknown_app">une appli inconnue</string>
<string name="panic_app_setting_summary">Aucune appli n\'a été définie</string>
<string name="panic_app_setting_none">Aucune</string>
<string name="dialog_title_connect_panic_app">Confirmer l\'application Panique</string>
<string name="dialog_message_connect_panic_app">Êtes-vous sûr de vouloir autoriser %1$s à déclencher les actions destructives du bouton Panique ?</string>
<string name="lock_setting_title">Se déconnecter</string>
<string name="lock_setting_summary">Se déconnecter de Briar lorsque le bouton Panique est pressé</string>
<string name="dialog_title_connect_panic_app">Confirmer l\'application d\'urgence</string>
<string name="dialog_message_connect_panic_app">Voulez-vous vraiment autoriser %1$s à déclencher les actions destructrices du bouton d\'urgence?</string>
<string name="lock_setting_title">Déconnexion</string>
<string name="lock_setting_summary">Se déconnecter de Briar lorsqu\'on appuie sur un bouton d\'urgence</string>
<string name="purge_setting_title">Supprimer le compte</string>
<string name="purge_setting_summary">Supprimer votre compte lorsqu\'un bouton Panique est pressé. Attention : ceci supprimera définitivement vos identités, contacts et messages.</string>
<string name="purge_setting_summary">Supprimer votre compte Briar lorsqu\'on appuie sur un bouton d\'urgence. Attention : cela supprimera définitivement vos identités, contacts et messages</string>
<string name="uninstall_setting_title">Désinstaller Briar</string>
<string name="uninstall_setting_summary">Ceci nécessite une confirmation manuelle en cas d\'événement de panique</string>
<string name="uninstall_setting_summary">Une confirmation manuelle est exigée en cas d\'événement d\'urgence</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Notifications</string>
<string name="notify_private_messages_setting_title">Messages privés</string>
<string name="notify_private_messages_setting_summary">Afficher les notifications pour les messages privés</string>
<string name="notify_private_messages_setting_summary">Afficher des alertes pour les messages privés</string>
<string name="notify_group_messages_setting_title">Messages de groupe</string>
<string name="notify_group_messages_setting_summary">Afficher les notifications pour les messages de groupe</string>
<string name="notify_forum_posts_setting_title">Messages de forum</string>
<string name="notify_forum_posts_setting_summary">Afficher les notifications des messages de forum</string>
<string name="notify_blog_posts_setting_title">Messages de blog</string>
<string name="notify_blog_posts_setting_summary">Afficher les notifications pour les messages de blog</string>
<string name="notify_vibration_setting">Vibration</string>
<string name="notify_sound_setting">Sonnerie</string>
<string name="notify_group_messages_setting_summary">Afficher des alertes pour les messages de groupe</string>
<string name="notify_forum_posts_setting_title">Articles de forum</string>
<string name="notify_forum_posts_setting_summary">Afficher des alertes pour les articles de forum</string>
<string name="notify_blog_posts_setting_title">Articles de blogue</string>
<string name="notify_blog_posts_setting_summary">Afficher des alertes pour pour les articles de blogue</string>
<string name="notify_vibration_setting">Vibrer</string>
<string name="notify_lock_screen_setting_title">Écran de verrouillage</string>
<string name="notify_lock_screen_setting_summary">Afficher les notifications sur l\'écran de verrouillage</string>
<string name="notify_sound_setting">Son</string>
<string name="notify_sound_setting_default">Sonnerie par défaut</string>
<string name="notify_sound_setting_disabled">Aucune</string>
<string name="choose_ringtone_title">Choisir la sonnerie</string>
<string name="notify_sound_setting_disabled">Aucun</string>
<string name="choose_ringtone_title">Choisir une sonnerie</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Commentaires</string>
<string name="send_feedback">Envoyer vos commentaires</string>
<string name="feedback_settings_title">Rétroaction</string>
<string name="send_feedback">Envoyer une rétroaction</string>
<!--Link Warning-->
<string name="link_warning_title">Avertissement de lien</string>
<string name="link_warning_intro">Vous êtes sur le point d\'ouvrir le lien suivant via une application externe.</string>
<string name="link_warning_text">Ceci pourrait être utilisé pour vous identifier. Évaluez la confiance que vous avez en la personne qui vous a envoyé ce lien et envisagez de l\'ouvrir avec Orfox.</string>
<string name="link_warning_intro">Vous êtes sur le point d\'ouvrir le lien suivant avec une appli externe.</string>
<string name="link_warning_text">Cela pourrait être utilisé pour vous identifier. Décidez si vous faites confiance à la personne qui vous a envoyé ce lien et envisagez de l\'ouvrir avec Orfox.</string>
<string name="link_warning_open_link">Ouvrir le lien</string>
<!--Crash Reporter-->
<string name="crash_report_title">Rapport de plantage Briar</string>
<string name="crash_report_title">Rapport de plantage de Briar</string>
<string name="briar_crashed">Désolé, Briar a planté.</string>
<string name="not_your_fault">Vous n\'y êtes pour rien.</string>
<string name="please_send_report">Aidez-nous à améliorer Briar en nous envoyant un rapport de plantage.</string>
<string name="please_send_report">Veuillez nous aider à améliorer Briar en nous envoyant un rapport de plantage.</string>
<string name="report_is_encrypted">Nous promettons que le rapport est chiffré et envoyé en toute sécurité. </string>
<string name="feedback_title">Commentaires</string>
<string name="describe_crash">Décrivez ce qui s\'est produit (optionnel)</string>
<string name="enter_feedback">Saisir vos commentaires</string>
<string name="optional_contact_email">Votre adresse émail (optionnel)</string>
<string name="include_debug_report_crash">Ajouter des données anonymes concernant le plantage</string>
<string name="include_debug_report_feedback">Ajouter des données anonymes concernant cet appareil</string>
<string name="feedback_title">Rétroaction</string>
<string name="describe_crash">Décrivez ce qui s\'est produit (facultatif)</string>
<string name="enter_feedback">Saisir votre rétroaction</string>
<string name="optional_contact_email">Votre adresse courriel (facultatif)</string>
<string name="include_debug_report_crash">Inclure des données anonymes concernant le plantage</string>
<string name="include_debug_report_feedback">Inclure des données anonymes concernant cet appareil</string>
<string name="could_not_load_report_data">Impossible de charger les données du rapport.</string>
<string name="send_report">Envoyer le rapport</string>
<string name="close">Fermer</string>
<string name="dev_report_saved">Rapport enregistré. Il sera envoyé lors de votre prochaine connexion à Briar.</string>
<string name="dev_report_saved">Le rapport a été enregistré. Il sera envoyé lors de votre prochaine connexion à Briar.</string>
<!--Sign Out-->
<string name="progress_title_logout">Déconnexion de Briar ...</string>
<string name="progress_title_logout">Déconnexion de Briar</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Superposition d\'écran détectée</string>
<string name="screen_filter_body">Une autre app s\'affiche par dessus Briar. Pour protéger votre sécurité, Briar ne répond pas au toucher lorsqu\'une autre app s\'affiche par dessus.\n\nEssayer de fermer les applications suivantes lorsque vous utilisez Briar :\n\n%1$s</string>
<string name="screen_filter_title">Une superposition d\'écran a été détectée</string>
<string name="screen_filter_body">Une autre appli s\'affiche par dessus Briar. Pour protéger votre sécurité, Briar ne répondra pas au toucher si autre appli s\'affiche par dessus.\n\nEssayez de fermer les applis suivantes lorsque vous utilisez Briar :\n\n%1$s</string>
</resources>

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