mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 19:29:06 +01:00
Compare commits
1 Commits
removable-
...
1743-xiaom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9277767d3 |
@@ -5,15 +5,6 @@ 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
|
||||
|
||||
@@ -6,7 +6,7 @@ apply from: 'witness.gradle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.3'
|
||||
buildToolsVersion '30.0.2'
|
||||
|
||||
packagingOptions {
|
||||
doNotStrip '**/*.so'
|
||||
@@ -14,9 +14,9 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10303
|
||||
versionName "1.3.3"
|
||||
targetSdkVersion 29
|
||||
versionCode 10302
|
||||
versionName "1.3.2"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -59,8 +59,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class AndroidBluetoothPlugin extends
|
||||
AbstractBluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
|
||||
class AndroidBluetoothPlugin
|
||||
extends BluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(AndroidBluetoothPlugin.class.getName());
|
||||
@@ -75,7 +75,6 @@ class AndroidBluetoothPlugin extends
|
||||
|
||||
// Non-null if the plugin started successfully
|
||||
private volatile BluetoothAdapter adapter = null;
|
||||
private volatile boolean stopDiscoverAndConnect;
|
||||
|
||||
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
||||
BluetoothConnectionFactory<BluetoothSocket> connectionFactory,
|
||||
@@ -188,40 +187,22 @@ class AndroidBluetoothPlugin extends
|
||||
@Nullable
|
||||
DuplexTransportConnection discoverAndConnect(String uuid) {
|
||||
if (adapter == null) return null;
|
||||
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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
} 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<>();
|
||||
|
||||
@@ -47,7 +47,7 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
|
||||
private final BackoffFactory backoffFactory;
|
||||
|
||||
@Inject
|
||||
AndroidBluetoothPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
public AndroidBluetoothPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
@WakefulIoExecutor Executor wakefulIoExecutor,
|
||||
AndroidExecutor androidExecutor,
|
||||
AndroidWakeLockManager wakeLockManager,
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
|
||||
private final Application app;
|
||||
|
||||
@Inject
|
||||
AndroidLanTcpPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
public AndroidLanTcpPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
@WakefulIoExecutor Executor wakefulIoExecutor,
|
||||
EventBus eventBus,
|
||||
BackoffFactory backoffFactory,
|
||||
|
||||
@@ -58,7 +58,7 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
|
||||
private final File torDirectory;
|
||||
|
||||
@Inject
|
||||
AndroidTorPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
public AndroidTorPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
@WakefulIoExecutor Executor wakefulIoExecutor,
|
||||
Application app,
|
||||
NetworkManager networkManager,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.briarproject.bramble.api;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface Consumer<T> {
|
||||
|
||||
void accept(T t);
|
||||
}
|
||||
@@ -292,16 +292,6 @@ 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/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.bramble.api.plugin.file;
|
||||
package org.briarproject.bramble.api.plugin;
|
||||
|
||||
public interface FileConstants {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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";
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,11 @@ 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, long totalLength) {
|
||||
Collection<MessageId> messageIds) {
|
||||
this.contactId = contactId;
|
||||
this.messageIds = messageIds;
|
||||
this.totalLength = totalLength;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
@@ -34,8 +32,4 @@ public class MessagesSentEvent extends Event {
|
||||
public Collection<MessageId> getMessageIds() {
|
||||
return messageIds;
|
||||
}
|
||||
|
||||
public long getTotalLength() {
|
||||
return totalLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,16 +347,6 @@ 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
|
||||
|
||||
@@ -415,17 +415,14 @@ 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) {
|
||||
Message message = db.getMessage(txn, m);
|
||||
totalLength += message.getRawLength();
|
||||
messages.add(message);
|
||||
messages.add(db.getMessage(txn, m));
|
||||
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
|
||||
}
|
||||
if (ids.isEmpty()) return null;
|
||||
db.lowerRequestedFlag(txn, c, ids);
|
||||
transaction.attach(new MessagesSentEvent(c, ids, totalLength));
|
||||
transaction.attach(new MessagesSentEvent(c, ids));
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -470,17 +467,14 @@ 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) {
|
||||
Message message = db.getMessage(txn, m);
|
||||
totalLength += message.getRawLength();
|
||||
messages.add(message);
|
||||
messages.add(db.getMessage(txn, m));
|
||||
db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
|
||||
}
|
||||
if (ids.isEmpty()) return null;
|
||||
db.lowerRequestedFlag(txn, c, ids);
|
||||
transaction.attach(new MessagesSentEvent(c, ids, totalLength));
|
||||
transaction.attach(new MessagesSentEvent(c, ids));
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -575,15 +569,6 @@ 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 {
|
||||
|
||||
@@ -1758,37 +1758,6 @@ 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 {
|
||||
|
||||
@@ -1,623 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,606 @@
|
||||
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;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface BluetoothPlugin extends DuplexPlugin {
|
||||
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;
|
||||
|
||||
boolean isDiscovering();
|
||||
@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;
|
||||
|
||||
@Nullable
|
||||
DuplexTransportConnection discoverAndConnectForSetup(String uuid);
|
||||
abstract DuplexTransportConnection discoverAndConnect(String uuid);
|
||||
|
||||
void stopDiscoverAndConnect();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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(19).of(database).startTransaction();
|
||||
exactly(18).of(database).startTransaction();
|
||||
will(returnValue(txn));
|
||||
exactly(19).of(database).containsContact(txn, contactId);
|
||||
exactly(18).of(database).containsContact(txn, contactId);
|
||||
will(returnValue(false));
|
||||
exactly(19).of(database).abortTransaction(txn);
|
||||
exactly(18).of(database).abortTransaction(txn);
|
||||
}});
|
||||
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||
eventExecutor, shutdownManager);
|
||||
@@ -349,7 +349,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.transaction(false, transaction ->
|
||||
db.getContact(transaction, contactId));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -357,15 +357,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.getMessageBytesToSend(transaction, contactId, 123));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.transaction(false, transaction ->
|
||||
db.getMessageStatus(transaction, contactId, groupId));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -373,7 +365,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.transaction(false, transaction ->
|
||||
db.getMessageStatus(transaction, contactId, messageId));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -381,7 +373,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.transaction(false, transaction ->
|
||||
db.getGroupVisibility(transaction, contactId, groupId));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
@@ -389,7 +381,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
try {
|
||||
db.transaction(true, transaction ->
|
||||
db.transaction(false, transaction ->
|
||||
db.getSyncVersions(transaction, contactId));
|
||||
fail();
|
||||
} catch (NoSuchContactException expected) {
|
||||
|
||||
@@ -227,8 +227,6 @@ 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);
|
||||
@@ -236,7 +234,6 @@ 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();
|
||||
@@ -261,7 +258,6 @@ 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);
|
||||
@@ -269,8 +265,6 @@ 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);
|
||||
@@ -278,7 +272,6 @@ 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);
|
||||
@@ -286,7 +279,6 @@ 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();
|
||||
@@ -310,7 +302,6 @@ 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);
|
||||
@@ -318,7 +309,6 @@ 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);
|
||||
@@ -326,8 +316,6 @@ 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);
|
||||
@@ -335,7 +323,6 @@ 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);
|
||||
@@ -343,7 +330,6 @@ 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();
|
||||
@@ -368,7 +354,6 @@ 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);
|
||||
@@ -376,8 +361,6 @@ 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();
|
||||
@@ -401,15 +384,10 @@ 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();
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,61 @@
|
||||
package org.briarproject.bramble.plugin.file;
|
||||
package org.briarproject.bramble.plugin;
|
||||
|
||||
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
|
||||
class RemovableDriveIntegrationTestModule {
|
||||
public class DesktopPluginModule extends PluginModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
PluginConfig providePluginConfig(RemovableDrivePluginFactory drive) {
|
||||
PluginConfig getPluginConfig(JavaBluetoothPluginFactory bluetooth,
|
||||
ModemPluginFactory modem, LanTcpPluginFactory lan,
|
||||
WanTcpPluginFactory wan) {
|
||||
@NotNullByDefault
|
||||
PluginConfig pluginConfig = new PluginConfig() {
|
||||
|
||||
@Override
|
||||
public Collection<DuplexPluginFactory> getDuplexFactories() {
|
||||
return emptyList();
|
||||
return asList(bluetooth, modem, lan, wan);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SimplexPluginFactory> getSimplexFactories() {
|
||||
return singletonList(drive);
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldPoll() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<TransportId, List<TransportId>> getTransportPreferences() {
|
||||
return emptyMap();
|
||||
// Prefer LAN to Bluetooth
|
||||
return singletonMap(BluetoothConstants.ID,
|
||||
singletonList(LanTcpConstants.ID));
|
||||
}
|
||||
|
||||
};
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ import static org.briarproject.bramble.util.StringUtils.isValidMac;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class JavaBluetoothPlugin extends
|
||||
AbstractBluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
|
||||
class JavaBluetoothPlugin
|
||||
extends BluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(JavaBluetoothPlugin.class.getName());
|
||||
@@ -108,11 +108,6 @@ class JavaBluetoothPlugin extends
|
||||
return null; // TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopDiscoverAndConnect() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private String makeUrl(String address, String uuid) {
|
||||
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
|
||||
private final File torDirectory;
|
||||
|
||||
@Inject
|
||||
UnixTorPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
public UnixTorPluginFactory(@IoExecutor Executor ioExecutor,
|
||||
@IoExecutor Executor wakefulIoExecutor,
|
||||
NetworkManager networkManager,
|
||||
LocationUtils locationUtils,
|
||||
|
||||
@@ -23,10 +23,8 @@ 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;
|
||||
@@ -47,22 +45,15 @@ import static org.junit.Assume.assumeTrue;
|
||||
public class BridgeTest extends BrambleTestCase {
|
||||
|
||||
@Parameters
|
||||
public static Iterable<Params> data() {
|
||||
public static Iterable<String> data() {
|
||||
BrambleJavaIntegrationTestComponent component =
|
||||
DaggerBrambleJavaIntegrationTestComponent.builder().build();
|
||||
BrambleCoreIntegrationTestEagerSingletons.Helper
|
||||
.injectEagerSingletons(component);
|
||||
// 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;
|
||||
return component.getCircumventionProvider().getBridges(false);
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -89,13 +80,11 @@ public class BridgeTest extends BrambleTestCase {
|
||||
|
||||
private final File torDir = getTestDirectory();
|
||||
private final String bridge;
|
||||
private final AtomicInteger failures;
|
||||
|
||||
private UnixTorPluginFactory factory;
|
||||
|
||||
public BridgeTest(Params params) {
|
||||
bridge = params.bridge;
|
||||
failures = params.failures;
|
||||
public BridgeTest(String bridge) {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -163,24 +152,10 @@ public class BridgeTest extends BrambleTestCase {
|
||||
clock.sleep(500);
|
||||
}
|
||||
if (plugin.getState() != ACTIVE) {
|
||||
LOG.warning("Could not connect to Tor within timeout");
|
||||
if (failures.incrementAndGet() > NUM_FAILURES_ALLOWED) {
|
||||
fail(failures.get() + " bridges are unreachable");
|
||||
}
|
||||
fail("Could not connect to Tor within timeout.");
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, es: es-ES, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
|
||||
lang_map = en: en-US, de: de-DE, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
|
||||
file_filter = fastlane/metadata/android/<lang>/short_description.txt
|
||||
source_file = fastlane/metadata/android/en-US/short_description.txt
|
||||
source_lang = en
|
||||
@@ -19,7 +19,7 @@ type = TXT
|
||||
minimum_perc = 100
|
||||
|
||||
[briar.google-play-full-description]
|
||||
lang_map = en: en-US, de: de-DE, es: es-ES, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
|
||||
lang_map = en: en-US, de: de-DE, gl: gl-ES, tr: tr-TR, zh-Hans: zh-CN
|
||||
file_filter = fastlane/metadata/android/<lang>/full_description.txt
|
||||
source_file = fastlane/metadata/android/en-US/full_description.txt
|
||||
source_lang = en
|
||||
|
||||
@@ -17,7 +17,7 @@ def getStdout = { command, defaultValue ->
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.3'
|
||||
buildToolsVersion '30.0.2'
|
||||
|
||||
packagingOptions {
|
||||
doNotStrip '**/*.so'
|
||||
@@ -25,9 +25,9 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10303
|
||||
versionName "1.3.3"
|
||||
targetSdkVersion 29
|
||||
versionCode 10302
|
||||
versionName "1.3.2"
|
||||
applicationId "org.briarproject.briar.android"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
Sicher kommunizieren, überall
|
||||
@@ -1 +0,0 @@
|
||||
Briar
|
||||
@@ -1,5 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
Mensajería segura, en cualquier lado.
|
||||
@@ -1 +0,0 @@
|
||||
Briar
|
||||
@@ -1 +0,0 @@
|
||||
https://www.youtube.com/watch?v=OuJjWdDfKuY
|
||||
@@ -322,6 +322,25 @@
|
||||
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"
|
||||
@@ -428,24 +447,6 @@
|
||||
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"
|
||||
@@ -458,6 +459,7 @@
|
||||
<package android:name="com.huawei.systemmanager" />
|
||||
<package android:name="com.huawei.powergenie" />
|
||||
<package android:name="com.evenwell.PowerMonitor" />
|
||||
<package android:name="com.miui.powerkeeper" />
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -28,7 +28,6 @@ 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;
|
||||
@@ -84,8 +83,7 @@ import dagger.Component;
|
||||
AppModule.class,
|
||||
AttachmentModule.class,
|
||||
ClockModule.class,
|
||||
MediaModule.class,
|
||||
RemovableDriveModule.class
|
||||
MediaModule.class
|
||||
})
|
||||
public interface AndroidComponent
|
||||
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,
|
||||
|
||||
@@ -24,8 +24,6 @@ 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;
|
||||
@@ -69,6 +67,7 @@ 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;
|
||||
@@ -93,7 +92,6 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
GroupListModule.class,
|
||||
GroupConversationModule.class,
|
||||
SharingModule.class,
|
||||
RemovableDriveModule.class
|
||||
})
|
||||
public class AppModule {
|
||||
|
||||
@@ -151,10 +149,8 @@ public class AppModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
PluginConfig providePluginConfig(AndroidBluetoothPluginFactory bluetooth,
|
||||
AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan,
|
||||
AndroidRemovableDrivePluginFactory drive) {
|
||||
AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan) {
|
||||
@NotNullByDefault
|
||||
PluginConfig pluginConfig = new PluginConfig() {
|
||||
|
||||
@@ -165,7 +161,7 @@ public class AppModule {
|
||||
|
||||
@Override
|
||||
public Collection<SimplexPluginFactory> getSimplexFactories() {
|
||||
return singletonList(drive);
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,6 +32,7 @@ public class DozeFragment extends SetupFragment
|
||||
private DozeView dozeView;
|
||||
private HuaweiProtectedAppsView huaweiProtectedAppsView;
|
||||
private HuaweiAppLaunchView huaweiAppLaunchView;
|
||||
private XiaomiBatterySaverView xiaomiBatterySaverView;
|
||||
private Button next;
|
||||
private boolean secondAttempt = false;
|
||||
|
||||
@@ -53,6 +54,8 @@ public class DozeFragment extends SetupFragment
|
||||
huaweiProtectedAppsView.setOnCheckedChangedListener(this);
|
||||
huaweiAppLaunchView = v.findViewById(R.id.huaweiAppLaunchView);
|
||||
huaweiAppLaunchView.setOnCheckedChangedListener(this);
|
||||
xiaomiBatterySaverView = v.findViewById(R.id.xiaomiBatterySaverView);
|
||||
xiaomiBatterySaverView.setOnCheckedChangedListener(this);
|
||||
next = v.findViewById(R.id.next);
|
||||
ProgressBar progressBar = v.findViewById(R.id.progress);
|
||||
|
||||
@@ -98,7 +101,8 @@ public class DozeFragment extends SetupFragment
|
||||
public void onCheckedChanged() {
|
||||
next.setEnabled(dozeView.isChecked() &&
|
||||
huaweiProtectedAppsView.isChecked() &&
|
||||
huaweiAppLaunchView.isChecked());
|
||||
huaweiAppLaunchView.isChecked() &&
|
||||
xiaomiBatterySaverView.isChecked());
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
|
||||
@@ -10,6 +10,7 @@ class DozeHelperImpl implements DozeHelper {
|
||||
Context appContext = context.getApplicationContext();
|
||||
return needsDozeWhitelisting(appContext) ||
|
||||
HuaweiProtectedAppsView.needsToBeShown(appContext) ||
|
||||
HuaweiAppLaunchView.needsToBeShown(appContext);
|
||||
HuaweiAppLaunchView.needsToBeShown(appContext) ||
|
||||
XiaomiBatterySaverView.needsToBeShown(appContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.briarproject.briar.android.account;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
class XiaomiBatterySaverView extends PowerView {
|
||||
|
||||
private final static String PACKAGE_NAME = "com.miui.powerkeeper";
|
||||
private final static String CLASS_NAME =
|
||||
PACKAGE_NAME + ".ui.HiddenAppsConfigActivity";
|
||||
|
||||
public XiaomiBatterySaverView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public XiaomiBatterySaverView(Context context,
|
||||
@Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public XiaomiBatterySaverView(Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setText(R.string.setup_xiaomi_text);
|
||||
setButtonText(R.string.setup_xiaomi_button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsToBeShown() {
|
||||
return needsToBeShown(getContext());
|
||||
}
|
||||
|
||||
public static boolean needsToBeShown(Context context) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(getIntent(),
|
||||
MATCH_DEFAULT_ONLY);
|
||||
return !resolveInfos.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@StringRes
|
||||
protected int getHelpText() {
|
||||
return R.string.setup_xiaomi_help;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onButtonClick() {
|
||||
getContext().startActivity(getIntent());
|
||||
setChecked(true);
|
||||
}
|
||||
|
||||
private static Intent getIntent() {
|
||||
Intent intent = new Intent();
|
||||
intent.setClassName(PACKAGE_NAME, CLASS_NAME);
|
||||
return intent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,11 +13,8 @@ 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.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.RssFeedImportActivity;
|
||||
import org.briarproject.briar.android.blog.RssFeedManageActivity;
|
||||
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
|
||||
import org.briarproject.briar.android.contact.ContactListFragment;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity;
|
||||
@@ -63,7 +60,6 @@ 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;
|
||||
@@ -165,7 +161,9 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(IntroductionActivity activity);
|
||||
|
||||
void inject(RssFeedActivity activity);
|
||||
void inject(RssFeedImportActivity activity);
|
||||
|
||||
void inject(RssFeedManageActivity activity);
|
||||
|
||||
void inject(StartupFailureActivity activity);
|
||||
|
||||
@@ -177,8 +175,6 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(CrashReportActivity crashReportActivity);
|
||||
|
||||
void inject(RemovableDriveActivity activity);
|
||||
|
||||
// Fragments
|
||||
|
||||
void inject(SetupFragment fragment);
|
||||
@@ -237,12 +233,4 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(
|
||||
BluetoothConnecterDialogFragment bluetoothConnecterDialogFragment);
|
||||
|
||||
void inject(RssFeedImportFragment fragment);
|
||||
|
||||
void inject(RssFeedManageFragment fragment);
|
||||
|
||||
void inject(RssFeedImportFailedDialogFragment fragment);
|
||||
|
||||
void inject(RssFeedDeleteFeedDialogFragment fragment);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,5 @@ 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;
|
||||
|
||||
}
|
||||
|
||||
@@ -20,8 +20,4 @@ public interface BlogModule {
|
||||
@ViewModelKey(BlogViewModel.class)
|
||||
ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(RssFeedViewModel.class)
|
||||
ViewModel bindRssFeedViewModel(RssFeedViewModel rssFeedViewModel);
|
||||
}
|
||||
|
||||
@@ -131,8 +131,15 @@ public class FeedFragment extends BaseFragment
|
||||
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
|
||||
startActivity(i);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_rss_feeds) {
|
||||
Intent i = new Intent(getActivity(), RssFeedActivity.class);
|
||||
} 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());
|
||||
startActivity(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,54 +7,91 @@ 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;
|
||||
|
||||
@NotNullByDefault
|
||||
class RssFeedAdapter extends ListAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
|
||||
class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
|
||||
|
||||
private final RssFeedListener listener;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
RssFeedAdapter(Context ctx, RssFeedListener listener) {
|
||||
super(ctx, Feed.class);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
R.layout.list_item_rss_feed, parent, false);
|
||||
return new FeedViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(FeedViewHolder ui, int position) {
|
||||
ui.bindItem(getItem(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));
|
||||
}
|
||||
|
||||
class FeedViewHolder extends RecyclerView.ViewHolder {
|
||||
private final Context ctx;
|
||||
@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 {
|
||||
private final View layout;
|
||||
private final TextView title;
|
||||
private final ImageButton delete;
|
||||
@@ -67,7 +104,6 @@ class RssFeedAdapter extends ListAdapter<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);
|
||||
@@ -77,44 +113,10 @@ class RssFeedAdapter extends ListAdapter<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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,6 @@ 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();
|
||||
|
||||
@@ -44,7 +44,6 @@ 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;
|
||||
@@ -150,9 +149,7 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
||||
@Nullable
|
||||
private final BluetoothAdapter bt;
|
||||
@Nullable // UiThread
|
||||
private Plugin wifiPlugin;
|
||||
@Nullable // UiThread
|
||||
private BluetoothPlugin bluetoothPlugin;
|
||||
private Plugin wifiPlugin, bluetoothPlugin;
|
||||
|
||||
// UiThread
|
||||
private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
|
||||
@@ -198,8 +195,7 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
||||
this.connectionManager = connectionManager;
|
||||
bt = BluetoothAdapter.getDefaultAdapter();
|
||||
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
|
||||
bluetoothPlugin = (BluetoothPlugin) pluginManager
|
||||
.getPlugin(BluetoothConstants.ID);
|
||||
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
||||
qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
|
||||
eventBus.addListener(this);
|
||||
IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
|
||||
@@ -222,8 +218,7 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
||||
@UiThread
|
||||
void resetPlugins() {
|
||||
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
|
||||
bluetoothPlugin = (BluetoothPlugin) pluginManager
|
||||
.getPlugin(BluetoothConstants.ID);
|
||||
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -380,13 +375,6 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
||||
}
|
||||
}
|
||||
|
||||
void stopDiscovery() {
|
||||
if (!isBluetoothSupported() || !bluetoothPlugin.isDiscovering()) {
|
||||
return;
|
||||
}
|
||||
bluetoothPlugin.stopDiscoverAndConnect();
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
@UiThread
|
||||
void showQrCodeFragmentIfAllowed() {
|
||||
|
||||
@@ -6,30 +6,23 @@ 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;
|
||||
@@ -38,24 +31,17 @@ 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 implements EventListener {
|
||||
class BluetoothConnecter {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -66,41 +52,32 @@ class BluetoothConnecter implements EventListener {
|
||||
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 BluetoothPlugin bluetoothPlugin;
|
||||
private volatile Plugin bluetoothPlugin;
|
||||
|
||||
private Permission locationPermission = Permission.UNKNOWN;
|
||||
private ContactId contactId = null;
|
||||
|
||||
@Inject
|
||||
BluetoothConnecter(Application app,
|
||||
PluginManager pluginManager,
|
||||
@IoExecutor Executor ioExecutor,
|
||||
AndroidExecutor androidExecutor,
|
||||
ConnectionRegistry connectionRegistry,
|
||||
EventBus eventBus,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
ConnectionManager connectionManager) {
|
||||
ConnectionRegistry connectionRegistry) {
|
||||
this.app = app;
|
||||
this.pluginManager = pluginManager;
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID);
|
||||
this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
||||
this.connectionRegistry = connectionRegistry;
|
||||
this.eventBus = eventBus;
|
||||
this.transportPropertyManager = transportPropertyManager;
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
boolean isConnectedViaBluetooth(ContactId contactId) {
|
||||
return connectionRegistry.isConnected(contactId, ID);
|
||||
return connectionRegistry.isConnected(contactId, BluetoothConstants.ID);
|
||||
}
|
||||
|
||||
boolean isDiscovering() {
|
||||
return bluetoothPlugin.isDiscovering();
|
||||
// TODO bluetoothPlugin.isDiscovering()
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +89,7 @@ class BluetoothConnecter implements EventListener {
|
||||
// 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 = (BluetoothPlugin) pluginManager.getPlugin(ID);
|
||||
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -172,84 +149,30 @@ class BluetoothConnecter implements EventListener {
|
||||
|
||||
@UiThread
|
||||
void onBluetoothDiscoverable(ContactItem contact) {
|
||||
contactId = contact.getContact().getId();
|
||||
connect();
|
||||
connect(contact.getContact().getId());
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
LOG.info("Contact connected to us");
|
||||
showToast(R.string.toast_connect_via_bluetooth_success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
pluginManager.setPluginEnabled(ID, true);
|
||||
|
||||
private void connect(ContactId contactId) {
|
||||
// TODO
|
||||
// * enable bluetooth connections setting, if not enabled
|
||||
// * wait for plugin to become active
|
||||
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);
|
||||
Random r = new Random();
|
||||
try {
|
||||
String uuid = null;
|
||||
try {
|
||||
uuid = transportPropertyManager
|
||||
.getRemoteProperties(contactId, ID).get(PROP_UUID);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
} catch (InterruptedException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -91,7 +91,7 @@ public class BluetoothConnecterDialogFragment extends DialogFragment {
|
||||
return;
|
||||
}
|
||||
if (bluetoothConnecter.isDiscovering()) {
|
||||
showToast(R.string.toast_connect_via_bluetooth_already_discovering);
|
||||
// TODO showToast(R.string.toast_connect_via_bluetooth_discovering);
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ 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;
|
||||
@@ -421,11 +420,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ class ConversationAdapter
|
||||
}
|
||||
items.beginBatchedUpdates();
|
||||
for (ConversationItem item : toRemove) items.remove(item);
|
||||
updateTimersInBatch();
|
||||
items.endBatchedUpdates();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?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>
|
||||
@@ -5,7 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/margin_medium">
|
||||
android:padding="@dimen/margin_medium"
|
||||
tools:context=".android.blog.RssFeedImportActivity">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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" />
|
||||
@@ -38,6 +38,15 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/huaweiProtectedAppsView" />
|
||||
|
||||
<org.briarproject.briar.android.account.XiaomiBatterySaverView
|
||||
android:id="@+id/xiaomiBatterySaverView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/margin_large"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/huaweiAppLaunchView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/next"
|
||||
style="@style/BriarButton"
|
||||
@@ -48,7 +57,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/huaweiAppLaunchView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/xiaomiBatterySaverView"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:enabled="true" />
|
||||
|
||||
|
||||
@@ -10,8 +10,13 @@
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_rss_feeds"
|
||||
android:title="@string/blogs_rss_feeds"
|
||||
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"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
@@ -40,9 +40,4 @@
|
||||
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>
|
||||
|
||||
@@ -461,12 +461,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -348,11 +348,13 @@
|
||||
<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-->
|
||||
|
||||
@@ -238,11 +238,13 @@
|
||||
<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-->
|
||||
|
||||
@@ -332,12 +332,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -421,12 +421,14 @@ 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-->
|
||||
|
||||
@@ -302,11 +302,13 @@
|
||||
<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-->
|
||||
|
||||
@@ -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,7 +162,6 @@
|
||||
<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>
|
||||
@@ -458,12 +457,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -162,7 +162,6 @@
|
||||
<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>
|
||||
@@ -458,12 +457,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -395,12 +395,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
<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>
|
||||
@@ -168,7 +165,6 @@
|
||||
<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>
|
||||
@@ -492,6 +488,7 @@
|
||||
<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>
|
||||
@@ -502,6 +499,7 @@
|
||||
|
||||
هر مخاطبی که با آن این خوراک را به اشتراک گذاشته اید ممکن است دیگر آپدیت دریافت نکند.</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>
|
||||
|
||||
@@ -314,12 +314,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -22,9 +22,6 @@
|
||||
<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 n’est 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 » n’est pas défini pour Briar dans l’écran « Lancement des applis », l’appli ne pourra pas fonctionner en arrière-plan.</string>
|
||||
<string name="warning_dozed">%s n’a pas pu fonctionner en arrière-plan</string>
|
||||
<!--Login-->
|
||||
<string name="enter_password">Mot de passe</string>
|
||||
@@ -162,7 +159,6 @@
|
||||
<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>
|
||||
@@ -173,7 +169,6 @@
|
||||
<!--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>
|
||||
@@ -458,12 +453,14 @@
|
||||
<string name="blogs_rss_feeds_import_button">Importer</string>
|
||||
<string name="blogs_rss_feeds_import_hint">Saisir l’URL du fil RSS</string>
|
||||
<string name="blogs_rss_feeds_import_error">Nous sommes désolés ! Une erreur est survenue lors de l’importation 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 d’autrui.\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 l’icô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-->
|
||||
@@ -560,17 +557,8 @@
|
||||
<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">L’activation de ce paramètre fera disparaître
|
||||
les messages de cette conversation automatiquement après 7\u00A0jours.
|
||||
\n\nPour l’exemplaire de l’expéditeur du message, le compte à rebours commence une fois qu’il 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="disappearing_messages_explanation_long">dsvkjfjhgvfdkjhgv dfkjgh fkjhsgkjfdkj fhfgfhg
|
||||
</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-->
|
||||
|
||||
@@ -454,12 +454,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -444,12 +444,14 @@
|
||||
<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-->
|
||||
|
||||
@@ -378,12 +378,14 @@
|
||||
<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-->
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user