Compare commits

..

45 Commits

Author SHA1 Message Date
Daniel Lublin
d3692b2a97 WIP add removabledrive test ui 2021-06-09 13:44:20 +02:00
Torsten Grote
bcbc96dc2d Merge branch '2037-create-removabledriveviewmodel' into '1802-sync-via-removable-storage'
Add RemovableDriveViewModel

See merge request briar/briar!1475
2021-06-09 11:32:10 +00:00
Daniel Lublin
1ddcd6cfff Make pkg private 2021-06-08 20:31:23 +02:00
Daniel Lublin
fd810f5c16 Move to new removabledrive package 2021-06-08 12:25:09 +02:00
Daniel Lublin
3f5e131250 Use US locale for now 2021-06-08 12:18:33 +02:00
Daniel Lublin
3ee516599d Add initial RemovableDriveViewModel 2021-06-07 13:17:50 +02:00
akwizgran
a5fb3bb4a4 Merge branch '2016-2017-2018-removable-drive-reader-writer' into '1802-sync-via-removable-storage'
Create removable drive manager and reader/writer tasks

See merge request briar/briar!1458
2021-05-11 14:01:53 +00:00
akwizgran
eae329cdfa Refactor manager and tasks to remove reliance on files. 2021-05-11 12:19:16 +01:00
akwizgran
0ce0551f0d Update progress of writer task. 2021-05-11 12:19:16 +01:00
akwizgran
a198e7d08e Ensure that observers see the final state even if they're added late. 2021-05-11 12:19:16 +01:00
akwizgran
bca6f1506e Add integration test for syncing via removable drives. 2021-05-11 12:19:16 +01:00
akwizgran
e420201b00 Implement RemovableDriveWriterTask, except for progress updates. 2021-05-11 12:19:16 +01:00
akwizgran
03248d04e5 Fix typo in class names. 2021-05-11 12:19:16 +01:00
akwizgran
2c39b02644 Implement RemovableDriverReaderTask. 2021-05-11 12:19:16 +01:00
akwizgran
c9c6f3682c Add task factory. 2021-05-11 12:19:16 +01:00
akwizgran
8f4a0ef030 Add removable drive manager with placeholder task implementations. 2021-05-11 12:19:14 +01:00
akwizgran
5fe22bcd57 Merge branch '2035-android-removable-drive-plugin' into '1802-sync-via-removable-storage'
Add Android implementation of RemovableDrivePlugin

See merge request briar/briar!1457
2021-05-11 11:13:14 +00:00
akwizgran
b4880af7e2 Add Android implementation of RemovableDrivePlugin. 2021-05-10 14:19:24 +01:00
akwizgran
51d21bd669 Decouple RemovableDrivePlugin from FileConstants. 2021-05-10 13:48:12 +01:00
Torsten Grote
b8f3728a0d Merge branch '2015-removable-drive-plugin' into '1802-sync-via-removable-storage'
Create RemovableDrivePlugin

See merge request briar/briar!1454
2021-05-10 12:47:21 +00:00
akwizgran
bbfd4f137d Merge branch '2013-db-method-for-amount-of-data-to-sync' into '1802-sync-via-removable-storage'
Add DB method for getting amount of data to sync

See merge request briar/briar!1452
2021-05-10 12:00:11 +00:00
Daniel Lublin
7e3ca76dd1 Merge branch '2014-messages-sent-event' into '1802-sync-via-removable-storage'
Update MessagesSentEvent to include amount of data sent

See merge request briar/briar!1453
2021-05-10 11:44:27 +00:00
akwizgran
524c8d26f8 Don't inject default RemovableDrivePluginFactory on Android. 2021-05-07 17:48:39 +01:00
akwizgran
7eccf7dac1 Decouple removable drive plugin from java.io.File for portability. 2021-05-07 17:36:10 +01:00
akwizgran
0bc06248ed Clean up plugin injection code, remove unused module. 2021-05-06 16:59:45 +01:00
akwizgran
c999f05cc7 Configure removable drive plugin for Android. 2021-05-06 16:59:45 +01:00
akwizgran
428269b312 Add removable drive plugin. 2021-05-06 16:59:45 +01:00
akwizgran
588e05ce83 Update MessagesSentEvent to include amount of data sent. 2021-05-06 16:20:15 +01:00
akwizgran
f7875c99b6 Add DB method for getting amount of data to sync. 2021-05-05 17:52:37 +01:00
Torsten Grote
21fd7f5eed Merge branch 'allow-one-unreachable-bridge' into 'master'
Allow BridgeTest to pass if one bridge is unreachable

See merge request briar/briar!1449
2021-05-04 12:31:38 +00:00
akwizgran
6354e91b55 Allow BridgeTest to pass if one bridge is unreachable. 2021-05-04 13:13:57 +01:00
Torsten Grote
8123c06348 Merge branch '2012-update-bubbles-after-removing-messages' into 'master'
Update disappearing message bubbles after removing messages

Closes #2012

See merge request briar/briar!1448
2021-05-03 16:57:04 +00:00
akwizgran
663c648337 Update disappearing message bubbles after removing messages. 2021-05-03 15:16:11 +01:00
akwizgran
bee4e94987 Bump version numbers for 1.3.3 release. 2021-05-03 13:55:39 +01:00
akwizgran
c44bdc8762 Update translations. 2021-05-03 13:38:14 +01:00
Torsten Grote
423ecf71d8 Merge branch '1894-viewmodel-for-rssfeed-activities' into 'master'
Introduce ViewModel for RssFeed*Activity

Closes #1894

See merge request briar/briar!1366
2021-05-03 12:35:32 +00:00
Daniel Lublin
73c7882cc0 Introduce RssFeedViewModel
Furnishing the RssFeed function as a single activity with fragments for
Manage and Import.
2021-05-03 09:40:40 +02:00
Torsten Grote
683af1ec3a Merge branch '1827-target-api-30' into 'master'
Raise target API level to 30, upgrade build tools to 30.0.3

Closes #1827

See merge request briar/briar!1445
2021-04-30 17:44:49 +00:00
akwizgran
77ed15311c Raise target API level to 30, upgrade build tools to 30.0.3. 2021-04-30 14:51:49 +01:00
akwizgran
3d72557618 Merge branch '1962-connect-via-bt-backend' into 'master'
Implement connect via Bluetooth backend

Closes #1962

See merge request briar/briar!1412
2021-04-27 12:27:55 +00:00
Daniel Lublin
e2a11d42f8 Implement backend for connect via bluetooth 2021-04-27 14:15:10 +02:00
akwizgran
0f5ea6ae66 Merge branch 'translation-update-script' into 'master'
Add script to update Android app translations

See merge request briar/briar!1442
2021-04-27 11:09:24 +00:00
akwizgran
5b2c9b85f9 Merge branch 'prevent-double-pipelines' into 'master'
Avoid duplicate CI pipelines running at the same time

See merge request briar/briar!1443
2021-04-27 11:08:13 +00:00
Torsten Grote
94921230d9 Avoid duplicate CI pipelines running at the same time
Docs:
https://docs.gitlab.com/ee/ci/yaml/README.html#switch-between-branch-pipelines-and-merge-request-pipelines
https://docs.gitlab.com/ee/ci/yaml/README.html#avoid-duplicate-pipelines
2021-04-26 15:37:16 -03:00
Torsten Grote
34788356e6 Add script to update Android app translations
add German and Spanish (incl. video) play store listing
2021-04-26 15:14:06 -03:00
128 changed files with 3471 additions and 1249 deletions

View File

@@ -5,6 +5,15 @@ stages:
- optional_tests
- check_reproducibility
workflow:
# when to create a CI pipeline
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never # avoids duplicate jobs for branch and MR
- if: '$CI_COMMIT_BRANCH'
- if: '$CI_COMMIT_TAG'
.base-test:
before_script:
- set -e

View File

@@ -6,7 +6,7 @@ apply from: 'witness.gradle'
android {
compileSdkVersion 30
buildToolsVersion '30.0.2'
buildToolsVersion '30.0.3'
packagingOptions {
doNotStrip '**/*.so'
@@ -14,9 +14,9 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 10302
versionName "1.3.2"
targetSdkVersion 30
versionCode 10303
versionName "1.3.3"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -59,8 +59,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class AndroidBluetoothPlugin
extends BluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
class AndroidBluetoothPlugin extends
AbstractBluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
private static final Logger LOG =
getLogger(AndroidBluetoothPlugin.class.getName());
@@ -75,6 +75,7 @@ class AndroidBluetoothPlugin
// Non-null if the plugin started successfully
private volatile BluetoothAdapter adapter = null;
private volatile boolean stopDiscoverAndConnect;
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
BluetoothConnectionFactory<BluetoothSocket> connectionFactory,
@@ -187,22 +188,40 @@ class AndroidBluetoothPlugin
@Nullable
DuplexTransportConnection discoverAndConnect(String uuid) {
if (adapter == null) return null;
for (String address : discoverDevices()) {
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
return connectTo(address, uuid);
} catch (IOException e) {
if (LOG.isLoggable(INFO)) {
LOG.info("Could not connect to "
+ scrubMacAddress(address));
if (!discoverSemaphore.tryAcquire()) {
LOG.info("Discover already running");
return null;
}
try {
stopDiscoverAndConnect = false;
for (String address : discoverDevices()) {
if (stopDiscoverAndConnect) {
break;
}
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
return connectTo(address, uuid);
} catch (IOException e) {
if (LOG.isLoggable(INFO)) {
LOG.info("Could not connect to "
+ scrubMacAddress(address));
}
}
}
} finally {
discoverSemaphore.release();
}
LOG.info("Could not connect to any devices");
return null;
}
@Override
public void stopDiscoverAndConnect() {
stopDiscoverAndConnect = true;
adapter.cancelDiscovery();
}
private Collection<String> discoverDevices() {
List<String> addresses = new ArrayList<>();
BlockingQueue<Intent> intents = new LinkedBlockingQueue<>();

View File

@@ -47,7 +47,7 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
private final BackoffFactory backoffFactory;
@Inject
public AndroidBluetoothPluginFactory(@IoExecutor Executor ioExecutor,
AndroidBluetoothPluginFactory(@IoExecutor Executor ioExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor,
AndroidExecutor androidExecutor,
AndroidWakeLockManager wakeLockManager,

View File

@@ -0,0 +1,42 @@
package org.briarproject.bramble.plugin.file;
import android.app.Application;
import android.net.Uri;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_URI;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@Immutable
@NotNullByDefault
class AndroidRemovableDrivePlugin extends RemovableDrivePlugin {
private final Application app;
AndroidRemovableDrivePlugin(Application app, int maxLatency) {
super(maxLatency);
this.app = app;
}
@Override
InputStream openInputStream(TransportProperties p) throws IOException {
String uri = p.get(PROP_URI);
if (isNullOrEmpty(uri)) throw new IllegalArgumentException();
return app.getContentResolver().openInputStream(Uri.parse(uri));
}
@Override
OutputStream openOutputStream(TransportProperties p) throws IOException {
String uri = p.get(PROP_URI);
if (isNullOrEmpty(uri)) throw new IllegalArgumentException();
return app.getContentResolver().openOutputStream(Uri.parse(uri));
}
}

View File

@@ -0,0 +1,47 @@
package org.briarproject.bramble.plugin.file;
import android.app.Application;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.concurrent.TimeUnit.DAYS;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID;
@Immutable
@NotNullByDefault
public class AndroidRemovableDrivePluginFactory implements
SimplexPluginFactory {
private static final int MAX_LATENCY = (int) DAYS.toMillis(14);
private final Application app;
@Inject
AndroidRemovableDrivePluginFactory(Application app) {
this.app = app;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return MAX_LATENCY;
}
@Nullable
@Override
public SimplexPlugin createPlugin(PluginCallback callback) {
return new AndroidRemovableDrivePlugin(app, MAX_LATENCY);
}
}

View File

@@ -37,7 +37,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
private final Application app;
@Inject
public AndroidLanTcpPluginFactory(@IoExecutor Executor ioExecutor,
AndroidLanTcpPluginFactory(@IoExecutor Executor ioExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor,
EventBus eventBus,
BackoffFactory backoffFactory,

View File

@@ -58,7 +58,7 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
private final File torDirectory;
@Inject
public AndroidTorPluginFactory(@IoExecutor Executor ioExecutor,
AndroidTorPluginFactory(@IoExecutor Executor ioExecutor,
@WakefulIoExecutor Executor wakefulIoExecutor,
Application app,
NetworkManager networkManager,

View File

@@ -0,0 +1,9 @@
package org.briarproject.bramble.api;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public interface Consumer<T> {
void accept(T t);
}

View File

@@ -292,6 +292,16 @@ public interface DatabaseComponent extends TransactionManager {
*/
Message getMessage(Transaction txn, MessageId m) throws DbException;
/**
* Returns the total length, including headers, of any messages that are
* eligible to be sent to the given contact via a transport with the given
* max latency.
* <p/>
* Read-only.
*/
long getMessageBytesToSend(Transaction txn, ContactId c, int maxLatency)
throws DbException;
/**
* Returns the IDs of all delivered messages in the given group.
* <p/>

View File

@@ -1,4 +1,4 @@
package org.briarproject.bramble.api.plugin;
package org.briarproject.bramble.api.plugin.file;
public interface FileConstants {

View File

@@ -0,0 +1,11 @@
package org.briarproject.bramble.api.plugin.file;
import org.briarproject.bramble.api.plugin.TransportId;
public interface RemovableDriveConstants {
TransportId ID = new TransportId("org.briarproject.bramble.drive");
String PROP_PATH = "path";
String PROP_URI = "uri";
}

View File

@@ -0,0 +1,41 @@
package org.briarproject.bramble.api.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
import javax.annotation.Nullable;
@NotNullByDefault
public interface RemovableDriveManager {
/**
* Returns the currently running reader task for the given contact,
* or null if no task is running.
*/
@Nullable
RemovableDriveTask getCurrentReaderTask(ContactId c);
/**
* Returns the currently running writer task for the given contact,
* or null if no task is running.
*/
@Nullable
RemovableDriveTask getCurrentWriterTask(ContactId c);
/**
* Starts and returns a reader task for the given contact, reading from
* a stream described by the given transport properties. If a reader task
* for the contact is already running, it will be returned and the
* transport properties will be ignored.
*/
RemovableDriveTask startReaderTask(ContactId c, TransportProperties p);
/**
* Starts and returns a writer task for the given contact, writing to
* a stream described by the given transport properties. If a writer task
* for the contact is already running, it will be returned and the
* transport properties will be ignored.
*/
RemovableDriveTask startWriterTask(ContactId c, TransportProperties p);
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.bramble.api.plugin.file;
import org.briarproject.bramble.api.Consumer;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
@NotNullByDefault
public interface RemovableDriveTask extends Runnable {
/**
* Returns the {@link TransportProperties} that were used for creating
* this task.
*/
TransportProperties getTransportProperties();
/**
* Adds an observer to the task. The observer will be notified of state
* changes on the event thread. If the task has already finished, the
* observer will be notified of its final state.
*/
void addObserver(Consumer<State> observer);
/**
* Removes an observer from the task.
*/
void removeObserver(Consumer<State> observer);
class State {
private final long done, total;
private final boolean finished, success;
public State(long done, long total, boolean finished, boolean success) {
this.done = done;
this.total = total;
this.finished = finished;
this.success = success;
}
/**
* Returns the total length in bytes of the messages read or written
* so far.
*/
public long getDone() {
return done;
}
/**
* Returns the total length in bytes of the messages that will have
* been read or written when the task is complete, or zero if the
* total is unknown.
*/
public long getTotal() {
return total;
}
public boolean isFinished() {
return finished;
}
public boolean isSuccess() {
return success;
}
}
}

View File

@@ -18,11 +18,13 @@ public class MessagesSentEvent extends Event {
private final ContactId contactId;
private final Collection<MessageId> messageIds;
private final long totalLength;
public MessagesSentEvent(ContactId contactId,
Collection<MessageId> messageIds) {
Collection<MessageId> messageIds, long totalLength) {
this.contactId = contactId;
this.messageIds = messageIds;
this.totalLength = totalLength;
}
public ContactId getContactId() {
@@ -32,4 +34,8 @@ public class MessagesSentEvent extends Event {
public Collection<MessageId> getMessageIds() {
return messageIds;
}
public long getTotalLength() {
return totalLength;
}
}

View File

@@ -347,6 +347,16 @@ interface Database<T> {
*/
Message getMessage(T txn, MessageId m) throws DbException;
/**
* Returns the total length, including headers, of any messages that are
* eligible to be sent to the given contact via a transport with the given
* max latency.
* <p/>
* Read-only.
*/
long getMessageBytesToSend(T txn, ContactId c, int maxLatency)
throws DbException;
/**
* Returns the IDs and states of all dependencies of the given message.
* For missing dependencies and dependencies in other groups, the state

View File

@@ -415,14 +415,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchContactException();
Collection<MessageId> ids =
db.getMessagesToSend(txn, c, maxLength, maxLatency);
long totalLength = 0;
List<Message> messages = new ArrayList<>(ids.size());
for (MessageId m : ids) {
messages.add(db.getMessage(txn, m));
Message message = db.getMessage(txn, m);
totalLength += message.getRawLength();
messages.add(message);
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
}
if (ids.isEmpty()) return null;
db.lowerRequestedFlag(txn, c, ids);
transaction.attach(new MessagesSentEvent(c, ids));
transaction.attach(new MessagesSentEvent(c, ids, totalLength));
return messages;
}
@@ -467,14 +470,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchContactException();
Collection<MessageId> ids =
db.getRequestedMessagesToSend(txn, c, maxLength, maxLatency);
long totalLength = 0;
List<Message> messages = new ArrayList<>(ids.size());
for (MessageId m : ids) {
messages.add(db.getMessage(txn, m));
Message message = db.getMessage(txn, m);
totalLength += message.getRawLength();
messages.add(message);
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
}
if (ids.isEmpty()) return null;
db.lowerRequestedFlag(txn, c, ids);
transaction.attach(new MessagesSentEvent(c, ids));
transaction.attach(new MessagesSentEvent(c, ids, totalLength));
return messages;
}
@@ -569,6 +575,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return db.getMessage(txn, m);
}
@Override
public long getMessageBytesToSend(Transaction transaction, ContactId c,
int maxLatency) throws DbException {
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
return db.getMessageBytesToSend(txn, c, maxLatency);
}
@Override
public Collection<MessageId> getMessageIds(Transaction transaction,
GroupId g) throws DbException {

View File

@@ -1758,6 +1758,37 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Override
public long getMessageBytesToSend(Connection txn, ContactId c,
int maxLatency) throws DbException {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT SUM(length) FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE AND seen = FALSE"
+ " AND (expiry <= ? OR eta > ?)";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
ps.setLong(3, now);
ps.setLong(4, eta);
rs = ps.executeQuery();
rs.next();
long total = rs.getInt(1);
rs.close();
ps.close();
return total;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public Collection<MessageId> getMessageIds(Connection txn, GroupId g)
throws DbException {

View File

@@ -0,0 +1,623 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Multiset;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.macToBytes;
import static org.briarproject.bramble.util.StringUtils.macToString;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
EventListener {
private static final Logger LOG =
getLogger(AbstractBluetoothPlugin.class.getName());
private final BluetoothConnectionLimiter connectionLimiter;
final BluetoothConnectionFactory<S> connectionFactory;
private final Executor ioExecutor, wakefulIoExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final PluginCallback callback;
private final int maxLatency, maxIdleTime;
private final AtomicBoolean used = new AtomicBoolean(false);
private final AtomicBoolean everConnected = new AtomicBoolean(false);
protected final PluginState state = new PluginState();
protected final Semaphore discoverSemaphore = new Semaphore(1);
private volatile String contactConnectionsUuid = null;
abstract void initialiseAdapter() throws IOException;
abstract boolean isAdapterEnabled();
/**
* Returns the local Bluetooth address, or null if no valid address can
* be found.
*/
@Nullable
abstract String getBluetoothAddress();
abstract SS openServerSocket(String uuid) throws IOException;
abstract void tryToClose(@Nullable SS ss);
abstract DuplexTransportConnection acceptConnection(SS ss)
throws IOException;
abstract boolean isValidAddress(String address);
abstract DuplexTransportConnection connectTo(String address, String uuid)
throws IOException;
@Nullable
abstract DuplexTransportConnection discoverAndConnect(String uuid);
AbstractBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
BluetoothConnectionFactory<S> connectionFactory,
Executor ioExecutor,
Executor wakefulIoExecutor,
SecureRandom secureRandom,
Backoff backoff,
PluginCallback callback,
int maxLatency,
int maxIdleTime) {
this.connectionLimiter = connectionLimiter;
this.connectionFactory = connectionFactory;
this.ioExecutor = ioExecutor;
this.wakefulIoExecutor = wakefulIoExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
}
void onAdapterEnabled() {
LOG.info("Bluetooth enabled");
// We may not have been able to get the local address before
ioExecutor.execute(this::updateProperties);
if (getState() == INACTIVE) bind();
}
void onAdapterDisabled() {
LOG.info("Bluetooth disabled");
connectionLimiter.allConnectionsClosed();
// The server socket may not have been closed automatically
SS ss = state.clearServerSocket();
if (ss != null) {
LOG.info("Closing server socket");
tryToClose(ss);
}
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
return maxIdleTime;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
Settings settings = callback.getSettings();
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
DEFAULT_PREF_EVER_CONNECTED));
state.setStarted(enabledByUser);
try {
initialiseAdapter();
} catch (IOException e) {
throw new PluginException(e);
}
updateProperties();
if (enabledByUser && isAdapterEnabled()) bind();
}
private void bind() {
ioExecutor.execute(() -> {
if (getState() != INACTIVE) return;
// Bind a server socket to accept connections from contacts
SS ss;
try {
ss = openServerSocket(contactConnectionsUuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
}
if (!state.setServerSocket(ss)) {
LOG.info("Closing redundant server socket");
tryToClose(ss);
return;
}
backoff.reset();
acceptContactConnections(ss);
});
}
private void updateProperties() {
TransportProperties p = callback.getLocalProperties();
String address = p.get(PROP_ADDRESS);
String uuid = p.get(PROP_UUID);
Settings s = callback.getSettings();
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
boolean changed = false;
if (address == null || isReflected) {
address = getBluetoothAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Local address " + scrubMacAddress(address));
}
if (address == null) {
if (everConnected.get()) {
address = getReflectedAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Reflected address " +
scrubMacAddress(address));
}
if (address != null) {
changed = true;
isReflected = true;
}
}
} else {
changed = true;
isReflected = false;
}
}
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
changed = true;
}
contactConnectionsUuid = uuid;
if (changed) {
p = new TransportProperties();
// If we previously used a reflected address and there's no longer
// a reflected address with enough votes to be used, we'll continue
// to use the old reflected address until there's a new winner
if (address != null) p.put(PROP_ADDRESS, address);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
s = new Settings();
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
callback.mergeSettings(s);
}
}
@Nullable
private String getReflectedAddress() {
// Count the number of votes for each reflected address
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
Multiset<String> votes = new Multiset<>();
for (TransportProperties p : callback.getRemoteProperties()) {
String address = p.get(key);
if (address != null && isValidAddress(address)) votes.add(address);
}
// If an address gets more than half of the votes, accept it
int total = votes.getTotal();
for (String address : votes.keySet()) {
if (votes.getCount(address) * 2 > total) return address;
}
return null;
}
private void acceptContactConnections(SS ss) {
while (true) {
DuplexTransportConnection conn;
try {
conn = acceptConnection(ss);
} catch (IOException e) {
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket();
return;
}
LOG.info("Connection received");
connectionLimiter.connectionOpened(conn);
backoff.reset();
setEverConnected();
callback.handleConnection(conn);
}
}
private void setEverConnected() {
if (!everConnected.getAndSet(true)) {
ioExecutor.execute(() -> {
Settings s = new Settings();
s.putBoolean(PREF_EVER_CONNECTED, true);
callback.mergeSettings(s);
// Contacts may already have sent a reflected address
updateProperties();
});
}
}
@Override
public void stop() {
SS ss = state.setStopped();
tryToClose(ss);
}
@Override
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
}
}
private void connect(TransportProperties p, ConnectionHandler h) {
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return;
wakefulIoExecutor.execute(() -> {
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
setEverConnected();
h.handleConnection(d);
}
});
}
@Nullable
private DuplexTransportConnection connect(String address, String uuid) {
// Validate the address
if (!isValidAddress(address)) {
if (LOG.isLoggable(WARNING))
// Not scrubbing here to be able to figure out the problem
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
try {
//noinspection ResultOfMethodCallIgnored
UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
try {
DuplexTransportConnection conn = connectTo(address, uuid);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubMacAddress(address));
return conn;
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Could not connect to " + scrubMacAddress(address));
return null;
}
}
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (getState() != ACTIVE) return null;
if (!connectionLimiter.canOpenContactConnection()) return null;
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return null;
DuplexTransportConnection conn = connect(address, uuid);
if (conn != null) connectionLimiter.connectionOpened(conn);
return conn;
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving key agreement connections
SS ss;
try {
ss = openServerSocket(uuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
if (getState() != ACTIVE) {
tryToClose(ss);
return null;
}
BdfList descriptor = new BdfList();
descriptor.add(TRANSPORT_ID_BLUETOOTH);
String address = getBluetoothAddress();
if (address != null) descriptor.add(macToBytes(address));
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
DuplexTransportConnection conn;
if (descriptor.size() == 1) {
if (LOG.isLoggable(INFO)) {
LOG.info("Discovering address for key agreement UUID " +
uuid);
}
conn = discoverAndConnect(uuid);
} else {
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
conn = connect(address, uuid);
}
if (conn != null) {
connectionLimiter.connectionOpened(conn);
setEverConnected();
}
return conn;
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return macToString(mac);
}
@Override
public boolean isDiscovering() {
return discoverSemaphore.availablePermits() == 0;
}
@Override
public DuplexTransportConnection discoverAndConnectForSetup(String uuid) {
DuplexTransportConnection conn = discoverAndConnect(uuid);
if (conn != null) {
connectionLimiter.connectionOpened(conn);
setEverConnected();
}
return conn;
}
@Override
public boolean supportsRendezvous() {
return false;
}
@Override
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
boolean alice, ConnectionHandler incoming) {
throw new UnsupportedOperationException();
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(ID.getString()))
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
} else if (e instanceof KeyAgreementListeningEvent) {
ioExecutor.execute(connectionLimiter::keyAgreementStarted);
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
ioExecutor.execute(connectionLimiter::keyAgreementEnded);
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
RemoteTransportPropertiesUpdatedEvent r =
(RemoteTransportPropertiesUpdatedEvent) e;
if (r.getTransportId().equals(ID)) {
ioExecutor.execute(this::updateProperties);
}
}
}
@IoExecutor
private void onSettingsUpdated(Settings settings) {
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
SS ss = state.setEnabledByUser(enabledByUser);
State s = getState();
if (ss != null) {
LOG.info("Disabled by user, closing server socket");
tryToClose(ss);
} else if (s == INACTIVE) {
if (isAdapterEnabled()) {
LOG.info("Enabled by user, opening server socket");
bind();
} else {
LOG.info("Enabled by user but adapter is disabled");
}
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final SS ss;
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
super(descriptor);
this.ss = ss;
}
@Override
public KeyAgreementConnection accept() throws IOException {
DuplexTransportConnection conn = acceptConnection(ss);
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
connectionLimiter.connectionOpened(conn);
return new KeyAgreementConnection(conn, ID);
}
@Override
public void close() {
tryToClose(ss);
}
}
@ThreadSafe
@NotNullByDefault
private class PluginState {
@GuardedBy("this")
private boolean started = false,
stopped = false,
enabledByUser = false;
@GuardedBy("this")
@Nullable
private SS serverSocket = null;
private synchronized void setStarted(boolean enabledByUser) {
started = true;
this.enabledByUser = enabledByUser;
callback.pluginStateChanged(getState());
}
@Nullable
private synchronized SS setStopped() {
stopped = true;
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
@Nullable
private synchronized SS setEnabledByUser(boolean enabledByUser) {
this.enabledByUser = enabledByUser;
SS ss = null;
if (!enabledByUser) {
ss = serverSocket;
serverSocket = null;
}
callback.pluginStateChanged(getState());
return ss;
}
private synchronized boolean setServerSocket(SS ss) {
if (stopped || serverSocket != null) return false;
serverSocket = ss;
callback.pluginStateChanged(getState());
return true;
}
@Nullable
private synchronized SS clearServerSocket() {
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
private synchronized State getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!enabledByUser) return DISABLED;
return serverSocket == null ? INACTIVE : ACTIVE;
}
private synchronized int getReasonsDisabled() {
return getState() == DISABLED ? REASON_USER : 0;
}
}
}

View File

@@ -1,606 +1,18 @@
package org.briarproject.bramble.plugin.bluetooth;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Multiset;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.Backoff;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.macToBytes;
import static org.briarproject.bramble.util.StringUtils.macToString;
@NotNullByDefault
public interface BluetoothPlugin extends DuplexPlugin {
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BluetoothPlugin<S, SS> implements DuplexPlugin, EventListener {
private static final Logger LOG =
getLogger(BluetoothPlugin.class.getName());
final BluetoothConnectionLimiter connectionLimiter;
final BluetoothConnectionFactory<S> connectionFactory;
private final Executor ioExecutor, wakefulIoExecutor;
private final SecureRandom secureRandom;
private final Backoff backoff;
private final PluginCallback callback;
private final int maxLatency, maxIdleTime;
private final AtomicBoolean used = new AtomicBoolean(false);
private final AtomicBoolean everConnected = new AtomicBoolean(false);
protected final PluginState state = new PluginState();
private volatile String contactConnectionsUuid = null;
abstract void initialiseAdapter() throws IOException;
abstract boolean isAdapterEnabled();
/**
* Returns the local Bluetooth address, or null if no valid address can
* be found.
*/
@Nullable
abstract String getBluetoothAddress();
abstract SS openServerSocket(String uuid) throws IOException;
abstract void tryToClose(@Nullable SS ss);
abstract DuplexTransportConnection acceptConnection(SS ss)
throws IOException;
abstract boolean isValidAddress(String address);
abstract DuplexTransportConnection connectTo(String address, String uuid)
throws IOException;
boolean isDiscovering();
@Nullable
abstract DuplexTransportConnection discoverAndConnect(String uuid);
DuplexTransportConnection discoverAndConnectForSetup(String uuid);
BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
BluetoothConnectionFactory<S> connectionFactory,
Executor ioExecutor,
Executor wakefulIoExecutor,
SecureRandom secureRandom,
Backoff backoff,
PluginCallback callback,
int maxLatency,
int maxIdleTime) {
this.connectionLimiter = connectionLimiter;
this.connectionFactory = connectionFactory;
this.ioExecutor = ioExecutor;
this.wakefulIoExecutor = wakefulIoExecutor;
this.secureRandom = secureRandom;
this.backoff = backoff;
this.callback = callback;
this.maxLatency = maxLatency;
this.maxIdleTime = maxIdleTime;
}
void onAdapterEnabled() {
LOG.info("Bluetooth enabled");
// We may not have been able to get the local address before
ioExecutor.execute(this::updateProperties);
if (getState() == INACTIVE) bind();
}
void onAdapterDisabled() {
LOG.info("Bluetooth disabled");
connectionLimiter.allConnectionsClosed();
// The server socket may not have been closed automatically
SS ss = state.clearServerSocket();
if (ss != null) {
LOG.info("Closing server socket");
tryToClose(ss);
}
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
return maxIdleTime;
}
@Override
public void start() throws PluginException {
if (used.getAndSet(true)) throw new IllegalStateException();
Settings settings = callback.getSettings();
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
DEFAULT_PREF_EVER_CONNECTED));
state.setStarted(enabledByUser);
try {
initialiseAdapter();
} catch (IOException e) {
throw new PluginException(e);
}
updateProperties();
if (enabledByUser && isAdapterEnabled()) bind();
}
private void bind() {
ioExecutor.execute(() -> {
if (getState() != INACTIVE) return;
// Bind a server socket to accept connections from contacts
SS ss;
try {
ss = openServerSocket(contactConnectionsUuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
}
if (!state.setServerSocket(ss)) {
LOG.info("Closing redundant server socket");
tryToClose(ss);
return;
}
backoff.reset();
acceptContactConnections(ss);
});
}
private void updateProperties() {
TransportProperties p = callback.getLocalProperties();
String address = p.get(PROP_ADDRESS);
String uuid = p.get(PROP_UUID);
Settings s = callback.getSettings();
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
boolean changed = false;
if (address == null || isReflected) {
address = getBluetoothAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Local address " + scrubMacAddress(address));
}
if (address == null) {
if (everConnected.get()) {
address = getReflectedAddress();
if (LOG.isLoggable(INFO)) {
LOG.info("Reflected address " +
scrubMacAddress(address));
}
if (address != null) {
changed = true;
isReflected = true;
}
}
} else {
changed = true;
isReflected = false;
}
}
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
changed = true;
}
contactConnectionsUuid = uuid;
if (changed) {
p = new TransportProperties();
// If we previously used a reflected address and there's no longer
// a reflected address with enough votes to be used, we'll continue
// to use the old reflected address until there's a new winner
if (address != null) p.put(PROP_ADDRESS, address);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
s = new Settings();
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
callback.mergeSettings(s);
}
}
@Nullable
private String getReflectedAddress() {
// Count the number of votes for each reflected address
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
Multiset<String> votes = new Multiset<>();
for (TransportProperties p : callback.getRemoteProperties()) {
String address = p.get(key);
if (address != null && isValidAddress(address)) votes.add(address);
}
// If an address gets more than half of the votes, accept it
int total = votes.getTotal();
for (String address : votes.keySet()) {
if (votes.getCount(address) * 2 > total) return address;
}
return null;
}
private void acceptContactConnections(SS ss) {
while (true) {
DuplexTransportConnection conn;
try {
conn = acceptConnection(ss);
} catch (IOException e) {
// This is expected when the server socket is closed
LOG.info("Server socket closed");
state.clearServerSocket();
return;
}
LOG.info("Connection received");
connectionLimiter.connectionOpened(conn);
backoff.reset();
setEverConnected();
callback.handleConnection(conn);
}
}
private void setEverConnected() {
if (!everConnected.getAndSet(true)) {
ioExecutor.execute(() -> {
Settings s = new Settings();
s.putBoolean(PREF_EVER_CONNECTED, true);
callback.mergeSettings(s);
// Contacts may already have sent a reflected address
updateProperties();
});
}
}
@Override
public void stop() {
SS ss = state.setStopped();
tryToClose(ss);
}
@Override
public State getState() {
return state.getState();
}
@Override
public int getReasonsDisabled() {
return state.getReasonsDisabled();
}
@Override
public boolean shouldPoll() {
return true;
}
@Override
public int getPollingInterval() {
return backoff.getPollingInterval();
}
@Override
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
properties) {
if (getState() != ACTIVE) return;
backoff.increment();
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
connect(p.getFirst(), p.getSecond());
}
}
private void connect(TransportProperties p, ConnectionHandler h) {
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return;
wakefulIoExecutor.execute(() -> {
DuplexTransportConnection d = createConnection(p);
if (d != null) {
backoff.reset();
setEverConnected();
h.handleConnection(d);
}
});
}
@Nullable
private DuplexTransportConnection connect(String address, String uuid) {
// Validate the address
if (!isValidAddress(address)) {
if (LOG.isLoggable(WARNING))
// Not scrubbing here to be able to figure out the problem
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
try {
//noinspection ResultOfMethodCallIgnored
UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubMacAddress(address));
try {
DuplexTransportConnection conn = connectTo(address, uuid);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubMacAddress(address));
return conn;
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Could not connect to " + scrubMacAddress(address));
return null;
}
}
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (getState() != ACTIVE) return null;
if (!connectionLimiter.canOpenContactConnection()) return null;
String address = p.get(PROP_ADDRESS);
if (isNullOrEmpty(address)) return null;
String uuid = p.get(PROP_UUID);
if (isNullOrEmpty(uuid)) return null;
DuplexTransportConnection conn = connect(address, uuid);
if (conn != null) connectionLimiter.connectionOpened(conn);
return conn;
}
@Override
public boolean supportsKeyAgreement() {
return true;
}
@Override
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving key agreement connections
SS ss;
try {
ss = openServerSocket(uuid);
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
if (getState() != ACTIVE) {
tryToClose(ss);
return null;
}
BdfList descriptor = new BdfList();
descriptor.add(TRANSPORT_ID_BLUETOOTH);
String address = getBluetoothAddress();
if (address != null) descriptor.add(macToBytes(address));
return new BluetoothKeyAgreementListener(descriptor, ss);
}
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, BdfList descriptor) {
if (getState() != ACTIVE) return null;
// No truncation necessary because COMMIT_LENGTH = 16
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
DuplexTransportConnection conn;
if (descriptor.size() == 1) {
if (LOG.isLoggable(INFO)) {
LOG.info("Discovering address for key agreement UUID " +
uuid);
}
conn = discoverAndConnect(uuid);
} else {
String address;
try {
address = parseAddress(descriptor);
} catch (FormatException e) {
LOG.info("Invalid address in key agreement descriptor");
return null;
}
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
conn = connect(address, uuid);
}
if (conn != null) {
connectionLimiter.connectionOpened(conn);
setEverConnected();
}
return conn;
}
private String parseAddress(BdfList descriptor) throws FormatException {
byte[] mac = descriptor.getRaw(1);
if (mac.length != 6) throw new FormatException();
return macToString(mac);
}
@Override
public boolean supportsRendezvous() {
return false;
}
@Override
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
boolean alice, ConnectionHandler incoming) {
throw new UnsupportedOperationException();
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(ID.getString()))
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
} else if (e instanceof KeyAgreementListeningEvent) {
ioExecutor.execute(connectionLimiter::keyAgreementStarted);
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
ioExecutor.execute(connectionLimiter::keyAgreementEnded);
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
RemoteTransportPropertiesUpdatedEvent r =
(RemoteTransportPropertiesUpdatedEvent) e;
if (r.getTransportId().equals(ID)) {
ioExecutor.execute(this::updateProperties);
}
}
}
@IoExecutor
private void onSettingsUpdated(Settings settings) {
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
DEFAULT_PREF_PLUGIN_ENABLE);
SS ss = state.setEnabledByUser(enabledByUser);
State s = getState();
if (ss != null) {
LOG.info("Disabled by user, closing server socket");
tryToClose(ss);
} else if (s == INACTIVE) {
if (isAdapterEnabled()) {
LOG.info("Enabled by user, opening server socket");
bind();
} else {
LOG.info("Enabled by user but adapter is disabled");
}
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final SS ss;
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
super(descriptor);
this.ss = ss;
}
@Override
public KeyAgreementConnection accept() throws IOException {
DuplexTransportConnection conn = acceptConnection(ss);
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
connectionLimiter.connectionOpened(conn);
return new KeyAgreementConnection(conn, ID);
}
@Override
public void close() {
tryToClose(ss);
}
}
@ThreadSafe
@NotNullByDefault
protected class PluginState {
@GuardedBy("this")
private boolean started = false,
stopped = false,
enabledByUser = false;
@GuardedBy("this")
@Nullable
private SS serverSocket = null;
synchronized void setStarted(boolean enabledByUser) {
started = true;
this.enabledByUser = enabledByUser;
callback.pluginStateChanged(getState());
}
@Nullable
synchronized SS setStopped() {
stopped = true;
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
@Nullable
synchronized SS setEnabledByUser(boolean enabledByUser) {
this.enabledByUser = enabledByUser;
SS ss = null;
if (!enabledByUser) {
ss = serverSocket;
serverSocket = null;
}
callback.pluginStateChanged(getState());
return ss;
}
synchronized boolean setServerSocket(SS ss) {
if (stopped || serverSocket != null) return false;
serverSocket = ss;
callback.pluginStateChanged(getState());
return true;
}
@Nullable
synchronized SS clearServerSocket() {
SS ss = serverSocket;
serverSocket = null;
callback.pluginStateChanged(getState());
return ss;
}
synchronized State getState() {
if (!started || stopped) return STARTING_STOPPING;
if (!enabledByUser) return DISABLED;
return serverSocket == null ? INACTIVE : ACTIVE;
}
synchronized int getReasonsDisabled() {
return getState() == DISABLED ? REASON_USER : 0;
}
}
void stopDiscoverAndConnect();
}

View File

@@ -0,0 +1,114 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@NotNullByDefault
abstract class AbstractRemovableDrivePlugin implements SimplexPlugin {
private static final Logger LOG =
getLogger(AbstractRemovableDrivePlugin.class.getName());
private final int maxLatency;
abstract InputStream openInputStream(TransportProperties p)
throws IOException;
abstract OutputStream openOutputStream(TransportProperties p)
throws IOException;
AbstractRemovableDrivePlugin(int maxLatency) {
this.maxLatency = maxLatency;
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return maxLatency;
}
@Override
public int getMaxIdleTime() {
// Unused for simplex transports
throw new UnsupportedOperationException();
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public State getState() {
return ACTIVE;
}
@Override
public int getReasonsDisabled() {
return 0;
}
@Override
public boolean shouldPoll() {
return false;
}
@Override
public int getPollingInterval() {
throw new UnsupportedOperationException();
}
@Override
public void poll(
Collection<Pair<TransportProperties, ConnectionHandler>> properties) {
throw new UnsupportedOperationException();
}
@Override
public TransportConnectionReader createReader(TransportProperties p) {
try {
return new TransportInputStreamReader(openInputStream(p));
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
}
@Override
public TransportConnectionWriter createWriter(TransportProperties p) {
try {
return new TransportOutputStreamWriter(this, openOutputStream(p));
} catch (IOException e) {
logException(LOG, WARNING, e);
return null;
}
}
}

View File

@@ -15,8 +15,8 @@ import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.FileConstants.PROP_PATH;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;

View File

@@ -0,0 +1,83 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
@ThreadSafe
@NotNullByDefault
class RemovableDriveManagerImpl
implements RemovableDriveManager, RemovableDriveTaskRegistry {
private final Executor ioExecutor;
private final RemovableDriveTaskFactory taskFactory;
private final ConcurrentHashMap<ContactId, RemovableDriveTask>
readers = new ConcurrentHashMap<>();
private final ConcurrentHashMap<ContactId, RemovableDriveTask>
writers = new ConcurrentHashMap<>();
@Inject
RemovableDriveManagerImpl(@IoExecutor Executor ioExecutor,
RemovableDriveTaskFactory taskFactory) {
this.ioExecutor = ioExecutor;
this.taskFactory = taskFactory;
}
@Nullable
@Override
public RemovableDriveTask getCurrentReaderTask(ContactId c) {
return readers.get(c);
}
@Nullable
@Override
public RemovableDriveTask getCurrentWriterTask(ContactId c) {
return writers.get(c);
}
@Override
public RemovableDriveTask startReaderTask(ContactId c,
TransportProperties p) {
RemovableDriveTask task = taskFactory.createReader(this, c, p);
RemovableDriveTask old = readers.putIfAbsent(c, task);
if (old == null) {
ioExecutor.execute(task);
return task;
} else {
return old;
}
}
@Override
public RemovableDriveTask startWriterTask(ContactId c,
TransportProperties p) {
RemovableDriveTask task = taskFactory.createWriter(this, c, p);
RemovableDriveTask old = writers.putIfAbsent(c, task);
if (old == null) {
ioExecutor.execute(task);
return task;
} else {
return old;
}
}
@Override
public void removeReader(ContactId c, RemovableDriveTask task) {
readers.remove(c, task);
}
@Override
public void removeWriter(ContactId c, RemovableDriveTask task) {
writers.remove(c, task);
}
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class RemovableDriveModule {
@Provides
@Singleton
RemovableDriveManager provideRemovableDriveManager(
RemovableDriveManagerImpl removableDriveManager) {
return removableDriveManager;
}
@Provides
RemovableDriveTaskFactory provideTaskFactory(
RemovableDriveTaskFactoryImpl taskFactory) {
return taskFactory;
}
}

View File

@@ -0,0 +1,38 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_PATH;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@Immutable
@NotNullByDefault
class RemovableDrivePlugin extends AbstractRemovableDrivePlugin {
RemovableDrivePlugin(int maxLatency) {
super(maxLatency);
}
@Override
InputStream openInputStream(TransportProperties p) throws IOException {
String path = p.get(PROP_PATH);
if (isNullOrEmpty(path)) throw new IllegalArgumentException();
return new FileInputStream(path);
}
@Override
OutputStream openOutputStream(TransportProperties p) throws IOException {
String path = p.get(PROP_PATH);
if (isNullOrEmpty(path)) throw new IllegalArgumentException();
return new FileOutputStream(path);
}
}

View File

@@ -0,0 +1,41 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.concurrent.TimeUnit.DAYS;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID;
@Immutable
@NotNullByDefault
public class RemovableDrivePluginFactory implements SimplexPluginFactory {
private static final int MAX_LATENCY = (int) DAYS.toMillis(14);
@Inject
RemovableDrivePluginFactory() {
}
@Override
public TransportId getId() {
return ID;
}
@Override
public int getMaxLatency() {
return MAX_LATENCY;
}
@Nullable
@Override
public SimplexPlugin createPlugin(PluginCallback callback) {
return new RemovableDrivePlugin(MAX_LATENCY);
}
}

View File

@@ -0,0 +1,88 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.event.MessageAddedEvent;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID;
@NotNullByDefault
class RemovableDriveReaderTask extends RemovableDriveTaskImpl
implements EventListener {
private final static Logger LOG =
getLogger(RemovableDriveReaderTask.class.getName());
RemovableDriveReaderTask(
Executor eventExecutor,
PluginManager pluginManager,
ConnectionManager connectionManager,
EventBus eventBus,
RemovableDriveTaskRegistry registry,
ContactId contactId,
TransportProperties transportProperties) {
super(eventExecutor, pluginManager, connectionManager, eventBus,
registry, contactId, transportProperties);
}
@Override
public void run() {
TransportConnectionReader r =
getPlugin().createReader(transportProperties);
if (r == null) {
LOG.warning("Failed to create reader");
registry.removeReader(contactId, this);
setSuccess(false);
return;
}
eventBus.addListener(this);
connectionManager.manageIncomingConnection(ID, new DecoratedReader(r));
}
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) {
MessageAddedEvent m = (MessageAddedEvent) e;
if (contactId.equals(m.getContactId())) {
LOG.info("Message received");
addDone(m.getMessage().getRawLength());
}
}
}
private class DecoratedReader implements TransportConnectionReader {
private final TransportConnectionReader delegate;
private DecoratedReader(TransportConnectionReader delegate) {
this.delegate = delegate;
}
@Override
public InputStream getInputStream() throws IOException {
return delegate.getInputStream();
}
@Override
public void dispose(boolean exception, boolean recognised)
throws IOException {
delegate.dispose(exception, recognised);
registry.removeReader(contactId, RemovableDriveReaderTask.this);
eventBus.removeListener(RemovableDriveReaderTask.this);
setSuccess(!exception && recognised);
}
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.properties.TransportProperties;
@NotNullByDefault
interface RemovableDriveTaskFactory {
RemovableDriveTask createReader(RemovableDriveTaskRegistry registry,
ContactId c, TransportProperties p);
RemovableDriveTask createWriter(RemovableDriveTaskRegistry registry,
ContactId c, TransportProperties p);
}

View File

@@ -0,0 +1,55 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class RemovableDriveTaskFactoryImpl implements RemovableDriveTaskFactory {
private final DatabaseComponent db;
private final Executor eventExecutor;
private final PluginManager pluginManager;
private final ConnectionManager connectionManager;
private final EventBus eventBus;
@Inject
RemovableDriveTaskFactoryImpl(
DatabaseComponent db,
@EventExecutor Executor eventExecutor,
PluginManager pluginManager,
ConnectionManager connectionManager,
EventBus eventBus) {
this.db = db;
this.eventExecutor = eventExecutor;
this.pluginManager = pluginManager;
this.connectionManager = connectionManager;
this.eventBus = eventBus;
}
@Override
public RemovableDriveTask createReader(RemovableDriveTaskRegistry registry,
ContactId c, TransportProperties p) {
return new RemovableDriveReaderTask(eventExecutor, pluginManager,
connectionManager, eventBus, registry, c, p);
}
@Override
public RemovableDriveTask createWriter(RemovableDriveTaskRegistry registry,
ContactId c, TransportProperties p) {
return new RemovableDriveWriterTask(db, eventExecutor, pluginManager,
connectionManager, eventBus, registry, c, p);
}
}

View File

@@ -0,0 +1,120 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.Consumer;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.lang.Math.min;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID;
@ThreadSafe
@NotNullByDefault
abstract class RemovableDriveTaskImpl implements RemovableDriveTask {
private final Executor eventExecutor;
private final PluginManager pluginManager;
final ConnectionManager connectionManager;
final EventBus eventBus;
final RemovableDriveTaskRegistry registry;
final ContactId contactId;
final TransportProperties transportProperties;
private final Object lock = new Object();
@GuardedBy("lock")
private final List<Consumer<State>> observers = new ArrayList<>();
@GuardedBy("lock")
private State state = new State(0, 0, false, false);
RemovableDriveTaskImpl(
Executor eventExecutor,
PluginManager pluginManager,
ConnectionManager connectionManager,
EventBus eventBus,
RemovableDriveTaskRegistry registry,
ContactId contactId,
TransportProperties transportProperties) {
this.eventExecutor = eventExecutor;
this.pluginManager = pluginManager;
this.connectionManager = connectionManager;
this.eventBus = eventBus;
this.registry = registry;
this.contactId = contactId;
this.transportProperties = transportProperties;
}
@Override
public TransportProperties getTransportProperties() {
return transportProperties;
}
@Override
public void addObserver(Consumer<State> o) {
State state;
synchronized (lock) {
observers.add(o);
state = this.state;
}
if (state.isFinished()) {
eventExecutor.execute(() -> o.accept(state));
}
}
@Override
public void removeObserver(Consumer<State> o) {
synchronized (lock) {
observers.remove(o);
}
}
SimplexPlugin getPlugin() {
return (SimplexPlugin) requireNonNull(pluginManager.getPlugin(ID));
}
void setTotal(long total) {
synchronized (lock) {
state = new State(state.getDone(), total, state.isFinished(),
state.isSuccess());
notifyObservers();
}
}
void addDone(long done) {
synchronized (lock) {
// Done and total come from different sources; make them consistent
done = min(state.getDone() + done, state.getTotal());
state = new State(done, state.getTotal(), state.isFinished(),
state.isSuccess());
}
notifyObservers();
}
void setSuccess(boolean success) {
synchronized (lock) {
state = new State(state.getDone(), state.getTotal(), true, success);
}
notifyObservers();
}
@GuardedBy("lock")
private void notifyObservers() {
List<Consumer<State>> observers = new ArrayList<>(this.observers);
State state = this.state;
eventExecutor.execute(() -> {
for (Consumer<State> o : observers) o.accept(state);
});
}
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
@NotNullByDefault
interface RemovableDriveTaskRegistry {
void removeReader(ContactId c, RemovableDriveTask task);
void removeWriter(ContactId c, RemovableDriveTask task);
}

View File

@@ -0,0 +1,120 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.ID;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class RemovableDriveWriterTask extends RemovableDriveTaskImpl
implements EventListener {
private static final Logger LOG =
getLogger(RemovableDriveWriterTask.class.getName());
private final DatabaseComponent db;
RemovableDriveWriterTask(
DatabaseComponent db,
Executor eventExecutor,
PluginManager pluginManager,
ConnectionManager connectionManager,
EventBus eventBus,
RemovableDriveTaskRegistry registry,
ContactId contactId,
TransportProperties transportProperties) {
super(eventExecutor, pluginManager, connectionManager, eventBus,
registry, contactId, transportProperties);
this.db = db;
}
@Override
public void run() {
SimplexPlugin plugin = getPlugin();
TransportConnectionWriter w = plugin.createWriter(transportProperties);
if (w == null) {
LOG.warning("Failed to create writer");
registry.removeWriter(contactId, this);
setSuccess(false);
return;
}
int maxLatency = plugin.getMaxLatency();
try {
setTotal(db.transactionWithResult(true, txn ->
db.getMessageBytesToSend(txn, contactId, maxLatency)));
} catch (DbException e) {
logException(LOG, WARNING, e);
registry.removeWriter(contactId, this);
setSuccess(false);
return;
}
eventBus.addListener(this);
connectionManager.manageOutgoingConnection(contactId, ID,
new DecoratedWriter(w));
}
@Override
public void eventOccurred(Event e) {
if (e instanceof MessagesSentEvent) {
MessagesSentEvent m = (MessagesSentEvent) e;
if (contactId.equals(m.getContactId())) {
if (LOG.isLoggable(INFO)) {
LOG.info(m.getMessageIds().size() + " messages sent");
}
addDone(m.getTotalLength());
}
}
}
private class DecoratedWriter implements TransportConnectionWriter {
private final TransportConnectionWriter delegate;
private DecoratedWriter(TransportConnectionWriter delegate) {
this.delegate = delegate;
}
@Override
public int getMaxLatency() {
return delegate.getMaxLatency();
}
@Override
public int getMaxIdleTime() {
return delegate.getMaxIdleTime();
}
@Override
public OutputStream getOutputStream() throws IOException {
return delegate.getOutputStream();
}
@Override
public void dispose(boolean exception) throws IOException {
delegate.dispose(exception);
registry.removeWriter(contactId, RemovableDriveWriterTask.this);
eventBus.removeListener(RemovableDriveWriterTask.this);
setSuccess(!exception);
}
}
}

View File

@@ -0,0 +1,34 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import java.io.InputStream;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@NotNullByDefault
class TransportInputStreamReader implements TransportConnectionReader {
private static final Logger LOG =
getLogger(TransportInputStreamReader.class.getName());
private final InputStream in;
TransportInputStreamReader(InputStream in) {
this.in = in;
}
@Override
public InputStream getInputStream() {
return in;
}
@Override
public void dispose(boolean exception, boolean recognised) {
tryToClose(in, LOG, WARNING);
}
}

View File

@@ -0,0 +1,47 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import java.io.OutputStream;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@NotNullByDefault
class TransportOutputStreamWriter implements TransportConnectionWriter {
private static final Logger LOG =
getLogger(TransportOutputStreamWriter.class.getName());
private final Plugin plugin;
private final OutputStream out;
TransportOutputStreamWriter(Plugin plugin, OutputStream out) {
this.plugin = plugin;
this.out = out;
}
@Override
public int getMaxLatency() {
return plugin.getMaxLatency();
}
@Override
public int getMaxIdleTime() {
return plugin.getMaxIdleTime();
}
@Override
public OutputStream getOutputStream() {
return out;
}
@Override
public void dispose(boolean exception) {
tryToClose(out, LOG, WARNING);
}
}

View File

@@ -298,11 +298,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception {
context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not)
exactly(18).of(database).startTransaction();
exactly(19).of(database).startTransaction();
will(returnValue(txn));
exactly(18).of(database).containsContact(txn, contactId);
exactly(19).of(database).containsContact(txn, contactId);
will(returnValue(false));
exactly(18).of(database).abortTransaction(txn);
exactly(19).of(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
@@ -349,7 +349,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getContact(transaction, contactId));
fail();
} catch (NoSuchContactException expected) {
@@ -357,7 +357,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageBytesToSend(transaction, contactId, 123));
fail();
} catch (NoSuchContactException expected) {
// Expected
}
try {
db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, groupId));
fail();
} catch (NoSuchContactException expected) {
@@ -365,7 +373,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getMessageStatus(transaction, contactId, messageId));
fail();
} catch (NoSuchContactException expected) {
@@ -373,7 +381,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getGroupVisibility(transaction, contactId, groupId));
fail();
} catch (NoSuchContactException expected) {
@@ -381,7 +389,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
}
try {
db.transaction(false, transaction ->
db.transaction(true, transaction ->
db.getSyncVersions(transaction, contactId));
fail();
} catch (NoSuchContactException expected) {

View File

@@ -227,6 +227,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
assertEquals(message.getRawLength(),
db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Changing the status to seen = true should make the message unsendable
db.raiseSeenFlag(txn, contactId, messageId);
@@ -234,6 +236,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
db.commitTransaction(txn);
db.close();
@@ -258,6 +261,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Marking the message delivered should make it sendable
db.setMessageState(txn, messageId, DELIVERED);
@@ -265,6 +269,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
assertEquals(message.getRawLength(),
db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Marking the message invalid should make it unsendable
db.setMessageState(txn, messageId, INVALID);
@@ -272,6 +278,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Marking the message pending should make it unsendable
db.setMessageState(txn, messageId, PENDING);
@@ -279,6 +286,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
db.commitTransaction(txn);
db.close();
@@ -302,6 +310,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Making the group visible should not make the message sendable
db.addGroupVisibility(txn, contactId, groupId, false);
@@ -309,6 +318,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Sharing the group should make the message sendable
db.setGroupVisibility(txn, contactId, groupId, true);
@@ -316,6 +326,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
assertEquals(message.getRawLength(),
db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Unsharing the group should make the message unsendable
db.setGroupVisibility(txn, contactId, groupId, false);
@@ -323,6 +335,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Making the group invisible should make the message unsendable
db.removeGroupVisibility(txn, contactId, groupId);
@@ -330,6 +343,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
db.commitTransaction(txn);
db.close();
@@ -354,6 +368,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(ids.isEmpty());
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(0, db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// Sharing the message should make it sendable
db.setMessageShared(txn, messageId, true);
@@ -361,6 +376,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(singletonList(messageId), ids);
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
assertEquals(message.getRawLength(),
db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
db.commitTransaction(txn);
db.close();
@@ -384,10 +401,15 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.getMessagesToSend(txn, contactId, message.getRawLength() - 1,
MAX_LATENCY);
assertTrue(ids.isEmpty());
assertEquals(message.getRawLength(),
db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
// The message is just the right size to send
ids = db.getMessagesToSend(txn, contactId, message.getRawLength(),
MAX_LATENCY);
assertEquals(singletonList(messageId), ids);
assertEquals(message.getRawLength(),
db.getMessageBytesToSend(txn, contactId, MAX_LATENCY));
db.commitTransaction(txn);
db.close();

View File

@@ -0,0 +1,168 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.Identity;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.junit.Assert.assertTrue;
public class RemovableDriveIntegrationTest extends BrambleTestCase {
private static final int TIMEOUT_MS = 5_000;
private final File testDir = getTestDirectory();
private final File aliceDir = new File(testDir, "alice");
private final File bobDir = new File(testDir, "bob");
private final SecretKey rootKey = getSecretKey();
private final long timestamp = System.currentTimeMillis();
private RemovableDriveIntegrationTestComponent alice, bob;
@Before
public void setUp() {
assertTrue(testDir.mkdirs());
alice = DaggerRemovableDriveIntegrationTestComponent.builder()
.testDatabaseConfigModule(
new TestDatabaseConfigModule(aliceDir)).build();
RemovableDriveIntegrationTestComponent.Helper
.injectEagerSingletons(alice);
bob = DaggerRemovableDriveIntegrationTestComponent.builder()
.testDatabaseConfigModule(
new TestDatabaseConfigModule(bobDir)).build();
RemovableDriveIntegrationTestComponent.Helper
.injectEagerSingletons(bob);
}
@Test
public void testWriteAndRead() throws Exception {
// Create the identities
Identity aliceIdentity =
alice.getIdentityManager().createIdentity("Alice");
Identity bobIdentity = bob.getIdentityManager().createIdentity("Bob");
// Set up the devices and get the contact IDs
ContactId bobId = setUp(alice, aliceIdentity,
bobIdentity.getLocalAuthor(), true);
ContactId aliceId = setUp(bob, bobIdentity,
aliceIdentity.getLocalAuthor(), false);
// Sync Alice's client versions and transport properties
read(bob, aliceId, write(alice, bobId), 2);
// Sync Bob's client versions and transport properties
read(alice, bobId, write(bob, aliceId), 2);
}
private ContactId setUp(RemovableDriveIntegrationTestComponent device,
Identity local, Author remote, boolean alice) throws Exception {
// Add an identity for the user
IdentityManager identityManager = device.getIdentityManager();
identityManager.registerIdentity(local);
// Start the lifecycle manager
LifecycleManager lifecycleManager = device.getLifecycleManager();
lifecycleManager.startServices(getSecretKey());
lifecycleManager.waitForStartup();
// Add the other user as a contact
ContactManager contactManager = device.getContactManager();
return contactManager.addContact(remote, local.getId(), rootKey,
timestamp, alice, true, true);
}
@SuppressWarnings("SameParameterValue")
private void read(RemovableDriveIntegrationTestComponent device,
ContactId contactId, File file, int deliveries) throws Exception {
// Listen for message deliveries
MessageDeliveryListener listener =
new MessageDeliveryListener(deliveries);
device.getEventBus().addListener(listener);
// Read the incoming stream
TransportProperties p = new TransportProperties();
p.put(PROP_PATH, file.getAbsolutePath());
RemovableDriveTask reader = device.getRemovableDriveManager()
.startReaderTask(contactId, p);
CountDownLatch disposedLatch = new CountDownLatch(1);
reader.addObserver(state -> {
if (state.isFinished()) disposedLatch.countDown();
});
// Wait for the messages to be delivered
assertTrue(listener.delivered.await(TIMEOUT_MS, MILLISECONDS));
// Clean up the listener
device.getEventBus().removeListener(listener);
// Wait for the reader to be disposed
disposedLatch.await(TIMEOUT_MS, MILLISECONDS);
}
private File write(RemovableDriveIntegrationTestComponent device,
ContactId contactId) throws Exception {
// Write the outgoing stream to a file
File file = File.createTempFile("sync", ".tmp", testDir);
TransportProperties p = new TransportProperties();
p.put(PROP_PATH, file.getAbsolutePath());
RemovableDriveTask writer = device.getRemovableDriveManager()
.startWriterTask(contactId, p);
CountDownLatch disposedLatch = new CountDownLatch(1);
writer.addObserver(state -> {
if (state.isFinished()) disposedLatch.countDown();
});
// Wait for the writer to be disposed
disposedLatch.await(TIMEOUT_MS, MILLISECONDS);
// Return the file containing the stream
return file;
}
private void tearDown(RemovableDriveIntegrationTestComponent device)
throws Exception {
// Stop the lifecycle manager
LifecycleManager lifecycleManager = device.getLifecycleManager();
lifecycleManager.stopServices();
lifecycleManager.waitForShutdown();
}
@After
public void tearDown() throws Exception {
// Tear down the devices
tearDown(alice);
tearDown(bob);
deleteTestDirectory(testDir);
}
@NotNullByDefault
private static class MessageDeliveryListener implements EventListener {
private final CountDownLatch delivered;
private MessageDeliveryListener(int deliveries) {
delivered = new CountDownLatch(deliveries);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent m = (MessageStateChangedEvent) e;
if (m.getState().equals(DELIVERED)) delivered.countDown();
}
}
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestSecureRandomModule;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
BrambleCoreModule.class,
DefaultBatteryManagerModule.class,
DefaultEventExecutorModule.class,
DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class,
RemovableDriveIntegrationTestModule.class,
RemovableDriveModule.class,
TestSecureRandomModule.class,
TimeTravelModule.class
})
interface RemovableDriveIntegrationTestComponent
extends BrambleCoreEagerSingletons {
ContactManager getContactManager();
EventBus getEventBus();
IdentityManager getIdentityManager();
LifecycleManager getLifecycleManager();
RemovableDriveManager getRemovableDriveManager();
class Helper {
public static void injectEagerSingletons(
RemovableDriveIntegrationTestComponent c) {
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
}
}
}

View File

@@ -1,61 +1,81 @@
package org.briarproject.bramble.plugin;
package org.briarproject.bramble.plugin.file;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.plugin.bluetooth.JavaBluetoothPluginFactory;
import org.briarproject.bramble.plugin.modem.ModemPluginFactory;
import org.briarproject.bramble.plugin.tcp.LanTcpPluginFactory;
import org.briarproject.bramble.plugin.tcp.WanTcpPluginFactory;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
@Module
public class DesktopPluginModule extends PluginModule {
class RemovableDriveIntegrationTestModule {
@Provides
PluginConfig getPluginConfig(JavaBluetoothPluginFactory bluetooth,
ModemPluginFactory modem, LanTcpPluginFactory lan,
WanTcpPluginFactory wan) {
@Singleton
PluginConfig providePluginConfig(RemovableDrivePluginFactory drive) {
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@Override
public Collection<DuplexPluginFactory> getDuplexFactories() {
return asList(bluetooth, modem, lan, wan);
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return emptyList();
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return singletonList(drive);
}
@Override
public boolean shouldPoll() {
return true;
return false;
}
@Override
public Map<TransportId, List<TransportId>> getTransportPreferences() {
// Prefer LAN to Bluetooth
return singletonMap(BluetoothConstants.ID,
singletonList(LanTcpConstants.ID));
return emptyMap();
}
};
return pluginConfig;
}
@Provides
FeatureFlags provideFeatureFlags() {
return new FeatureFlags() {
@Override
public boolean shouldEnableImageAttachments() {
return true;
}
@Override
public boolean shouldEnableProfilePictures() {
return true;
}
@Override
public boolean shouldEnableDisappearingMessages() {
return true;
}
@Override
public boolean shouldEnableConnectViaBluetooth() {
return true;
}
};
}
}

View File

@@ -25,8 +25,8 @@ import static org.briarproject.bramble.util.StringUtils.isValidMac;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class JavaBluetoothPlugin
extends BluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
class JavaBluetoothPlugin extends
AbstractBluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
private static final Logger LOG =
getLogger(JavaBluetoothPlugin.class.getName());
@@ -108,6 +108,11 @@ class JavaBluetoothPlugin
return null; // TODO
}
@Override
public void stopDiscoverAndConnect() {
// TODO
}
private String makeUrl(String address, String uuid) {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
}

View File

@@ -55,7 +55,7 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
private final File torDirectory;
@Inject
public UnixTorPluginFactory(@IoExecutor Executor ioExecutor,
UnixTorPluginFactory(@IoExecutor Executor ioExecutor,
@IoExecutor Executor wakefulIoExecutor,
NetworkManager networkManager,
LocationUtils locationUtils,

View File

@@ -23,8 +23,10 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -45,15 +47,22 @@ import static org.junit.Assume.assumeTrue;
public class BridgeTest extends BrambleTestCase {
@Parameters
public static Iterable<String> data() {
public static Iterable<Params> data() {
BrambleJavaIntegrationTestComponent component =
DaggerBrambleJavaIntegrationTestComponent.builder().build();
BrambleCoreIntegrationTestEagerSingletons.Helper
.injectEagerSingletons(component);
return component.getCircumventionProvider().getBridges(false);
// Share a failure counter among all the test instances
AtomicInteger failures = new AtomicInteger(0);
List<String> bridges =
component.getCircumventionProvider().getBridges(false);
List<Params> states = new ArrayList<>(bridges.size());
for (String bridge : bridges) states.add(new Params(bridge, failures));
return states;
}
private final static long TIMEOUT = SECONDS.toMillis(60);
private final static int NUM_FAILURES_ALLOWED = 1;
private final static Logger LOG = getLogger(BridgeTest.class.getName());
@@ -80,11 +89,13 @@ public class BridgeTest extends BrambleTestCase {
private final File torDir = getTestDirectory();
private final String bridge;
private final AtomicInteger failures;
private UnixTorPluginFactory factory;
public BridgeTest(String bridge) {
this.bridge = bridge;
public BridgeTest(Params params) {
bridge = params.bridge;
failures = params.failures;
}
@Before
@@ -152,10 +163,24 @@ public class BridgeTest extends BrambleTestCase {
clock.sleep(500);
}
if (plugin.getState() != ACTIVE) {
fail("Could not connect to Tor within timeout.");
LOG.warning("Could not connect to Tor within timeout");
if (failures.incrementAndGet() > NUM_FAILURES_ALLOWED) {
fail(failures.get() + " bridges are unreachable");
}
}
} finally {
plugin.stop();
}
}
private static class Params {
private final String bridge;
private final AtomicInteger failures;
private Params(String bridge, AtomicInteger failures) {
this.bridge = bridge;
this.failures = failures;
}
}
}

View File

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

View File

@@ -17,7 +17,7 @@ def getStdout = { command, defaultValue ->
android {
compileSdkVersion 30
buildToolsVersion '30.0.2'
buildToolsVersion '30.0.3'
packagingOptions {
doNotStrip '**/*.so'
@@ -25,9 +25,9 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 10302
versionName "1.3.2"
targetSdkVersion 30
versionCode 10303
versionName "1.3.3"
applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true

View File

@@ -0,0 +1,5 @@
Briar ist eine Messaging-App für Aktivisten, Journalisten und jeden, der eine sichere, einfache und robuste Art der Kommunikation benötigt. Im Gegensatz zu herkömmlichen Messaging-Apps benötigt Briar keinen zentralen Server. Nachrichten werden direkt zwischen den Endgeräten der Benutzer ausgetauscht. Wenn das Internet ausfällt, kann Briar diese auch über Bluetooth oder WLAN austauschen, um den Informationsaustausch in einer Krise aufrecht zu erhalten. Mit einer Internet Verbindung synchronisiert sich Briar über das Tor-Netzwerk und schützt so die Benutzer und ihre Kontakte vor Überwachung.
Die App bietet private Nachrichten, Gruppen und Foren sowie Blogs. Die Unterstützung für das Tor-Netzwerk ist in die App integriert. Alles, was du in Briar machst, wird nur auf deinem Gerät gespeichert, es sei denn, du entscheidest dich, es mit anderen Benutzern zu teilen.
Es gibt keine Werbung und kein Tracking. Der Quellcode der App ist komplett offen für jeden einsehbar und wurde bereits professionell auditiert. Alle Versionen von Briar sind reproduzierbar, so dass überprüft werden kann, ob der veröffentlichte Quellcode genau mit der hier veröffentlichten App übereinstimmt. Die Entwicklung wird von einem kleinen Non-Profit-Team durchgeführt.

View File

@@ -0,0 +1 @@
Sicher kommunizieren, überall

View File

@@ -0,0 +1 @@
Briar

View File

@@ -0,0 +1,5 @@
Briar es una aplicación de mensajería diseñada para activistas, periodistas y cualquier otra persona que necesita una forma segura, fácil y robusta de comunicación. A diferencia de las aplicaciones de mensajería tradicionales, Briar no necesita un servidor central los mensajes son sincronizados directamente entre los dispositivos de los usuarios. Si no hay conexión a internet disponible, Briar puede sincronizarse vía Bluetooth o WiFi, manteniendo la información fluyendo en crisis. Si Internet está disponible, Briar puede sincronizarse vía red Tor, protegiendo a los usuarios y sus relaciones de la vigilancia.
La aplicación tiene como características mensajes, grupos y foros privados, como así también blogs. El soporte para red Tor está incorporado en la aplicación. Todo lo que haces en Briar solamente es almacenado en tu dispositivo, a menos que decidas compartirlo con otros usuarios.
No hay publicidades ni rastreo. El código fuente de la aplicación está completamente abierto para que cualquiera lo inspeccione, y ya ha sido auditado profesionalmente. Todas las versiones de Briar son reproducibles, haciendo posible verificar que el código fuente publicado se corresponda exactamente con la aplicación publicada aquí. El desarrollo es realizado por un reducido equipo sin fines de lucro.

View File

@@ -0,0 +1 @@
Mensajería segura, en cualquier lado.

View File

@@ -0,0 +1 @@
Briar

View File

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

View File

@@ -322,25 +322,6 @@
android:value="org.briarproject.briar.android.blog.BlogActivity" />
</activity>
<activity
android:name="org.briarproject.briar.android.blog.RssFeedImportActivity"
android:label="@string/blogs_rss_feeds_import"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" />
</activity>
<activity
android:name="org.briarproject.briar.android.blog.RssFeedManageActivity"
android:label="@string/blogs_rss_feeds_manage"
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.contact.add.nearby.AddNearbyContactActivity"
android:label="@string/add_contact_title"
@@ -447,6 +428,24 @@
android:theme="@style/BriarTheme"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name="org.briarproject.briar.android.blog.RssFeedActivity"
android:label="@string/blogs_rss_feeds"
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.removabledrive.RemovableDriveActivity"
android:label="TODO Removable Drive"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.conversation.ConversationActivity" />
</activity>
<activity
android:name=".android.contact.add.remote.PendingContactListActivity"
android:label="@string/pending_contact_requests"

View File

@@ -28,6 +28,7 @@ import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.file.RemovableDriveModule;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreEagerSingletons;
@@ -83,7 +84,8 @@ import dagger.Component;
AppModule.class,
AttachmentModule.class,
ClockModule.class,
MediaModule.class
MediaModule.class,
RemovableDriveModule.class
})
public interface AndroidComponent
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,

View File

@@ -24,6 +24,8 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import org.briarproject.bramble.api.reporting.DevConfig;
import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory;
import org.briarproject.bramble.plugin.file.AndroidRemovableDrivePluginFactory;
import org.briarproject.bramble.plugin.file.RemovableDriveModule;
import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory;
import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory;
import org.briarproject.bramble.util.AndroidUtils;
@@ -67,7 +69,6 @@ import dagger.Provides;
import static android.content.Context.MODE_PRIVATE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS;
@@ -92,6 +93,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
GroupListModule.class,
GroupConversationModule.class,
SharingModule.class,
RemovableDriveModule.class
})
public class AppModule {
@@ -149,8 +151,10 @@ public class AppModule {
}
@Provides
@Singleton
PluginConfig providePluginConfig(AndroidBluetoothPluginFactory bluetooth,
AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan) {
AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan,
AndroidRemovableDrivePluginFactory drive) {
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@@ -161,7 +165,7 @@ public class AppModule {
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return emptyList();
return singletonList(drive);
}
@Override

View File

@@ -13,8 +13,11 @@ import org.briarproject.briar.android.blog.BlogPostFragment;
import org.briarproject.briar.android.blog.FeedFragment;
import org.briarproject.briar.android.blog.ReblogActivity;
import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity;
import org.briarproject.briar.android.blog.RssFeedManageActivity;
import org.briarproject.briar.android.blog.RssFeedActivity;
import org.briarproject.briar.android.blog.RssFeedDeleteFeedDialogFragment;
import org.briarproject.briar.android.blog.RssFeedImportFailedDialogFragment;
import org.briarproject.briar.android.blog.RssFeedImportFragment;
import org.briarproject.briar.android.blog.RssFeedManageFragment;
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.contact.ContactListFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity;
@@ -60,6 +63,7 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsFragment;
import org.briarproject.briar.android.removabledrive.RemovableDriveActivity;
import org.briarproject.briar.android.reporting.CrashFragment;
import org.briarproject.briar.android.reporting.CrashReportActivity;
import org.briarproject.briar.android.reporting.ReportFormFragment;
@@ -161,9 +165,7 @@ public interface ActivityComponent {
void inject(IntroductionActivity activity);
void inject(RssFeedImportActivity activity);
void inject(RssFeedManageActivity activity);
void inject(RssFeedActivity activity);
void inject(StartupFailureActivity activity);
@@ -175,6 +177,8 @@ public interface ActivityComponent {
void inject(CrashReportActivity crashReportActivity);
void inject(RemovableDriveActivity activity);
// Fragments
void inject(SetupFragment fragment);
@@ -233,4 +237,12 @@ public interface ActivityComponent {
void inject(
BluetoothConnecterDialogFragment bluetoothConnecterDialogFragment);
void inject(RssFeedImportFragment fragment);
void inject(RssFeedManageFragment fragment);
void inject(RssFeedImportFailedDialogFragment fragment);
void inject(RssFeedDeleteFeedDialogFragment fragment);
}

View File

@@ -14,5 +14,7 @@ public interface RequestCodes {
int REQUEST_ATTACH_IMAGE = 13;
int REQUEST_SAVE_ATTACHMENT = 14;
int REQUEST_AVATAR_IMAGE = 15;
int REQUEST_REMOVABLE_DRIVE_WRITE = 16;
int REQUEST_REMOVABLE_DRIVE_READ = 17;
}

View File

@@ -20,4 +20,8 @@ public interface BlogModule {
@ViewModelKey(BlogViewModel.class)
ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
@Binds
@IntoMap
@ViewModelKey(RssFeedViewModel.class)
ViewModel bindRssFeedViewModel(RssFeedViewModel rssFeedViewModel);
}

View File

@@ -131,15 +131,8 @@ public class FeedFragment extends BaseFragment
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_import) {
Intent i = new Intent(getActivity(), RssFeedImportActivity.class);
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_manage) {
Blog personalBlog = viewModel.getPersonalBlog().getValue();
if (personalBlog == null) return false;
Intent i = new Intent(getActivity(), RssFeedManageActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
} else if (itemId == R.id.action_rss_feeds) {
Intent i = new Intent(getActivity(), RssFeedActivity.class);
startActivity(i);
return true;
}

View File

@@ -0,0 +1,69 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedActivity extends BriarActivity
implements BaseFragmentListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_container);
if (savedInstanceState == null) {
showInitialFragment(RssFeedManageFragment.newInstance());
}
viewModel.getImportResult().observeEvent(this, this::onImportResult);
}
private void onImportResult(RssFeedViewModel.ImportResult result) {
if (result == IMPORTED) {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(RssFeedImportFragment.TAG) != null) {
onBackPressed();
}
} else if (result == FAILED) {
RssFeedImportFailedDialogFragment dialog =
RssFeedImportFailedDialogFragment.newInstance();
dialog.show(getSupportFragmentManager(),
RssFeedImportFailedDialogFragment.TAG);
} else if (result == EXISTS) {
Toast.makeText(this, R.string.blogs_rss_feeds_import_exists,
Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -7,91 +7,54 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.api.feed.Feed;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
@NotNullByDefault
class RssFeedAdapter extends ListAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
private final RssFeedListener listener;
RssFeedAdapter(Context ctx, RssFeedListener listener) {
super(ctx, Feed.class);
RssFeedAdapter(RssFeedListener listener) {
super(new DiffUtil.ItemCallback<Feed>() {
@Override
public boolean areItemsTheSame(Feed a, Feed b) {
return a.getUrl().equals(b.getUrl()) &&
a.getBlogId().equals(b.getBlogId()) &&
a.getAdded() == b.getAdded();
}
@Override
public boolean areContentsTheSame(Feed a, Feed b) {
return a.getUpdated() == b.getUpdated();
}
});
this.listener = listener;
}
@Override
public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(ctx).inflate(
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_item_rss_feed, parent, false);
return new FeedViewHolder(v);
}
@Override
public void onBindViewHolder(FeedViewHolder ui, int position) {
Feed item = getItemAt(position);
if (item == null) return;
// Feed Title
ui.title.setText(item.getTitle());
// Delete Button
ui.delete.setOnClickListener(v -> listener.onDeleteClick(item));
// Author
if (item.getRssAuthor() != null) {
ui.author.setText(item.getRssAuthor());
ui.author.setVisibility(VISIBLE);
ui.authorLabel.setVisibility(VISIBLE);
} else {
ui.author.setVisibility(GONE);
ui.authorLabel.setVisibility(GONE);
}
// Imported and Last Updated
ui.imported.setText(UiUtils.formatDate(ctx, item.getAdded()));
ui.updated.setText(UiUtils.formatDate(ctx, item.getUpdated()));
// Description
if (item.getDescription() != null) {
ui.description.setText(item.getDescription());
ui.description.setVisibility(VISIBLE);
} else {
ui.description.setVisibility(GONE);
}
// Open feed's blog when clicked
ui.layout.setOnClickListener(v -> listener.onFeedClick(item));
ui.bindItem(getItem(position));
}
@Override
public int compare(Feed a, Feed b) {
if (a == b) return 0;
long aTime = a.getAdded(), bTime = b.getAdded();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
return 0;
}
@Override
public boolean areContentsTheSame(Feed a, Feed b) {
return a.getUpdated() == b.getUpdated();
}
@Override
public boolean areItemsTheSame(Feed a, Feed b) {
return a.getUrl().equals(b.getUrl()) &&
a.getBlogId().equals(b.getBlogId()) &&
a.getAdded() == b.getAdded();
}
static class FeedViewHolder extends RecyclerView.ViewHolder {
class FeedViewHolder extends RecyclerView.ViewHolder {
private final Context ctx;
private final View layout;
private final TextView title;
private final ImageButton delete;
@@ -104,6 +67,7 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
private FeedViewHolder(View v) {
super(v);
ctx = v.getContext();
layout = v;
title = v.findViewById(R.id.titleView);
delete = v.findViewById(R.id.deleteButton);
@@ -113,10 +77,44 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
authorLabel = v.findViewById(R.id.author);
description = v.findViewById(R.id.descriptionView);
}
private void bindItem(Feed item) {
// Feed Title
title.setText(item.getTitle());
// Delete Button
delete.setOnClickListener(v -> listener.onDeleteClick(item));
// Author
if (item.getRssAuthor() != null) {
author.setText(item.getRssAuthor());
author.setVisibility(VISIBLE);
authorLabel.setVisibility(VISIBLE);
} else {
author.setVisibility(GONE);
authorLabel.setVisibility(GONE);
}
// Imported and Last Updated
imported.setText(formatDate(ctx, item.getAdded()));
updated.setText(formatDate(ctx, item.getUpdated()));
// Description
if (item.getDescription() != null) {
description.setText(item.getDescription());
description.setVisibility(VISIBLE);
} else {
description.setVisibility(GONE);
}
// Open feed's blog when clicked
layout.setOnClickListener(v -> listener.onFeedClick(item));
}
}
interface RssFeedListener {
void onFeedClick(Feed feed);
void onDeleteClick(Feed feed);
}

View File

@@ -0,0 +1,64 @@
package org.briarproject.briar.android.blog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedDeleteFeedDialogFragment extends DialogFragment {
final static String TAG = RssFeedDeleteFeedDialogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
static RssFeedDeleteFeedDialogFragment newInstance(GroupId groupId) {
Bundle args = new Bundle();
args.putByteArray(GROUP_ID, groupId.getBytes());
RssFeedDeleteFeedDialogFragment f =
new RssFeedDeleteFeedDialogFragment();
f.setArguments(args);
return f;
}
@Override
public void onAttach(Context ctx) {
super.onAttach(ctx);
((BaseActivity) requireActivity()).getActivityComponent().inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
GroupId groupId = new GroupId(
requireNonNull(requireArguments().getByteArray(GROUP_ID)));
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(),
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_rss_remove_feed));
builder.setMessage(
getString(R.string.blogs_rss_remove_feed_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok,
(dialog, which) -> viewModel.removeFeed(groupId));
return builder.create();
}
}

View File

@@ -1,170 +0,0 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.api.feed.FeedManager;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
public class RssFeedImportActivity extends BriarActivity {
private static final Logger LOG =
Logger.getLogger(RssFeedImportActivity.class.getName());
private EditText urlInput;
private Button importButton;
private ProgressBar progressBar;
@Inject
@IoExecutor
Executor ioExecutor;
@Inject
@SuppressWarnings("WeakerAccess")
volatile FeedManager feedManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rss_feed_import);
urlInput = findViewById(R.id.urlInput);
urlInput.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) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisableImportButton();
}
});
urlInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) {
publish();
return true;
}
return false;
});
importButton = findViewById(R.id.importButton);
importButton.setOnClickListener(v -> publish());
progressBar = findViewById(R.id.progressBar);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
private void enableOrDisableImportButton() {
String url = urlInput.getText().toString();
importButton.setEnabled(validateAndNormaliseUrl(url) != null);
}
@Nullable
private String validateAndNormaliseUrl(String url) {
if (!Patterns.WEB_URL.matcher(url).matches()) return null;
try {
return new URL(url).toString();
} catch (MalformedURLException e) {
return null;
}
}
private void publish() {
// hide import button, show progress bar
importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
String url = validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError();
importFeed(url);
}
private void importFeed(String url) {
ioExecutor.execute(() -> {
try {
feedManager.addFeed(url);
feedImported();
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
importFailed();
}
});
}
private void feedImported() {
runOnUiThreadUnlessDestroyed(this::supportFinishAfterTransition);
}
private void importFailed() {
runOnUiThreadUnlessDestroyed(() -> {
// hide progress bar, show publish button
progressBar.setVisibility(GONE);
importButton.setVisibility(VISIBLE);
// show error dialog
AlertDialog.Builder builder =
new AlertDialog.Builder(RssFeedImportActivity.this,
R.style.BriarDialogTheme);
builder.setMessage(R.string.blogs_rss_feeds_import_error);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.try_again_button,
(dialog, which) -> publish());
AlertDialog dialog = builder.create();
dialog.show();
});
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.android.blog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedImportFailedDialogFragment extends DialogFragment {
final static String TAG = RssFeedImportFailedDialogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
static RssFeedImportFailedDialogFragment newInstance() {
return new RssFeedImportFailedDialogFragment();
}
@Override
public void onAttach(Context ctx) {
super.onAttach(ctx);
((BaseActivity) requireActivity()).getActivityComponent().inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder =
new AlertDialog.Builder(requireActivity(),
R.style.BriarDialogTheme);
builder.setMessage(R.string.blogs_rss_feeds_import_error);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.try_again_button,
(dialog, which) -> viewModel.retryImportFeed());
return builder.create();
}
}

View File

@@ -0,0 +1,124 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedImportFragment extends BaseFragment {
public static final String TAG = RssFeedImportFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
private EditText urlInput;
private Button importButton;
private ProgressBar progressBar;
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireActivity().setTitle(getString(R.string.blogs_rss_feeds_import));
View v = inflater.inflate(R.layout.fragment_rss_feed_import,
container, false);
urlInput = v.findViewById(R.id.urlInput);
urlInput.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) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisableImportButton();
}
});
urlInput.setOnEditorActionListener((view, actionId, event) -> {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) {
publish();
return true;
}
return false;
});
importButton = v.findViewById(R.id.importButton);
importButton.setOnClickListener(view -> publish());
progressBar = v.findViewById(R.id.progressBar);
viewModel.getIsImporting().observe(getViewLifecycleOwner(),
this::onIsImporting);
return v;
}
@Override
public String getUniqueTag() {
return TAG;
}
private void enableOrDisableImportButton() {
String url = urlInput.getText().toString();
importButton.setEnabled(viewModel.validateAndNormaliseUrl(url) != null);
}
private void publish() {
String url = viewModel
.validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError();
viewModel.importFeed(url);
}
private void onIsImporting(Boolean importing) {
if (importing) {
// show progress bar, hide import button
importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
} else {
// show publish button, hide progress bar
importButton.setVisibility(VISIBLE);
progressBar.setVisibility(GONE);
}
}
}

View File

@@ -1,178 +0,0 @@
package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.RssFeedAdapter.RssFeedListener;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
public class RssFeedManageActivity extends BriarActivity
implements RssFeedListener {
private static final Logger LOG =
Logger.getLogger(RssFeedManageActivity.class.getName());
private BriarRecyclerView list;
private RssFeedAdapter adapter;
@Inject
@SuppressWarnings("WeakerAccess")
volatile FeedManager feedManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rss_feed_manage);
adapter = new RssFeedAdapter(this, this);
list = findViewById(R.id.feedList);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
}
@Override
public void onStart() {
super.onStart();
loadFeeds();
}
@Override
public void onStop() {
super.onStop();
adapter.clear();
list.showProgressBar();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.rss_feed_manage_actions, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
} else if (item.getItemId() == R.id.action_rss_feeds_import) {
Intent i = new Intent(this, RssFeedImportActivity.class);
startActivity(i);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onFeedClick(Feed feed) {
Intent i = new Intent(this, BlogActivity.class);
i.putExtra(GROUP_ID, feed.getBlogId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
@Override
public void onDeleteClick(Feed feed) {
DialogInterface.OnClickListener okListener =
(dialog, which) -> deleteFeed(feed);
AlertDialog.Builder builder = new AlertDialog.Builder(this,
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_rss_remove_feed));
builder.setMessage(
getString(R.string.blogs_rss_remove_feed_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok,
okListener);
builder.show();
}
private void loadFeeds() {
int revision = adapter.getRevision();
runOnDbThread(() -> {
try {
displayFeeds(revision, feedManager.getFeeds());
} catch (DbException e) {
logException(LOG, WARNING, e);
onLoadError();
}
});
}
private void displayFeeds(int revision, List<Feed> feeds) {
runOnUiThreadUnlessDestroyed(() -> {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (feeds.isEmpty()) list.showData();
else adapter.addAll(feeds);
} else {
LOG.info("Concurrent update, reloading");
loadFeeds();
}
});
}
private void deleteFeed(Feed feed) {
runOnDbThread(() -> {
try {
feedManager.removeFeed(feed);
onFeedDeleted(feed);
} catch (DbException e) {
logException(LOG, WARNING, e);
onDeleteError();
}
});
}
private void onLoadError() {
runOnUiThreadUnlessDestroyed(() -> {
list.setEmptyText(R.string.blogs_rss_feeds_manage_error);
list.showData();
});
}
private void onFeedDeleted(Feed feed) {
runOnUiThreadUnlessDestroyed(() -> {
adapter.incrementRevision();
adapter.remove(feed);
});
}
private void onDeleteError() {
runOnUiThreadUnlessDestroyed(() -> Snackbar.make(list,
R.string.blogs_rss_feeds_manage_delete_error,
LENGTH_LONG).show());
}
}

View File

@@ -0,0 +1,123 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.feed.Feed;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.RssFeedAdapter.RssFeedListener;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedManageFragment extends BaseFragment
implements RssFeedListener {
public static final String TAG = RssFeedManageFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
private BriarRecyclerView list;
private final RssFeedAdapter adapter = new RssFeedAdapter(this);
public static RssFeedManageFragment newInstance() {
return new RssFeedManageFragment();
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireActivity().setTitle(R.string.blogs_rss_feeds);
View v = inflater.inflate(R.layout.fragment_rss_feed_manage,
container, false);
list = v.findViewById(R.id.feedList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
viewModel.getFeeds().observe(getViewLifecycleOwner(), result -> result
.onError(e -> {
list.setEmptyText(R.string.blogs_rss_feeds_manage_error);
list.showData();
})
.onSuccess(feeds -> {
adapter.submitList(feeds);
if (requireNonNull(feeds).size() == 0) {
list.showData();
}
})
);
return v;
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.rss_feed_manage_actions, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
requireActivity().onBackPressed();
return true;
} else if (item.getItemId() == R.id.action_rss_feeds_import) {
showNextFragment(new RssFeedImportFragment());
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onFeedClick(Feed feed) {
Intent i = new Intent(getActivity(), BlogActivity.class);
i.putExtra(GROUP_ID, feed.getBlogId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
@Override
public void onDeleteClick(Feed feed) {
RssFeedDeleteFeedDialogFragment dialog =
RssFeedDeleteFeedDialogFragment.newInstance(feed.getBlogId());
dialog.show(getParentFragmentManager(),
RssFeedDeleteFeedDialogFragment.TAG);
}
}

View File

@@ -0,0 +1,180 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import android.util.Patterns;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED;
@NotNullByDefault
class RssFeedViewModel extends DbViewModel {
enum ImportResult {IMPORTED, FAILED, EXISTS}
private static final Logger LOG =
getLogger(RssFeedViewModel.class.getName());
private final FeedManager feedManager;
private final Executor ioExecutor;
private final Executor dbExecutor;
private final MutableLiveData<LiveResult<List<Feed>>> feeds =
new MutableLiveData<>();
@Nullable
private volatile String urlFailedImport = null;
private final MutableLiveData<Boolean> isImporting =
new MutableLiveData<>(false);
private final MutableLiveEvent<ImportResult> importResult =
new MutableLiveEvent<>();
@Inject
RssFeedViewModel(Application app,
FeedManager feedManager,
@IoExecutor Executor ioExecutor,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.feedManager = feedManager;
this.ioExecutor = ioExecutor;
this.dbExecutor = dbExecutor;
loadFeeds();
}
@Nullable
String validateAndNormaliseUrl(String url) {
if (!Patterns.WEB_URL.matcher(url).matches()) return null;
try {
return new URL(url).toString();
} catch (MalformedURLException e) {
return null;
}
}
LiveData<LiveResult<List<Feed>>> getFeeds() {
return feeds;
}
private void loadFeeds() {
loadFromDb(this::loadFeeds, feeds::setValue);
}
@DatabaseExecutor
private List<Feed> loadFeeds(Transaction txn) throws DbException {
long start = now();
List<Feed> feeds = feedManager.getFeeds(txn);
Collections.sort(feeds);
logDuration(LOG, "Loading feeds", start);
return feeds;
}
void removeFeed(GroupId groupId) {
dbExecutor.execute(() -> {
List<Feed> updated = removeListItems(getList(feeds), feed -> {
if (feed.getBlogId().equals(groupId)) {
try {
feedManager.removeFeed(feed);
return true;
} catch (DbException e) {
handleException(e);
}
}
return false;
});
if (updated != null) {
feeds.postValue(new LiveResult<>(updated));
}
});
}
LiveEvent<ImportResult> getImportResult() {
return importResult;
}
LiveData<Boolean> getIsImporting() {
return isImporting;
}
void importFeed(String url) {
isImporting.setValue(true);
urlFailedImport = null;
ioExecutor.execute(() -> {
try {
if (exists(url)) {
importResult.postEvent(EXISTS);
return;
}
Feed feed = feedManager.addFeed(url);
List<Feed> updated = addListItem(getList(feeds), feed);
if (updated != null) {
Collections.sort(updated);
feeds.postValue(new LiveResult<>(updated));
}
importResult.postEvent(IMPORTED);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
urlFailedImport = url;
importResult.postEvent(FAILED);
} finally {
isImporting.postValue(false);
}
});
}
void retryImportFeed() {
if (urlFailedImport == null) {
throw new AssertionError();
}
importFeed(urlFailedImport);
}
private boolean exists(String url) {
List<Feed> list = getList(feeds);
if (list != null) {
for (Feed feed : list) {
if (url.equals(feed.getUrl())) {
return true;
}
}
}
return false;
}
}

View File

@@ -73,6 +73,7 @@ public class AddNearbyContactIntroFragment extends BaseFragment {
scrollView = v.findViewById(R.id.scrollView);
View button = v.findViewById(R.id.continueButton);
button.setOnClickListener(view -> {
viewModel.stopDiscovery();
viewModel.onContinueClicked();
if (permissionManager.checkPermissions()) {
viewModel.showQrCodeFragmentIfAllowed();

View File

@@ -44,6 +44,7 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.plugin.bluetooth.BluetoothPlugin;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeFinished;
import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult.Error;
@@ -149,7 +150,9 @@ class AddNearbyContactViewModel extends AndroidViewModel
@Nullable
private final BluetoothAdapter bt;
@Nullable // UiThread
private Plugin wifiPlugin, bluetoothPlugin;
private Plugin wifiPlugin;
@Nullable // UiThread
private BluetoothPlugin bluetoothPlugin;
// UiThread
private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
@@ -195,7 +198,8 @@ class AddNearbyContactViewModel extends AndroidViewModel
this.connectionManager = connectionManager;
bt = BluetoothAdapter.getDefaultAdapter();
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
bluetoothPlugin = (BluetoothPlugin) pluginManager
.getPlugin(BluetoothConstants.ID);
qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
eventBus.addListener(this);
IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
@@ -218,7 +222,8 @@ class AddNearbyContactViewModel extends AndroidViewModel
@UiThread
void resetPlugins() {
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
bluetoothPlugin = (BluetoothPlugin) pluginManager
.getPlugin(BluetoothConstants.ID);
}
@UiThread
@@ -375,6 +380,13 @@ class AddNearbyContactViewModel extends AndroidViewModel
}
}
void stopDiscovery() {
if (!isBluetoothSupported() || !bluetoothPlugin.isDiscovering()) {
return;
}
bluetoothPlugin.stopDiscoverAndConnect();
}
@SuppressWarnings("StatementWithEmptyBody")
@UiThread
void showQrCodeFragmentIfAllowed() {

View File

@@ -6,23 +6,30 @@ import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.widget.Toast;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.plugin.bluetooth.BluetoothPlugin;
import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.ContactItem;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
@@ -31,17 +38,24 @@ import androidx.appcompat.app.AlertDialog;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
class BluetoothConnecter {
class BluetoothConnecter implements EventListener {
private final Logger LOG = getLogger(BluetoothConnecter.class.getName());
private final long BT_ACTIVE_TIMEOUT = SECONDS.toMillis(5);
private enum Permission {
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
}
@@ -52,32 +66,41 @@ class BluetoothConnecter {
private final AndroidExecutor androidExecutor;
private final ConnectionRegistry connectionRegistry;
private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
private final EventBus eventBus;
private final TransportPropertyManager transportPropertyManager;
private final ConnectionManager connectionManager;
private volatile Plugin bluetoothPlugin;
private volatile BluetoothPlugin bluetoothPlugin;
private Permission locationPermission = Permission.UNKNOWN;
private ContactId contactId = null;
@Inject
BluetoothConnecter(Application app,
PluginManager pluginManager,
@IoExecutor Executor ioExecutor,
AndroidExecutor androidExecutor,
ConnectionRegistry connectionRegistry) {
ConnectionRegistry connectionRegistry,
EventBus eventBus,
TransportPropertyManager transportPropertyManager,
ConnectionManager connectionManager) {
this.app = app;
this.pluginManager = pluginManager;
this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor;
this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
this.bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID);
this.connectionRegistry = connectionRegistry;
this.eventBus = eventBus;
this.transportPropertyManager = transportPropertyManager;
this.connectionManager = connectionManager;
}
boolean isConnectedViaBluetooth(ContactId contactId) {
return connectionRegistry.isConnected(contactId, BluetoothConstants.ID);
return connectionRegistry.isConnected(contactId, ID);
}
boolean isDiscovering() {
// TODO bluetoothPlugin.isDiscovering()
return false;
return bluetoothPlugin.isDiscovering();
}
/**
@@ -89,7 +112,7 @@ class BluetoothConnecter {
// When this class is instantiated before we are logged in
// (like when returning to a killed activity), bluetoothPlugin would be
// null and we consider bluetooth not supported. So reset here.
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID);
}
@UiThread
@@ -149,30 +172,84 @@ class BluetoothConnecter {
@UiThread
void onBluetoothDiscoverable(ContactItem contact) {
connect(contact.getContact().getId());
contactId = contact.getContact().getId();
connect();
}
private void connect(ContactId contactId) {
// TODO
// * enable bluetooth connections setting, if not enabled
// * wait for plugin to become active
ioExecutor.execute(() -> {
Random r = new Random();
try {
showToast(R.string.toast_connect_via_bluetooth_start);
// TODO do real work here
Thread.sleep(r.nextInt(3000) + 3000);
if (r.nextBoolean()) {
showToast(R.string.toast_connect_via_bluetooth_success);
} else {
showToast(R.string.toast_connect_via_bluetooth_error);
@Override
public void eventOccurred(@NonNull Event e) {
if (e instanceof ConnectionOpenedEvent) {
ConnectionOpenedEvent c = (ConnectionOpenedEvent) e;
if (c.getContactId().equals(contactId) && c.isIncoming() &&
c.getTransportId() == ID) {
if (bluetoothPlugin != null) {
bluetoothPlugin.stopDiscoverAndConnect();
}
} catch (InterruptedException e) {
logException(LOG, WARNING, e);
LOG.info("Contact connected to us");
showToast(R.string.toast_connect_via_bluetooth_success);
}
}
}
private void connect() {
pluginManager.setPluginEnabled(ID, true);
ioExecutor.execute(() -> {
if (!waitForBluetoothActive()) {
showToast(R.string.bt_plugin_status_inactive);
LOG.warning("Bluetooth plugin didn't become active");
return;
}
showToast(R.string.toast_connect_via_bluetooth_start);
eventBus.addListener(this);
try {
String uuid = null;
try {
uuid = transportPropertyManager
.getRemoteProperties(contactId, ID).get(PROP_UUID);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
if (isNullOrEmpty(uuid)) {
LOG.warning("PROP_UUID missing for contact");
return;
}
DuplexTransportConnection conn = bluetoothPlugin
.discoverAndConnectForSetup(uuid);
if (conn == null) {
if (!isConnectedViaBluetooth(contactId)) {
LOG.warning("Failed to connect");
showToast(R.string.toast_connect_via_bluetooth_error);
} else {
LOG.info("Failed to connect, but contact connected");
}
return;
}
connectionManager.manageOutgoingConnection(contactId, ID, conn);
showToast(R.string.toast_connect_via_bluetooth_success);
} finally {
eventBus.removeListener(this);
}
});
}
private boolean waitForBluetoothActive() {
long left = BT_ACTIVE_TIMEOUT;
final long sleep = 250;
try {
while (left > 0) {
if (bluetoothPlugin.getState() == ACTIVE) {
return true;
}
Thread.sleep(sleep);
left -= sleep;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return (bluetoothPlugin.getState() == ACTIVE);
}
private void showToast(@StringRes int res) {
androidExecutor.runOnUiThread(() ->
Toast.makeText(app, res, Toast.LENGTH_LONG).show()

View File

@@ -91,7 +91,7 @@ public class BluetoothConnecterDialogFragment extends DialogFragment {
return;
}
if (bluetoothConnecter.isDiscovering()) {
// TODO showToast(R.string.toast_connect_via_bluetooth_discovering);
showToast(R.string.toast_connect_via_bluetooth_already_discovering);
dismiss();
}
}

View File

@@ -53,6 +53,7 @@ import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.removabledrive.RemovableDriveActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.ImagePreview;
@@ -420,6 +421,11 @@ public class ConversationActivity extends BriarActivity
} else if (itemId == R.id.action_social_remove_person) {
askToRemoveContact();
return true;
} else if (itemId == R.id.action_removable_drive_write) {
Intent intent = new Intent(this, RemovableDriveActivity.class);
intent.putExtra(CONTACT_ID, contactId.getInt());
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}

View File

@@ -142,6 +142,7 @@ class ConversationAdapter
}
items.beginBatchedUpdates();
for (ConversationItem item : toRemove) items.remove(item);
updateTimersInBatch();
items.endBatchedUpdates();
}

View File

@@ -0,0 +1,174 @@
package org.briarproject.briar.android.removabledrive;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.TextView;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask.State;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
// TODO 19 will be our requirement for sneakernet support, right. The file apis
// used require this.
@RequiresApi(api = 19)
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RemovableDriveActivity extends BriarActivity
implements BaseFragmentListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
private RemovableDriveViewModel viewModel;
private TextView text;
private Button writeButton;
private Button readButton;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(RemovableDriveViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_removable_drive);
text = findViewById(R.id.sneaker_text);
writeButton = findViewById(R.id.sneaker_write);
readButton = findViewById(R.id.sneaker_read);
Intent intent = getIntent();
int contactId = intent.getIntExtra(CONTACT_ID, -1);
if (contactId == -1) {
writeButton.setEnabled(false);
readButton.setEnabled(false);
return;
}
// TODO we can pass an extra named DocumentsContract.EXTRA_INITIAL_URI
// to have the filepicker start on the usb-stick -- if get hold of URI
// of the same. USB manager API?
// Overall, passing this extra requires extending the ready-made
// contracts and overriding createIntent.
writeButton.setText("Write for contactId " + contactId);
ActivityResultLauncher<String> createDocument =
registerForActivityResult(
new ActivityResultContracts.CreateDocument(),
uri -> write(contactId, uri));
writeButton.setOnClickListener(
v -> createDocument.launch(viewModel.getFileName()));
readButton.setText("Read for contactId " + contactId);
ActivityResultLauncher<String> getContent =
registerForActivityResult(
new ActivityResultContracts.GetContent(),
uri -> read(contactId, uri));
readButton.setOnClickListener(
v -> getContent.launch("application/octet-stream"));
LiveData<State> state;
state = viewModel.ongoingWrite(new ContactId(contactId));
if (state == null) {
writeButton.setEnabled(true);
} else {
say("\nOngoing write:");
writeButton.setEnabled(false);
state.observe(this, (taskState) -> handleState("write", taskState));
}
state = viewModel.ongoingRead(new ContactId(contactId));
if (state == null) {
readButton.setEnabled(true);
} else {
say("\nOngoing read:");
readButton.setEnabled(false);
state.observe(this, (taskState) -> handleState("read", taskState));
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void write(int contactId, @Nullable Uri uri) {
if (contactId == -1) {
throw new IllegalStateException();
}
if (uri == null) {
say("no URI picked for write");
return;
}
say("\nWriting to URI: " + uri);
writeButton.setEnabled(false);
LiveData<State> state = viewModel.write(new ContactId(contactId), uri);
state.observe(this, (taskState) -> handleState("write", taskState));
}
private void read(int contactId, @Nullable Uri uri) {
if (contactId == -1) {
throw new IllegalStateException();
}
if (uri == null) {
say("no URI picked for read");
return;
}
say("\nReading from URI: " + uri);
readButton.setEnabled(false);
LiveData<State> state = viewModel.read(new ContactId(contactId), uri);
state.observe(this, (taskState) -> handleState("read", taskState));
}
private void handleState(String action, State taskState) {
say(String.format(Locale.getDefault(),
"%s: bytes done: %d of %d. %s. %s.",
action, taskState.getDone(), taskState.getTotal(),
taskState.isFinished() ? "Finished" : "Ongoing",
taskState.isFinished() ?
(taskState.isSuccess() ? "Success" : "Failed") : ".."));
if (taskState.isFinished()) {
if (action.equals("write")) {
writeButton.setEnabled(true);
} else if (action.equals("read")) {
readButton.setEnabled(true);
}
}
}
private void say(String txt) {
String time = new SimpleDateFormat("HH:mm:ss", Locale.getDefault())
.format(new Date());
txt = String.format("%s %s\n", time, txt);
text.setText(text.getText().toString().concat(txt));
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.briar.android.removabledrive;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
public interface RemovableDriveModule {
@Binds
@IntoMap
@ViewModelKey(RemovableDriveViewModel.class)
ViewModel bindRemovableDriveViewModel(RemovableDriveViewModel removableDriveViewModel);
}

View File

@@ -0,0 +1,101 @@
package org.briarproject.briar.android.removabledrive;
import android.app.Application;
import android.net.Uri;
import org.briarproject.bramble.api.Consumer;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask;
import org.briarproject.bramble.api.plugin.file.RemovableDriveTask.State;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.Locale.US;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.PROP_URI;
@NotNullByDefault
class RemovableDriveViewModel extends AndroidViewModel {
private static final Logger LOG =
getLogger(RemovableDriveViewModel.class.getName());
private final RemovableDriveManager manager;
private final ConcurrentHashMap<Consumer<State>, RemovableDriveTask>
observers = new ConcurrentHashMap<>();
@Inject
RemovableDriveViewModel(Application app,
RemovableDriveManager removableDriveManager) {
super(app);
this.manager = removableDriveManager;
}
String getFileName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS", US);
return sdf.format(new Date());
}
LiveData<State> write(ContactId contactId, Uri uri) {
TransportProperties p = new TransportProperties();
p.put(PROP_URI, uri.toString());
return observe(manager.startWriterTask(contactId, p));
}
LiveData<State> read(ContactId contactId, Uri uri) {
TransportProperties p = new TransportProperties();
p.put(PROP_URI, uri.toString());
return observe(manager.startReaderTask(contactId, p));
}
@Nullable
LiveData<State> ongoingWrite(ContactId contactId) {
RemovableDriveTask task = manager.getCurrentWriterTask(contactId);
if (task == null) {
return null;
}
return observe(task);
}
@Nullable
LiveData<State> ongoingRead(ContactId contactId) {
RemovableDriveTask task = manager.getCurrentReaderTask(contactId);
if (task == null) {
return null;
}
return observe(task);
}
private LiveData<State> observe(RemovableDriveTask task) {
MutableLiveData<State> state = new MutableLiveData<>();
Consumer<State> observer = state::postValue;
task.addObserver(observer);
observers.put(observer, task);
return state;
}
@Override
protected void onCleared() {
for (Map.Entry<Consumer<State>, RemovableDriveTask> entry
: observers.entrySet()) {
entry.getValue().removeObserver(entry.getKey());
}
}
}

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
tools:context=".android.removabledrive.RemovableDriveActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<Button
android:id="@+id/sneaker_write"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:enabled="false"
android:text=""
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:enabled="true" />
<Button
android:id="@+id/sneaker_read"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:enabled="false"
android:text=""
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/sneaker_write"
tools:enabled="true" />
<TextView
android:id="@+id/sneaker_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large"
android:textSize="12sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/sneaker_read" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -5,8 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
tools:context=".android.blog.RssFeedImportActivity">
android:padding="@dimen/margin_medium">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<org.briarproject.briar.android.view.BriarRecyclerView 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:id="@+id/feedList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:emptyText="@string/blogs_rss_feeds_manage_empty_state"
app:scrollToEnd="false"
tools:listitem="@layout/list_item_rss_feed" />

View File

@@ -10,13 +10,8 @@
app:showAsAction="always"/>
<item
android:id="@+id/action_rss_feeds_import"
android:title="@string/blogs_rss_feeds_import"
app:showAsAction="never"/>
<item
android:id="@+id/action_rss_feeds_manage"
android:title="@string/blogs_rss_feeds_manage"
android:id="@+id/action_rss_feeds"
android:title="@string/blogs_rss_feeds"
app:showAsAction="never"/>
</menu>

View File

@@ -40,4 +40,9 @@
android:title="@string/delete_contact"
app:showAsAction="never" />
<item
android:id="@+id/action_removable_drive_write"
android:title="Transfer via removable drive"
app:showAsAction="never" />
</menu>

View File

@@ -461,14 +461,12 @@
<string name="blogs_rss_feeds_import_button">إستيراد</string>
<string name="blogs_rss_feeds_import_hint">ادخال رابط تحديثات RSS</string>
<string name="blogs_rss_feeds_import_error">معذرة! حدث خطأ في استيراد التحديثات.</string>
<string name="blogs_rss_feeds_manage">إدارة تحديثات RSS</string>
<string name="blogs_rss_feeds_manage_imported">تم استيراد:</string>
<string name="blogs_rss_feeds_manage_author">المؤلف/ة:</string>
<string name="blogs_rss_feeds_manage_updated">آخر تحديث:</string>
<string name="blogs_rss_remove_feed">ازالة الخلاصة</string>
<string name="blogs_rss_remove_feed_dialog_message">هل أنت متأكد/ة من رغبتك في حذف هذه الخلاصة؟\n\nالمنشورات ستحذف من جهازك وليس من أجهزة الآخرين.\n\nأي جهة اتصال قمت/ي بمشاركة هذه الخلاصة معها قد لا تتمكن من استلام التحديثات.</string>
<string name="blogs_rss_remove_feed_ok">حذف</string>
<string name="blogs_rss_feeds_manage_delete_error">لا يمكن حذف الخلاصة!</string>
<string name="blogs_rss_feeds_manage_empty_state">لا خلاصات RSS للعرض\n\nالرجاء لمس علامة + لإستيراد خلاصة.</string>
<string name="blogs_rss_feeds_manage_error">حدث خطأ في جلب خلاصاتك. الرجاء المحاولة لاحقًا.</string>
<!--Settings Profile Picture-->

View File

@@ -348,13 +348,11 @@
<string name="blogs_rss_feeds_import_button">İdxal</string>
<string name="blogs_rss_feeds_import_hint">RSS kanalın linkini daxil edin</string>
<string name="blogs_rss_feeds_import_error">Üzr istəyirik! Feed-inizdə idxal bir xəta baş verdi.</string>
<string name="blogs_rss_feeds_manage">RSS Feeds idarəetmə</string>
<string name="blogs_rss_feeds_manage_imported">İdxal olundu:</string>
<string name="blogs_rss_feeds_manage_author">Müəllif:</string>
<string name="blogs_rss_feeds_manage_updated">Son yeniləmə:</string>
<string name="blogs_rss_remove_feed">Feed\'i sil</string>
<string name="blogs_rss_remove_feed_ok">Sil</string>
<string name="blogs_rss_feeds_manage_delete_error">Feed silinmədi</string>
<string name="blogs_rss_feeds_manage_empty_state">Xeyr RSS əks etdirmir\n\nİdxal etmək üçün + düyməsinə toxunun</string>
<string name="blogs_rss_feeds_manage_error">Kanal yükləmədə bir problem var. Zəhmət olmasa bir az sonra yenə cəhd edin.</string>
<!--Settings Profile Picture-->

View File

@@ -238,13 +238,11 @@
<string name="blogs_rss_feeds_import_button">Внасяне</string>
<string name="blogs_rss_feeds_import_hint">Въведете URL адреса на RSS емисията</string>
<string name="blogs_rss_feeds_import_error">Възникна грешка при внасянето на емисия.</string>
<string name="blogs_rss_feeds_manage">Управление на RSS емисии</string>
<string name="blogs_rss_feeds_manage_imported">Внесена:</string>
<string name="blogs_rss_feeds_manage_author">Автор:</string>
<string name="blogs_rss_feeds_manage_updated">Последно актуализиране:</string>
<string name="blogs_rss_remove_feed">Премахване на емисия</string>
<string name="blogs_rss_remove_feed_ok">Премахване</string>
<string name="blogs_rss_feeds_manage_delete_error">Емисията не можа да бъде изтрита!</string>
<string name="blogs_rss_feeds_manage_error">Възникна проблем при зареждането на емисиите ви. Моля, опитайте пак по-късно.</string>
<!--Settings Display-->
<!--Settings Network-->

View File

@@ -332,14 +332,12 @@
<string name="blogs_rss_feeds_import_button">Uvezi</string>
<string name="blogs_rss_feeds_import_hint">Unesi URL od RSS kanala</string>
<string name="blogs_rss_feeds_import_error">Žao nam je! Došlo je do greške pri unosu vašeg kanala.</string>
<string name="blogs_rss_feeds_manage">Upravljanje RSS kanalima</string>
<string name="blogs_rss_feeds_manage_imported">Uvezeno:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Zadnje ažuriranje:</string>
<string name="blogs_rss_remove_feed">Uklonite kanal</string>
<string name="blogs_rss_remove_feed_dialog_message">Jeste li sigurni da želite da uklonite kanal?\n\nPostovi će biti uklonjeni sa vašeg uređaja ali ne is uređaja drugih ljudi.\n\nKontakti kojima ste podijelili ovaj blog će možda prestati da dobijaju novosti.</string>
<string name="blogs_rss_remove_feed_ok">Ukloni</string>
<string name="blogs_rss_feeds_manage_delete_error">Kanal nije bilo moguće ukloniti!</string>
<string name="blogs_rss_feeds_manage_empty_state">Nema RSS kanala za prikazivanje\n\nDotaknite + ikonu da uvezete kanal</string>
<string name="blogs_rss_feeds_manage_error">Došlo je do problema pri učitavanju vaših kanala. Probajte opet kasnije.</string>
<!--Settings Profile Picture-->

View File

@@ -421,14 +421,12 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="blogs_rss_feeds_import_button">Subscriu-me</string>
<string name="blogs_rss_feeds_import_hint">Escriviu l\'URL del canal de notícies RSS</string>
<string name="blogs_rss_feeds_import_error">Ens sap greu! S\'ha produït un error en subscriure-us al vostre canal de notícies.</string>
<string name="blogs_rss_feeds_manage">Gestiona els canals de notícies RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importat:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Darrera actualització:</string>
<string name="blogs_rss_remove_feed">Suprimeix la subscripció al canal de notícies</string>
<string name="blogs_rss_remove_feed_dialog_message">Segur que voleu suprimir la subscripció a aquest canal de notícies?\n\nLes notícies d\'aquest canal s\'eliminaran del vostre dispositiu però no del d\'altres persones.\n\nEls contactes amb els que hàgeu compartit aquest canal poden deixar de rebre les actualitzacions.</string>
<string name="blogs_rss_remove_feed_ok">Suprimeix la subscripció</string>
<string name="blogs_rss_feeds_manage_delete_error">La subscripció al canal de notícies no s\'ha pogut suprimir.</string>
<string name="blogs_rss_feeds_manage_empty_state">No hi ha cap notícia per mostrar\n\nFeu un toc sobre la icona + per subscriure-us a un canal de notícies</string>
<string name="blogs_rss_feeds_manage_error">S\'ha produït un problema en actualitzar els vostres canals de notícies. Torneu-ho a provar més endavant.</string>
<!--Settings Profile Picture-->

View File

@@ -302,13 +302,11 @@
<string name="blogs_rss_feeds_import_button">Import</string>
<string name="blogs_rss_feeds_import_hint">Zadejte URL adresu RSS kanálu</string>
<string name="blogs_rss_feeds_import_error">Omlouváme se! Vyskytla se chyba při importu vašeho kanálu.</string>
<string name="blogs_rss_feeds_manage">Správa RSS kanálů</string>
<string name="blogs_rss_feeds_manage_imported">Importováno:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Naposledy aktualizováno:</string>
<string name="blogs_rss_remove_feed">Odstranit kanál</string>
<string name="blogs_rss_remove_feed_ok">Odstranit</string>
<string name="blogs_rss_feeds_manage_delete_error">Kanál nemohl být odstraněn !</string>
<string name="blogs_rss_feeds_manage_empty_state">Žádné RSS kanály k zobrazení\n\nKlikněte na ikonu + pro nahrání příspěvků</string>
<string name="blogs_rss_feeds_manage_error">Vyskytl se problém s načtením vašeho kanálu příspěvků. Zkuste to prosím později.</string>
<!--Settings Display-->

View File

@@ -141,7 +141,7 @@
<string name="fix">Behoben</string>
<string name="help">Hilfe</string>
<string name="sorry">Entschuldigung</string>
<string name="error_start_activity">Nicht Verfügbar für dein System</string>
<string name="error_start_activity">Nicht verfügbar für dein System</string>
<string name="status_heading">Status:</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Keine Kontakte vorhanden</string>
@@ -162,6 +162,7 @@
<string name="menu_item_connect_via_bluetooth">Über Bluetooth verbinden</string>
<string name="dialog_title_connect_via_bluetooth">Über Bluetooth verbinden</string>
<string name="dialog_message_connect_via_bluetooth">Dein Kontakt muss in der Nähe sein, damit dies funktioniert.\n\nDu und dein Kontakt sollten beide gleichzeitig \"Start\" drücken.</string>
<string name="toast_connect_via_bluetooth_already_discovering">Versucht bereits, eine Verbindung über Bluetooth herzustellen</string>
<string name="toast_connect_via_bluetooth_not_discoverable">Kann ohne Bluetooth nicht fortgesetzt werden</string>
<string name="toast_connect_via_bluetooth_no_location_permission">Kann ohne Standortberechtigung nicht fortgesetzt werden</string>
<string name="toast_connect_via_bluetooth_start">Verbinde über Bluetooth…</string>
@@ -457,14 +458,12 @@
<string name="blogs_rss_feeds_import_button">Importieren</string>
<string name="blogs_rss_feeds_import_hint">URL des RSS-Feeds eingeben</string>
<string name="blogs_rss_feeds_import_error">Es tut uns Leid! Es gab einen Fehler beim Importieren deines Feeds.</string>
<string name="blogs_rss_feeds_manage">RSS-Feeds verwalten</string>
<string name="blogs_rss_feeds_manage_imported">Importiert:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Letzte Aktualisierung:</string>
<string name="blogs_rss_remove_feed">Feed entfernen</string>
<string name="blogs_rss_remove_feed_dialog_message">Bist du sicher, dass du diesen Feed löschen willst?\n\nBeiträge werden von deinem Gerät entfernt, aber nicht von den Geräten anderer Personen.\n\nAlle Kontakte, für die du diesen Feed freigegeben hast, können keine Updates mehr erhalten.</string>
<string name="blogs_rss_remove_feed_ok">Aufheben</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">Keine RSS-Feeds vorhanden\n\nTippe auf das + Symbol, um einen Feed zu importieren</string>
<string name="blogs_rss_feeds_manage_error">Es gab ein Problem beim Laden deiner Feeds. Bitte versuche es später erneut.</string>
<!--Settings Profile Picture-->

View File

@@ -162,6 +162,7 @@
<string name="menu_item_connect_via_bluetooth">Conectar mediante Bluetooth</string>
<string name="dialog_title_connect_via_bluetooth">Conectar mediante Bluetooth</string>
<string name="dialog_message_connect_via_bluetooth">Tu contacto necesita estar cerca para que esto funcione.\n\nTú y tu contacto deberían presionar \"Iniciar\" ambos al mismo tiempo.</string>
<string name="toast_connect_via_bluetooth_already_discovering">Ya se está intentando conectar mediante Bluetooth</string>
<string name="toast_connect_via_bluetooth_not_discoverable">No se puede continuar sin Bluetooth</string>
<string name="toast_connect_via_bluetooth_no_location_permission">No se puede continuar sin permiso de ubicación</string>
<string name="toast_connect_via_bluetooth_start">Conectar mediante Bluetooth...</string>
@@ -457,14 +458,12 @@
<string name="blogs_rss_feeds_import_button">Importar</string>
<string name="blogs_rss_feeds_import_hint">Introduce la URL del canal RSS</string>
<string name="blogs_rss_feeds_import_error">¡Lo sentimos! Hubo un error importando tu canal.</string>
<string name="blogs_rss_feeds_manage">Administrar canales RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importado:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Última actualización:</string>
<string name="blogs_rss_remove_feed">Eliminar canal RSS</string>
<string name="blogs_rss_remove_feed_dialog_message">¿Estás seguro de que quieres quitar este canal RSS?\n\nLos mensajes se eliminarán de tu dispositivo, pero no de los dispositivos de otras personas.\n\nEs posible que los contactos con los que hayas compartido este canal dejen de recibir actualizaciones.</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_feeds_manage_delete_error">¡El canal no pudo ser eliminado!</string>
<string name="blogs_rss_feeds_manage_empty_state">No hay canales RSS que mostrar\n\nGolpea el icono + para importar uno</string>
<string name="blogs_rss_feeds_manage_error">Hubo un problema cargando tus canales RSS. Por favor, prueba más tarde.</string>
<!--Settings Profile Picture-->

View File

@@ -395,14 +395,12 @@
<string name="blogs_rss_feeds_import_button">Inportatu</string>
<string name="blogs_rss_feeds_import_hint">Sartu RSS jarioaren URLa</string>
<string name="blogs_rss_feeds_import_error">Sentitzen dugu! Zure jarioa inportatzean errore bat gertatu da.</string>
<string name="blogs_rss_feeds_manage">Kudeatu RSS jarioak</string>
<string name="blogs_rss_feeds_manage_imported">Inportatuta:</string>
<string name="blogs_rss_feeds_manage_author">Egilea:</string>
<string name="blogs_rss_feeds_manage_updated">Azken eguneratzea:</string>
<string name="blogs_rss_remove_feed">Kendu jarioa</string>
<string name="blogs_rss_remove_feed_dialog_message">Ziur zaude jario hau kendu nahi duzula?\n\nSarrerak zure gailutik kenduko dira baina ez besteen gailuetatik.\n\nJario hau beste inorekin partekatu baduzu agian eguneratzeak jasotzeari utziko diote.</string>
<string name="blogs_rss_remove_feed_ok">Kendu</string>
<string name="blogs_rss_feeds_manage_delete_error">Ezin izan da jarioa ezabatu!</string>
<string name="blogs_rss_feeds_manage_empty_state">Ez dago erakusteko RSS jariorik\n\nSakatu + ikonoa jario bat inportatzeko</string>
<string name="blogs_rss_feeds_manage_error">Arazo bat egon da zure jarioak kargatzean. Saiatu berriro geroago.</string>
<!--Settings Profile Picture-->

View File

@@ -24,6 +24,9 @@
<string name="setup_huawei_text">لطفا روی دکمه زیر کلیک کنید و مطمئن شوید که از Briar (برایر) در صفحه \"برنامه های محافظت شده\" محافظت می شود.</string>
<string name="setup_huawei_button">حفاظت از Briar (برایر)</string>
<string name="setup_huawei_help">اگر Briar (برایر) به فهرست برنامه های محافظت شده اضافه نشده، نمی تواند در پس زمینه مشغول به کار باشد.</string>
<string name="setup_huawei_app_launch_text">لطفا روی دکمه زیر زده، صفحه‌ی \"راه اندازی برنامه\" را باز کرده و از این که برایر (Briar) بر روی \"مدیریت دستی\" تنظیم شده باشد، اطمینان حاصل کنید.</string>
<string name="setup_huawei_app_launch_button">باز کردن تنظیمات باتری</string>
<string name="setup_huawei_app_launch_help">اگر در صفحه \"راه اندازی برنامه\"، برایر (Briar) بر روی گزینه \"مدیریت دستی\" تنظیم نشده باشد، برنامه قادر به فعالیت در پس‌زمینه نخواهد بود.</string>
<string name="warning_dozed">ناتوانی %s برای اجراء در پس زمینه</string>
<!--Login-->
<string name="enter_password">گذرواژه</string>
@@ -165,6 +168,7 @@
<string name="menu_item_connect_via_bluetooth">اتصال از طریق بلوتوث</string>
<string name="dialog_title_connect_via_bluetooth">اتصال از طریق بلوتوث</string>
<string name="dialog_message_connect_via_bluetooth">برای امکان این عملکرد، مخاطب شما باید نزدیک باشد. \n\n شما و مخاطبتان باید همزمان گزینه‌ی \"شروع\" را بفشارید.</string>
<string name="toast_connect_via_bluetooth_already_discovering">در حال تلاش برای اتصال از طریق بلوتوث</string>
<string name="toast_connect_via_bluetooth_not_discoverable">امکان ادامه بدون بلوتوث وجود ندارد</string>
<string name="toast_connect_via_bluetooth_no_location_permission">امکان ادامه بدون اجازه مکان‌یابی وجود ندارد</string>
<string name="toast_connect_via_bluetooth_start">در حال اتصال از طریق بلوتوث</string>
@@ -488,7 +492,6 @@
<string name="blogs_rss_feeds_import_button">وارد کردن</string>
<string name="blogs_rss_feeds_import_hint">آدرس خوراک RSS را وارد کنید</string>
<string name="blogs_rss_feeds_import_error">متاسفیم! وارد کردن خوراک شما با خطا مواجه شده است.</string>
<string name="blogs_rss_feeds_manage">مدیریت خوراک های RSS</string>
<string name="blogs_rss_feeds_manage_imported">وارد شده:</string>
<string name="blogs_rss_feeds_manage_author">نویسنده:</string>
<string name="blogs_rss_feeds_manage_updated">آخرین به روز رسانی:</string>
@@ -499,7 +502,6 @@
هر مخاطبی که با آن این خوراک را به اشتراک گذاشته اید ممکن است دیگر آپدیت دریافت نکند.</string>
<string name="blogs_rss_remove_feed_ok">حذف</string>
<string name="blogs_rss_feeds_manage_delete_error">خوراک نمی تواند پاک شود!</string>
<string name="blogs_rss_feeds_manage_empty_state">هیچ خوراک RSS برای نمایش وجود ندارد
برای وارد کردن خوراک روی آیکون + ضربه بزنید</string>

View File

@@ -314,14 +314,12 @@
<string name="blogs_rss_feeds_import_button">Tuo</string>
<string name="blogs_rss_feeds_import_hint">Syötä RSS syötteen URL osoite</string>
<string name="blogs_rss_feeds_import_error">Pahoittelemme! Syötteen noutamisessa tapahtui virhe.</string>
<string name="blogs_rss_feeds_manage">Muokkaa RSS syötteitä</string>
<string name="blogs_rss_feeds_manage_imported">Tuotu:</string>
<string name="blogs_rss_feeds_manage_author">Tekijä:</string>
<string name="blogs_rss_feeds_manage_updated">Viimeksi päivitetty:</string>
<string name="blogs_rss_remove_feed">Poista syöte</string>
<string name="blogs_rss_remove_feed_dialog_message">Oletko varma, että haluat poistaa tämän syötteen?\n\nKirjoitukset poistuvat sinun laitteelta, mutta ei muiden laitteilta.\n\nKäyttäjät joiden kanssa olet jakanut tämän syötteen eivät välttämättä saa uusia päivityksiä.</string>
<string name="blogs_rss_remove_feed_ok">Poista</string>
<string name="blogs_rss_feeds_manage_delete_error">Syötteen poistaminen epäonnistui!</string>
<string name="blogs_rss_feeds_manage_empty_state">Ei RSS syötteitä\n\nNapauta + nappia lisätäksesi syötteen</string>
<string name="blogs_rss_feeds_manage_error">Syötteiden lataamisessa tapahtui virhe. Yritä myöhemmin uudelleen.</string>
<!--Settings Display-->

View File

@@ -22,6 +22,9 @@
<string name="setup_huawei_text">Veuillez toucher le bouton ci-dessous et vous assurer que Briar est protégée dans lécran « Applis protégées ».</string>
<string name="setup_huawei_button">Protéger Briar</string>
<string name="setup_huawei_help">Si Briar nest pas ajoutée à la liste des applis protégées, elle ne pourra pas fonctionner en arrière-plan.</string>
<string name="setup_huawei_app_launch_text">Veuillez toucher le bouton ci-dessous, ouvrir lécran « Lancement des applis » et vous assurer que « Gérer manuellement » est défini pour Briar.</string>
<string name="setup_huawei_app_launch_button">Ouvrez les paramètres de la pile</string>
<string name="setup_huawei_app_launch_help">Si « Gérer manuellement » nest pas défini pour Briar dans lécran « Lancement des applis », lappli ne pourra pas fonctionner en arrière-plan.</string>
<string name="warning_dozed">%s na pas pu fonctionner en arrière-plan</string>
<!--Login-->
<string name="enter_password">Mot de passe</string>
@@ -159,6 +162,7 @@
<string name="menu_item_connect_via_bluetooth">Se connecter par Bluetooth</string>
<string name="dialog_title_connect_via_bluetooth">Se connecter par Bluetooth</string>
<string name="dialog_message_connect_via_bluetooth">Afin que cela fonctionne, votre contact doit être à proximité.\n\nVotre contact et vous devriez appuyer ensemble sur « Commencer ».</string>
<string name="toast_connect_via_bluetooth_already_discovering">Tentative de connexion par Bluetooth déjà en cours</string>
<string name="toast_connect_via_bluetooth_not_discoverable">Impossible de poursuivre sans le Bluetooth</string>
<string name="toast_connect_via_bluetooth_no_location_permission">Impossible de poursuivre sans la permission de position</string>
<string name="toast_connect_via_bluetooth_start">Connexion par Bluetooth…</string>
@@ -169,6 +173,7 @@
<!--The placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_disabled">Vos messages ne disparaîtront pas. %1$s</string>
<!--The first placeholder will show a contact's name. The second placeholder will show a duration like "7 days". The third placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_enabled">Les messages de %1$s disparaîtront après %2$s. %3$s</string>
<plurals name="duration_minutes">
<item quantity="one">%d minute</item>
<item quantity="other">%d minutes</item>
@@ -453,14 +458,12 @@
<string name="blogs_rss_feeds_import_button">Importer</string>
<string name="blogs_rss_feeds_import_hint">Saisir lURL du fil RSS</string>
<string name="blogs_rss_feeds_import_error">Nous sommes désolés! Une erreur est survenue lors de limportation de votre fil.</string>
<string name="blogs_rss_feeds_manage">Gérer les fils 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 fil</string>
<string name="blogs_rss_remove_feed_dialog_message">Voulez-vous vraiment supprimer ce fil?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce fil pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_rss_remove_feed_ok">Supprimer</string>
<string name="blogs_rss_feeds_manage_delete_error">Impossible de supprimer le fil!</string>
<string name="blogs_rss_feeds_manage_empty_state">Aucun fil RSS à afficher\n\nTouchez licône + pour importer un fil</string>
<string name="blogs_rss_feeds_manage_error">Un problème est survenu lors du chargement de vos fils. Veuillez réessayer plus tard.</string>
<!--Settings Profile Picture-->
@@ -557,8 +560,17 @@
<string name="cannot_load_ringtone">Impossible de charger la sonnerie</string>
<!--Conversation Settings-->
<string name="disappearing_messages_title">Messages éphémères</string>
<string name="disappearing_messages_explanation_long">dsvkjfjhgvfdkjhgv dfkjgh fkjhsgkjfdkj fhfgfhg
</string>
<string name="disappearing_messages_explanation_long">Lactivation de ce paramètre fera disparaître
les messages de cette conversation automatiquement après 7\u00A0jours.
\n\nPour lexemplaire de lexpéditeur du message, le compte à rebours commence une fois quil a été remis.
Pour le destinataire, le compte à rebours commence une fois que le
message a été lu.
\n\nLes messages éphémères sont marqués par une icône de bombe.
\n\nSoyez conscient que les destinataires peuvent toujours faire des
copies des messages que vous envoyez.
\n\nSi vous changez ce paramètre, il sera appliqué à vos nouveaux messages immédiatement
et aux messages de votre contact une fois que ce contact recevra votre prochain message.
Votre contact peut aussi changer ce paramètre pour vous deux.</string>
<string name="learn_more">En apprendre davantage</string>
<string name="disappearing_messages_summary">Faire disparaître automatiquement les futurs messages de cette conversation après 7 jours.</string>
<!--Settings Feedback-->

View File

@@ -454,14 +454,12 @@
<string name="blogs_rss_feeds_import_button">Importar</string>
<string name="blogs_rss_feeds_import_hint">Escribe o URL da fonte RSS</string>
<string name="blogs_rss_feeds_import_error">Lamentámolo! Algo fallou ao importar a fonte.</string>
<string name="blogs_rss_feeds_manage">Xestionar Fontes RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importado:</string>
<string name="blogs_rss_feeds_manage_author">Autor/a:</string>
<string name="blogs_rss_feeds_manage_updated">Última actualización:</string>
<string name="blogs_rss_remove_feed">Eliminar fonte</string>
<string name="blogs_rss_remove_feed_dialog_message">Tes a certeza de querer eliminar esta fonte?\n\nAs entradas eliminaranse do teu dispositivo pero non dos dispositivos doutras persoas\n\nTodas as persoas coas que compartiches esta fonte poderían deixar de recibir actualizacións.</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_feeds_manage_delete_error">Non se puido eliminar a fonte!</string>
<string name="blogs_rss_feeds_manage_empty_state">Sen fontes RSS que mostrar\n\nToque na icona + para importar unha fonte</string>
<string name="blogs_rss_feeds_manage_error">Aconteceu un problema ao cargar as túas fontes. Por favor, inténtao máis tarde.</string>
<!--Settings Profile Picture-->

View File

@@ -444,14 +444,12 @@
<string name="blogs_rss_feeds_import_button">ייבא</string>
<string name="blogs_rss_feeds_import_hint">הכנס את כתובת האתר של הזנת ה־RSS</string>
<string name="blogs_rss_feeds_import_error">אנחנו מצטערים! הייתה שגיאה ביבוא ההזנה שלך.</string>
<string name="blogs_rss_feeds_manage">נהל הזנות RSS</string>
<string name="blogs_rss_feeds_manage_imported">מיובא:</string>
<string name="blogs_rss_feeds_manage_author">מחבר:</string>
<string name="blogs_rss_feeds_manage_updated">עודכן לאחרונה:</string>
<string name="blogs_rss_remove_feed">הסר הזנה</string>
<string name="blogs_rss_remove_feed_dialog_message">האם אכן ברצונך להסיר הזנה זו?\n\nרשומות יוסרו ממכשירך אבל לא ממכשירים של אנשים אחרים.\n\nאנשי קשר כלשהם ששיתפת איתם הזנה זו עלולים להפסיק לקבל עדכונים.</string>
<string name="blogs_rss_remove_feed_ok">הסר</string>
<string name="blogs_rss_feeds_manage_delete_error">ההזנה לא יכלה להימחק!</string>
<string name="blogs_rss_feeds_manage_empty_state">אין הזנות RSS להראות\n\nהקש על הצלמית + כדי לייבא הזנה</string>
<string name="blogs_rss_feeds_manage_error">הייתה בעיה בטעינת ההזנות שלך. אנא נסה שוב מאוחר יותר.</string>
<!--Settings Profile Picture-->

View File

@@ -378,14 +378,12 @@
<string name="blogs_rss_feeds_import_button">आयात</string>
<string name="blogs_rss_feeds_import_hint">आरएसएस फ़ीड का यूआरएल दर्ज करें</string>
<string name="blogs_rss_feeds_import_error">हमें खेद है! आपकी फ़ीड आयात करने में एक त्रुटि हुई</string>
<string name="blogs_rss_feeds_manage">आरएसएस फ़ीड प्रबंधित करें</string>
<string name="blogs_rss_feeds_manage_imported">आयातित:</string>
<string name="blogs_rss_feeds_manage_author">लेखक:</string>
<string name="blogs_rss_feeds_manage_updated">आखरी अपडेट:</string>
<string name="blogs_rss_remove_feed">फ़ीड निकालें</string>
<string name="blogs_rss_remove_feed_dialog_message">क्या आप वाकई इस फीड को हटाना चाहते हैं? \ N \ n पोस्ट आपके डिवाइस से हटा दिए जाएंगे, लेकिन अन्य लोगों के डिवाइस से नहीं। \ N \ n आपके द्वारा इस फ़ीड को साझा करने वाले किसी भी संपर्क को अपडेट प्राप्त करना बंद हो सकता है।</string>
<string name="blogs_rss_remove_feed_ok">हटाना</string>
<string name="blogs_rss_feeds_manage_delete_error">फीड हटाया नहीं जा सका!</string>
<string name="blogs_rss_feeds_manage_empty_state">कोई आरएसएस फ़ीड दिखाने के लिए फ़ीड नहीं करता \ n \ n फ़ीड आयात करने के लिए + आइकन टैप करें</string>
<string name="blogs_rss_feeds_manage_error">आपकी फ़ीड लोड करने में एक समस्या थी बाद में पुन: प्रयास करें।</string>
<!--Settings Profile Picture-->

View File

@@ -22,6 +22,9 @@
<string name="setup_huawei_text">Kérjük érintse meg a gombot alább és ellenőrizze, hogy a Briar védett, a \"Védett alkalmazások\" képernyőn.</string>
<string name="setup_huawei_button">A Briar védelme</string>
<string name="setup_huawei_help">Ha Briar nincs hozzáadva a védett alkalmazások listájához, akkor nem képes futni a háttérben.</string>
<string name="setup_huawei_app_launch_text">Kérjük érintsd meg a gombot alább, hogy megnyitsd az \"App indítás\" képernyőt és ellenőrizd, hogy a Briar beállított \"Kézi kezelés\"-re.</string>
<string name="setup_huawei_app_launch_button">Az akkumulátor beállítások megnyitása</string>
<string name="setup_huawei_app_launch_help">Ha a Briar nincs beállítva \"Kézi kezelés\"-re az \"App indítás\" képernyőn, nem fog tudni futni a háttérben.</string>
<string name="warning_dozed">%s nem tud futni a háttérben</string>
<!--Login-->
<string name="enter_password">Jelszó</string>
@@ -159,6 +162,7 @@
<string name="menu_item_connect_via_bluetooth">Csatlakozás bluetooth-on keresztül</string>
<string name="dialog_title_connect_via_bluetooth">Csatlakozás bluetooth-on keresztül</string>
<string name="dialog_message_connect_via_bluetooth">A kapcsolatai közel kell legyenek, hogy ez működjön.\n\n Ön és a kapcsolata egyaránt meg kell nyomja a \"Start\" gombot egyidőben.</string>
<string name="toast_connect_via_bluetooth_already_discovering">Már próbálkozik csatlakozni Bluetooth-on</string>
<string name="toast_connect_via_bluetooth_not_discoverable">Nem folytatható Bluetooth nélkül</string>
<string name="toast_connect_via_bluetooth_no_location_permission">Nem folytatható hely engedélyek nélkül</string>
<string name="toast_connect_via_bluetooth_start">Csatlakozás Bluetooth-on...</string>
@@ -461,7 +465,6 @@ Kapcsolatai, akivel megosztotta ezt a blogot, lehet nem kapnak többé frissít
<string name="blogs_rss_feeds_import_button">Importálás</string>
<string name="blogs_rss_feeds_import_hint">Adja meg az RSS feed URL címét</string>
<string name="blogs_rss_feeds_import_error">Elnézését kérjük! Probléma akadt a feed-je importálásával.</string>
<string name="blogs_rss_feeds_manage">RSS feed-ek kezelés</string>
<string name="blogs_rss_feeds_manage_imported">Importálva:</string>
<string name="blogs_rss_feeds_manage_author">Szerző:</string>
<string name="blogs_rss_feeds_manage_updated">Utolsó frissítés:</string>
@@ -469,7 +472,6 @@ Kapcsolatai, akivel megosztotta ezt a blogot, lehet nem kapnak többé frissít
<string name="blogs_rss_remove_feed_dialog_message">Biztosan eltávolítja ezt a feed-et?
\n\nA bejegyzések törlődni fognak az Ön eszközéről, de nem a többi ember eszközéről.\n\nKapcsolatai, akivel megosztotta ezt a feed-et, lehet nem kapnak többé frissítést.</string>
<string name="blogs_rss_remove_feed_ok">Eltávolít</string>
<string name="blogs_rss_feeds_manage_delete_error">A feed nem törölhető!</string>
<string name="blogs_rss_feeds_manage_empty_state">Nincs megjelenítendő</string>
<string name="blogs_rss_feeds_manage_error">Hiba történt a feed-jei betöltésével. Kérjük próbálja újra később.</string>
<!--Settings Profile Picture-->

View File

@@ -22,6 +22,9 @@
<string name="setup_huawei_text">Ýttu á hnappinn hér fyrir neðan og gakktu úr skugga um að Briar sé varið á skjánum \"Varin forrit\".</string>
<string name="setup_huawei_button">Verja Briar</string>
<string name="setup_huawei_help">Ef Briar er ekki bætt á listann yfir varin forrit, getur það ekki keyrt í bakgrunni.</string>
<string name="setup_huawei_app_launch_text">Ýttu á hnappinn hér fyrir neðan, opnaðu \"Ræsing forrits\" skjáinn og gakktu úr skugga um að Briar sé stillt á \"Stýra handvirkt\".</string>
<string name="setup_huawei_app_launch_button">Opna rafhlöðustillingar</string>
<string name="setup_huawei_app_launch_help">Ef Briar er ekki stillt á \"Stýra handvirkt\" í \"Ræsing forrits\" skjánum, mun það ekki geta keyrt í bakgrunni.</string>
<string name="warning_dozed">%s gat ekki keyrt í bakgrunni</string>
<!--Login-->
<string name="enter_password">Lykilorð</string>
@@ -159,6 +162,7 @@
<string name="menu_item_connect_via_bluetooth">Tengjast í gegnum Bluetooth</string>
<string name="dialog_title_connect_via_bluetooth">Tengjast í gegnum Bluetooth</string>
<string name="dialog_message_connect_via_bluetooth">Tengiliðurinn þinn þarf að vera nálægt til að þetta virki.\n\nÞú og tengiliðurinn ættuð bæði að ýta á \"Byrja\" á sama tíma.</string>
<string name="toast_connect_via_bluetooth_already_discovering">Þegar að reyna að tengjast í gegnum Bluetooth</string>
<string name="toast_connect_via_bluetooth_not_discoverable">Get ekki haldið áfram án Bluetooth</string>
<string name="toast_connect_via_bluetooth_no_location_permission">Get ekki haldið áfram án heimildar til að nota staðsetningu</string>
<string name="toast_connect_via_bluetooth_start">Tengist í gegnum Bluetooth…</string>
@@ -454,14 +458,12 @@
<string name="blogs_rss_feeds_import_button">Flytja inn</string>
<string name="blogs_rss_feeds_import_hint">Settu inn slóðina á RSS-streymið</string>
<string name="blogs_rss_feeds_import_error">Því miður! Það kom upp villa við að flytja inn streymið.</string>
<string name="blogs_rss_feeds_manage">Sýsla með RSS-streymi</string>
<string name="blogs_rss_feeds_manage_imported">Flutt inn:</string>
<string name="blogs_rss_feeds_manage_author">Höfundur:</string>
<string name="blogs_rss_feeds_manage_updated">Síðast uppfært:</string>
<string name="blogs_rss_remove_feed">Fjarlægja streymi</string>
<string name="blogs_rss_remove_feed_dialog_message">Ertu viss um að þú viljir fjarlægja þetta streymi?\n\nFærslur verða fjarlægðar af tækinu þínu en ekki tækjum annars fólks.\n\nAllir tengiliðir sem þú hefur deilt þessu streymi með gætu hætt að fá uppfærslur.</string>
<string name="blogs_rss_remove_feed_ok">Fjarlægja</string>
<string name="blogs_rss_feeds_manage_delete_error">Ekki var hægt að eyða streyminu!</string>
<string name="blogs_rss_feeds_manage_empty_state">Engin RSS-streymi til að birta\n\nÝttu á + táknið til að flytja inn streymi</string>
<string name="blogs_rss_feeds_manage_error">Vandamál hefur komið upp með að hlaða inn streymunum þínum. Reyndu aftur síðar.</string>
<!--Settings Profile Picture-->

View File

@@ -22,6 +22,9 @@
<string name="setup_huawei_text">Premi il pulsante qua sotto e assicurati che Briar sia protetto nella schermata \"App protette\"</string>
<string name="setup_huawei_button">Proteggi Briar</string>
<string name="setup_huawei_help">Se Briar non viene aggiunto nell\'elenco di app protette, non potrà funzionare in background.</string>
<string name="setup_huawei_app_launch_text">Tocca il pulsante sotto, apri la schermata \"Esecuzione app\" e assicurati che Briar sia su \"Gestisci manualmente\".</string>
<string name="setup_huawei_app_launch_button">Apri impostazioni batteria</string>
<string name="setup_huawei_app_launch_help">Se Briar non è su \"Gestisci manualmente\" nella schermata \"Esecuzione app\", non potrà funzionare in secondo piano.</string>
<string name="warning_dozed">%s non ha potuto funzionare in background</string>
<!--Login-->
<string name="enter_password">Password</string>
@@ -130,7 +133,7 @@
<string name="allow">Abilita</string>
<string name="open">Apri</string>
<string name="change">Cambia</string>
<string name="start">Avvia</string>
<string name="start">Inizia</string>
<string name="no_data">Nessun dato</string>
<string name="ellipsis">...</string>
<string name="text_too_long">Il testo inserito è troppo lungo</string>
@@ -146,6 +149,8 @@
<string name="date_no_private_messages">Nessun messaggio.</string>
<string name="no_private_messages">Nessun messaggio da mostrare</string>
<string name="message_hint">Nuovo messaggio</string>
<string name="message_hint_auto_delete">Nuovo messaggio dissolvente</string>
<string name="message_error">Errore nell\'invio del messaggio</string>
<string name="image_caption_hint">Aggiungi una didascalia (facoltativo)</string>
<string name="image_attach">Allega immagine</string>
<string name="image_attach_error">Impossibile allegare l\'immagine/i</string>
@@ -153,12 +158,41 @@
<string name="image_attach_error_invalid_mime_type">Formato immagine non supportato: %s</string>
<string name="set_contact_alias">Cambia il nome del contatto</string>
<string name="set_contact_alias_hint">Nome contatto</string>
<string name="menu_item_disappearing_messages">Messaggi dissolventi</string>
<string name="menu_item_connect_via_bluetooth">Connessione attraverso Bluetooth</string>
<string name="dialog_title_connect_via_bluetooth">Connessione attraverso Bluetooth</string>
<string name="dialog_message_connect_via_bluetooth">Per funzionare, il tuo contatto deve essere nelle vicinanze.\n\nDovreste entrambi premere \"Inizia\" nello stesso momento.</string>
<string name="toast_connect_via_bluetooth_already_discovering">Si sta già tentanto di connettersi via Bluetooth</string>
<string name="toast_connect_via_bluetooth_not_discoverable">Impossibile continuare senza Bluetooth</string>
<string name="toast_connect_via_bluetooth_no_location_permission">Impossibile continuare senza l\'autorizzazione per la geolocalizzazione</string>
<string name="toast_connect_via_bluetooth_start">Connessione via Bluetooth…</string>
<string name="toast_connect_via_bluetooth_success">Connessione via Bluetooth riuscita</string>
<string name="toast_connect_via_bluetooth_error">Connessione via Bluetooth fallita</string>
<!--The first placeholder will show a duration like "7 days". The second placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_enabled">I tuoi messaggi spariranno dopo %1$s. %2$s</string>
<!--The placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_disabled">I tuoi messaggi non spariranno. %1$s</string>
<!--The first placeholder will show a contact's name. The second placeholder will show a duration like "7 days". The third placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_enabled">I messaggi di %1$s spariranno dopo %2$s. %3$s</string>
<plurals name="duration_minutes">
<item quantity="one">%d minuto</item>
<item quantity="other">%d minuti</item>
</plurals>
<plurals name="duration_hours">
<item quantity="one">%d ora</item>
<item quantity="other">%d ore</item>
</plurals>
<plurals name="duration_days">
<item quantity="one">%d giorno</item>
<item quantity="other">%d giorni</item>
</plurals>
<!--The first placeholder will show a contact's name. The second placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_disabled">I messaggi di %1$s non spariranno. %2$s</string>
<string name="tap_to_learn_more">Tocca per saperne di più.</string>
<string name="auto_delete_changed_warning_title">Messaggi dissolventi cambiati</string>
<string name="auto_delete_changed_warning_message_enabled">Dato che hai iniziato a comporre il messaggio, i messaggi dissolventi sono stati attivati.</string>
<string name="auto_delete_changed_warning_message_disabled">Dato che hai iniziato a comporre il messaggio, i messaggi dissolventi sono stati disattivati.</string>
<string name="auto_delete_changed_warning_send">Invia comunque</string>
<string name="delete_all_messages">Elimina tutti i messaggi</string>
<string name="dialog_title_delete_all_messages">Conferma l\'eliminazione dei messaggi</string>
<string name="dialog_message_delete_all_messages">Sei sicuro di voler eliminare tutti i messaggi?</string>
@@ -277,6 +311,7 @@
<string name="introduction_response_accepted_sent">Hai accettato l\'introduzione a %1$s.</string>
<string name="introduction_response_accepted_sent_info">Prima che %1$s venga aggiunto ai tuoi contatti, dovranno anche loro accettare l\'introduzione. Questo potrebbe richiedere un po\' di tempo.</string>
<string name="introduction_response_declined_sent">Hai declinato l\'introduzione a %1$s.</string>
<string name="introduction_response_declined_auto">L\'introduzione a %1$s è stata rifiutata automaticamente.</string>
<string name="introduction_response_accepted_received">%1$s ha accettato l\'introduzione a %2$s.</string>
<string name="introduction_response_declined_received">%1$s ha declinato l\'introduzione a %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s dice che %2$s ha declinato l\'introduzione.</string>
@@ -323,6 +358,7 @@
</plurals>
<string name="groups_invitations_response_accepted_sent">Hai accettato l\'invito al gruppo da %s.</string>
<string name="groups_invitations_response_declined_sent">Hai rifiutato l\'invito al gruppo da %s.</string>
<string name="groups_invitations_response_declined_auto">L\'invito di %s al gruppo è stato rifiutato automaticamente.</string>
<string name="groups_invitations_response_accepted_received">%s ha accettato l\'invito al gruppo.</string>
<string name="groups_invitations_response_declined_received">%s ha rifiutato l\'invito al gruppo.</string>
<string name="sharing_status_groups">Solo il creatore può invitare nuovi membri nel gruppo. Sotto ci sono i membri correnti del gruppo.</string>
@@ -373,6 +409,7 @@
<string name="forum_invitation_already_sharing">Già in condivisione</string>
<string name="forum_invitation_response_accepted_sent">Hai accettato l\'invito al forum da %s</string>
<string name="forum_invitation_response_declined_sent">Hai declinato l\'invito al forum da %s</string>
<string name="forum_invitation_response_declined_auto">L\'invito di %s al forum è stato rifiutato automaticamente.</string>
<string name="forum_invitation_response_accepted_received">%s ha accettato il tuo invito al forum.</string>
<string name="forum_invitation_response_declined_received">%s ha declinato il tuo invito al forum.</string>
<string name="sharing_status">Stato Condivisione</string>
@@ -407,6 +444,7 @@
<string name="blogs_sharing_snackbar">Blog condiviso con i contatti selezionati</string>
<string name="blogs_sharing_response_accepted_sent">Hai accettato l\'invito al blog da %s.</string>
<string name="blogs_sharing_response_declined_sent">Hai declinato l\'invito al blog da %s.</string>
<string name="blogs_sharing_response_declined_auto">L\'invito di %s al blog è stato rifiutato automaticamente.</string>
<string name="blogs_sharing_response_accepted_received">%s ha accettato l\'invito al blog.</string>
<string name="blogs_sharing_response_declined_received">%s ha declinato l\'invito al blog.</string>
<string name="blogs_sharing_invitation_received">%1$s ha condiviso il blog \"%2$s\" con te.</string>
@@ -420,14 +458,12 @@
<string name="blogs_rss_feeds_import_button">Importa</string>
<string name="blogs_rss_feeds_import_hint">Inserire l\'URL dell\'RSS feed</string>
<string name="blogs_rss_feeds_import_error">Ci dispiace! C\'è stato un errore nell\'importazione del tuo feed.</string>
<string name="blogs_rss_feeds_manage">Gestisci gli RSS Feed</string>
<string name="blogs_rss_feeds_manage_imported">Importato:</string>
<string name="blogs_rss_feeds_manage_author">Autore:</string>
<string name="blogs_rss_feeds_manage_updated">Ultimo Aggiornamento:</string>
<string name="blogs_rss_remove_feed">Rimuovi feed</string>
<string name="blogs_rss_remove_feed_dialog_message">Sei sicuro di voler rimuovere questo feed?\n\nI post saranno rimossi dal tuo dispositivo ma non dai dispositivi delle altre persone.\n\nTutti i contatti con cui hai condiviso questo feed potrebbero smettere di ricevere aggiornamenti.</string>
<string name="blogs_rss_remove_feed_ok">Rimuovi</string>
<string name="blogs_rss_feeds_manage_delete_error">Non è stato possibile cancellare il feed!</string>
<string name="blogs_rss_feeds_manage_empty_state">Nessun feed RSS da mostrare\n\nClicca l\'icona + per importare un feed</string>
<string name="blogs_rss_feeds_manage_error">C\'è stato un problema nel caricare i tuoi feeds. Per favore riprova fra poco.</string>
<!--Settings Profile Picture-->
@@ -523,7 +559,18 @@
<string name="choose_ringtone_title">Scegli suoneria</string>
<string name="cannot_load_ringtone">Impossibile caricare la suoneria</string>
<!--Conversation Settings-->
<string name="disappearing_messages_title">Messaggi dissolventi</string>
<string name="disappearing_messages_explanation_long">L\'attivazione di questa impostazione farà sparire
automaticamente i nuovi messaggi di questa conversazione dopo 7\u00A0giorni.
\n\nIl conto alla rovescia per la copia del messaggio del mittente inizia dopo la sua consegna.
Il conto alla rovescia per il destinatario inizia dopo la lettura del messaggio.
\n\nI messaggi che spariranno sono contrassegnati da un\'icona di una bomba.
\n\nTieni presente che i destinatari possono comunque fare copie dei messaggi che invii.
\n\nSe cambi questa impostazione, verrà applicata ai tuoi nuovi messaggi immediatamente e ai messaggi
del tuo contatto una volta che riceve il tuo prossimo messaggio.
Anche il tuo contatto può cambiare questa impostazioni per entrambe le parti.</string>
<string name="learn_more">Per saperne di più</string>
<string name="disappearing_messages_summary">Fai sparire automaticamente i messaggi futuri di questa conversazione dopo 7\u00A0giorni.</string>
<!--Settings Feedback-->
<string name="send_feedback">Invia feedback</string>
<!--Link Warning-->
@@ -577,6 +624,9 @@
<string name="permission_camera_location_request_body">Per scansionare il codice QR, Briar ha bisogno di accedere alla fotocamera.\n\nPer trovare dispositivi Bluetooth, Briar ha bisogno di accedere alla tua posizione.\n\nBriar non memorizza la tua posizione, nè la condivide con terzi.</string>
<string name="permission_camera_denied_body">Hai negato l\'accesso alla fotocamera, ma questa serve per aggiungere i contatti.\n\nConsidera la possibilità di concedere l\'accesso.</string>
<string name="permission_location_denied_body">Hai negato l\'accesso alla tua posizione, ma Briar ha bisogno di questa autorizzazione per trovare i dispositivi Bluetooth.\n\nConsidera la possibilità di concedere l\'accesso.</string>
<string name="permission_location_setting_title">Impostazioni di posizione</string>
<string name="permission_location_setting_body">La geolocalizzazione del tuo dispositivo deve essere attiva per trovare altri dispositivi via Bluetooth. Attivala per continuare. Puoi disattivarla di nuovo in seguito.</string>
<string name="permission_location_setting_button">Attiva geolocalizzazione</string>
<string name="qr_code">Codice QR</string>
<string name="show_qr_code_fullscreen">Mostra codice QR a tutto schermo</string>
<!--App Locking-->

View File

@@ -369,14 +369,12 @@
<string name="blogs_rss_feeds_import_button">インポート</string>
<string name="blogs_rss_feeds_import_hint">RSSフィードのURLを入力してください</string>
<string name="blogs_rss_feeds_import_error">申し訳ありません! フィードのインポート中にエラーが発生しました。</string>
<string name="blogs_rss_feeds_manage">RSSフィードを管理</string>
<string name="blogs_rss_feeds_manage_imported">インポート済み:</string>
<string name="blogs_rss_feeds_manage_author">著者:</string>
<string name="blogs_rss_feeds_manage_updated">最終更新:</string>
<string name="blogs_rss_remove_feed">フィードを削除</string>
<string name="blogs_rss_remove_feed_dialog_message">このフィードを削除してもよろしいですか?\n\n投稿はデバイスから削除されますが、他の人のデバイスからは削除されません。\n\nこのフィードを共有した人は更新の受信を停止されます。</string>
<string name="blogs_rss_remove_feed_ok">解除</string>
<string name="blogs_rss_feeds_manage_delete_error">フィードを削除できませんでした!</string>
<string name="blogs_rss_feeds_manage_empty_state">表示するRSSフィードはありません\n\n「」アイコンをタップしてフィードをインポートします</string>
<string name="blogs_rss_feeds_manage_error">フィードの読み込み中に問題が発生しました。 後でもう一度やり直してください。</string>
<!--Settings Profile Picture-->

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