Compare commits

..

1 Commits

Author SHA1 Message Date
akwizgran
327e12e9f8 Don't allow multiple messages to be queued in memory for validation at startup. 2020-09-24 15:42:18 +01:00
151 changed files with 1105 additions and 2543 deletions

View File

@@ -28,11 +28,6 @@
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" /> <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
</JavaCodeStyleSettings> </JavaCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<XML> <XML>

View File

@@ -3,4 +3,3 @@ gen
build build
.settings .settings
src/main/res/raw/*.zip src/main/res/raw/*.zip
src/main/jniLibs

View File

@@ -5,14 +5,14 @@ apply plugin: 'witness'
apply from: 'witness.gradle' apply from: 'witness.gradle'
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion 29
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion '29.0.2'
defaultConfig { defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion 16
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion 28
versionCode rootProject.ext.versionCode versionCode 10209
versionName rootProject.ext.versionName versionName "1.2.9"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -53,12 +53,10 @@ dependencies {
} }
def torBinariesDir = 'src/main/res/raw' def torBinariesDir = 'src/main/res/raw'
def torLibsDir = 'src/main/jniLibs'
task cleanTorBinaries { task cleanTorBinaries {
doLast { doLast {
delete fileTree(torBinariesDir) { include '*.zip' } delete fileTree(torBinariesDir) { include '*.zip' }
delete fileTree(torLibsDir) { include '**/*.so' }
} }
} }
@@ -69,36 +67,8 @@ task unpackTorBinaries {
copy { copy {
from configurations.tor.collect { zipTree(it) } from configurations.tor.collect { zipTree(it) }
into torBinariesDir into torBinariesDir
include 'geoip.zip' // TODO: Remove after next Tor upgrade, which won't include non-PIE binaries
} include 'geoip.zip', '*_pie.zip'
configurations.tor.each { outer ->
zipTree(outer).each { inner ->
if (inner.name.endsWith('_arm_pie.zip')) {
copy {
from zipTree(inner)
into torLibsDir
rename '(.*)', 'armeabi-v7a/lib$1.so'
}
} else if (inner.name.endsWith('_arm64_pie.zip')) {
copy {
from zipTree(inner)
into torLibsDir
rename '(.*)', 'arm64-v8a/lib$1.so'
}
} else if (inner.name.endsWith('_x86_pie.zip')) {
copy {
from zipTree(inner)
into torLibsDir
rename '(.*)', 'x86/lib$1.so'
}
} else if (inner.name.endsWith('_x86_64_pie.zip')) {
copy {
from zipTree(inner)
into torLibsDir
rename '(.*)', 'x86_64/lib$1.so'
}
}
}
} }
} }
dependsOn cleanTorBinaries dependsOn cleanTorBinaries
@@ -106,6 +76,5 @@ task unpackTorBinaries {
tasks.withType(MergeResources) { tasks.withType(MergeResources) {
inputs.dir torBinariesDir inputs.dir torBinariesDir
inputs.dir torLibsDir
dependsOn unpackTorBinaries dependsOn unpackTorBinaries
} }

View File

@@ -135,7 +135,6 @@ class AndroidBluetoothPlugin
@Override @Override
@Nullable @Nullable
String getBluetoothAddress() { String getBluetoothAddress() {
if (adapter == null) return null;
String address = AndroidUtils.getBluetoothAddress(app, adapter); String address = AndroidUtils.getBluetoothAddress(app, adapter);
return address.isEmpty() ? null : address; return address.isEmpty() ? null : address;
} }

View File

@@ -16,42 +16,19 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.util.AndroidUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.net.SocketFactory; import javax.net.SocketFactory;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class AndroidTorPlugin extends TorPlugin { class AndroidTorPlugin extends TorPlugin {
private static final List<String> LIBRARY_ARCHITECTURES =
asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64");
private static final String TOR_LIB_NAME = "libtor.so";
private static final String OBFS4_LIB_NAME = "libobfs4proxy.so";
private static final Logger LOG =
getLogger(AndroidTorPlugin.class.getName());
private final Application app; private final Application app;
private final AndroidWakeLock wakeLock; private final AndroidWakeLock wakeLock;
private final File torLib, obfs4Lib;
AndroidTorPlugin(Executor ioExecutor, AndroidTorPlugin(Executor ioExecutor,
Executor wakefulIoExecutor, Executor wakefulIoExecutor,
@@ -78,9 +55,6 @@ class AndroidTorPlugin extends TorPlugin {
maxIdleTime, torDirectory); maxIdleTime, torDirectory);
this.app = app; this.app = app;
wakeLock = wakeLockManager.createWakeLock("TorPlugin"); wakeLock = wakeLockManager.createWakeLock("TorPlugin");
String nativeLibDir = app.getApplicationInfo().nativeLibraryDir;
torLib = new File(nativeLibDir, TOR_LIB_NAME);
obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME);
} }
@Override @Override
@@ -111,112 +85,4 @@ class AndroidTorPlugin extends TorPlugin {
super.stop(); super.stop();
wakeLock.release(); wakeLock.release();
} }
@Override
protected File getTorExecutableFile() {
return torLib.exists() ? torLib : super.getTorExecutableFile();
}
@Override
protected File getObfs4ExecutableFile() {
return obfs4Lib.exists() ? obfs4Lib : super.getObfs4ExecutableFile();
}
@Override
protected void installTorExecutable() throws IOException {
File extracted = super.getTorExecutableFile();
if (torLib.exists()) {
// If an older version left behind a Tor binary, delete it
if (extracted.exists()) {
if (extracted.delete()) LOG.info("Deleted Tor binary");
else LOG.info("Failed to delete Tor binary");
}
} else if (SDK_INT < 29) {
// The binary wasn't extracted at install time. Try to extract it
extractLibraryFromApk(TOR_LIB_NAME, extracted);
} else {
// No point extracting the binary, we won't be allowed to execute it
throw new FileNotFoundException(torLib.getAbsolutePath());
}
}
@Override
protected void installObfs4Executable() throws IOException {
File extracted = super.getObfs4ExecutableFile();
if (obfs4Lib.exists()) {
// If an older version left behind an obfs4 binary, delete it
if (extracted.exists()) {
if (extracted.delete()) LOG.info("Deleted obfs4 binary");
else LOG.info("Failed to delete obfs4 binary");
}
} else if (SDK_INT < 29) {
// The binary wasn't extracted at install time. Try to extract it
extractLibraryFromApk(OBFS4_LIB_NAME, extracted);
} else {
// No point extracting the binary, we won't be allowed to execute it
throw new FileNotFoundException(obfs4Lib.getAbsolutePath());
}
}
private void extractLibraryFromApk(String libName, File dest)
throws IOException {
File sourceDir = new File(app.getApplicationInfo().sourceDir);
if (sourceDir.isFile()) {
// Look for other APK files in the same directory, if we're allowed
File parent = sourceDir.getParentFile();
if (parent != null) sourceDir = parent;
}
List<String> libPaths = getSupportedLibraryPaths(libName);
for (File apk : findApkFiles(sourceDir)) {
ZipInputStream zin = new ZipInputStream(new FileInputStream(apk));
for (ZipEntry e = zin.getNextEntry(); e != null;
e = zin.getNextEntry()) {
if (libPaths.contains(e.getName())) {
if (LOG.isLoggable(INFO)) {
LOG.info("Extracting " + e.getName()
+ " from " + apk.getAbsolutePath());
}
extract(zin, dest); // Zip input stream will be closed
return;
}
}
zin.close();
}
throw new FileNotFoundException(libName);
}
/**
* Returns all files with the extension .apk or .APK under the given root.
*/
private List<File> findApkFiles(File root) {
List<File> files = new ArrayList<>();
findApkFiles(root, files);
return files;
}
private void findApkFiles(File f, List<File> files) {
if (f.isFile() && f.getName().toLowerCase().endsWith(".apk")) {
files.add(f);
} else if (f.isDirectory()) {
File[] children = f.listFiles();
if (children != null) {
for (File child : children) findApkFiles(child, files);
}
}
}
/**
* Returns the paths at which libraries with the given name would be found
* inside an APK file, for all architectures supported by the device, in
* order of preference.
*/
private List<String> getSupportedLibraryPaths(String libName) {
List<String> architectures = new ArrayList<>();
for (String abi : AndroidUtils.getSupportedArchitectures()) {
if (LIBRARY_ARCHITECTURES.contains(abi)) {
architectures.add("lib/" + abi + "/" + libName);
}
}
return architectures;
}
} }

View File

@@ -1,25 +0,0 @@
package org.briarproject.bramble.api;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
/**
* An item that can be consumed once.
*/
@NotNullByDefault
public class Consumable<T> {
private final AtomicReference<T> reference;
public Consumable(T item) {
reference = new AtomicReference<>(item);
}
@Nullable
public T consume() {
return reference.getAndSet(null);
}
}

View File

@@ -35,24 +35,24 @@ public interface ClientHelper {
Message createMessageForStoringMetadata(GroupId g); Message createMessageForStoringMetadata(GroupId g);
Message getSmallMessage(MessageId m) throws DbException; Message getMessage(MessageId m) throws DbException;
Message getSmallMessage(Transaction txn, MessageId m) throws DbException; Message getMessage(Transaction txn, MessageId m) throws DbException;
BdfList getSmallMessageAsList(MessageId m) BdfList getMessageAsList(MessageId m) throws DbException, FormatException;
throws DbException, FormatException;
BdfList getSmallMessageAsList(Transaction txn, MessageId m) BdfList getMessageAsList(Transaction txn, MessageId m) throws DbException,
throws DbException, FormatException; FormatException;
BdfDictionary getGroupMetadataAsDictionary(GroupId g) BdfDictionary getGroupMetadataAsDictionary(GroupId g) throws DbException,
throws DbException, FormatException; FormatException;
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g) BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
throws DbException, FormatException; throws DbException, FormatException;
BdfDictionary getMessageMetadataAsDictionary(MessageId m) BdfDictionary getMessageMetadataAsDictionary(MessageId m)
throws DbException, FormatException; throws DbException,
FormatException;
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m) BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
throws DbException, FormatException; throws DbException, FormatException;
@@ -67,8 +67,8 @@ public interface ClientHelper {
BdfDictionary query) throws DbException, FormatException; BdfDictionary query) throws DbException, FormatException;
Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary( Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary(
Transaction txn, GroupId g, BdfDictionary query) Transaction txn, GroupId g, BdfDictionary query) throws DbException,
throws DbException, FormatException; FormatException;
void mergeGroupMetadata(GroupId g, BdfDictionary metadata) void mergeGroupMetadata(GroupId g, BdfDictionary metadata)
throws DbException, FormatException; throws DbException, FormatException;

View File

@@ -163,23 +163,27 @@ public interface DatabaseComponent extends TransactionManager {
* less than or equal to the given length, for transmission over a * less than or equal to the given length, for transmission over a
* transport with the given maximum latency. Returns null if there are no * transport with the given maximum latency. Returns null if there are no
* sendable messages that fit in the given length. * sendable messages that fit in the given length.
*
* @param small True if only single-block messages should be sent
*/ */
@Nullable @Nullable
Collection<Message> generateBatch(Transaction txn, ContactId c, Collection<Message> generateBatch(Transaction txn, ContactId c,
int maxLength, int maxLatency, boolean small) throws DbException; int maxLength, int maxLatency) throws DbException;
/** /**
* Returns an offer for the given contact for transmission over a * Returns an offer for the given contact for transmission over a
* transport with the given maximum latency, or null if there are no * transport with the given maximum latency, or null if there are no
* messages to offer. * messages to offer.
*
* @param small True if only single-block messages should be offered
*/ */
@Nullable @Nullable
Offer generateOffer(Transaction txn, ContactId c, int maxMessages, Offer generateOffer(Transaction txn, ContactId c, int maxMessages,
int maxLatency, boolean small) throws DbException; int maxLatency) throws DbException;
/**
* Returns a request for the given contact, or null if there are no
* messages to request.
*/
@Nullable
Request generateRequest(Transaction txn, ContactId c, int maxMessages)
throws DbException;
/** /**
* Returns a batch of messages for the given contact, with a total length * Returns a batch of messages for the given contact, with a total length
@@ -268,14 +272,13 @@ public interface DatabaseComponent extends TransactionManager {
Collection<Identity> getIdentities(Transaction txn) throws DbException; Collection<Identity> getIdentities(Transaction txn) throws DbException;
/** /**
* Returns the single-block message with the given ID. * Returns the message with the given ID.
* <p/> * <p/>
* Read-only. * Read-only.
* *
* @throws MessageDeletedException if the message has been deleted * @throws MessageDeletedException if the message has been deleted
* @throws MessageTooLargeException if the message has more than one block
*/ */
Message getSmallMessage(Transaction txn, MessageId m) throws DbException; Message getMessage(Transaction txn, MessageId m) throws DbException;
/** /**
* Returns the IDs of all delivered messages in the given group. * Returns the IDs of all delivered messages in the given group.

View File

@@ -1,8 +0,0 @@
package org.briarproject.bramble.api.db;
/**
* Thrown when a multi-block message is requested from the database via a
* method that is only suitable for requesting single-block messages.
*/
public class MessageTooLargeException extends DbException {
}

View File

@@ -29,15 +29,10 @@ public interface SyncConstants {
*/ */
int MESSAGE_HEADER_LENGTH = UniqueId.LENGTH + 8; int MESSAGE_HEADER_LENGTH = UniqueId.LENGTH + 8;
/**
* The maximum length of a block in bytes.
*/
int MAX_BLOCK_LENGTH = 32 * 1024; // 32 KiB
/** /**
* The maximum length of a message body in bytes. * The maximum length of a message body in bytes.
*/ */
int MAX_MESSAGE_BODY_LENGTH = MAX_BLOCK_LENGTH; int MAX_MESSAGE_BODY_LENGTH = 32 * 1024; // 32 KiB
/** /**
* The maximum length of a message in bytes. * The maximum length of a message in bytes.

View File

@@ -7,16 +7,16 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
/** /**
* An event that is broadcast when one or more messages are received from, or * An event that is broadcast when a message is received from, or offered by, a
* offered by, a contact and need to be acknowledged. * contact and needs to be acknowledged.
*/ */
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class MessagesToAckEvent extends Event { public class MessageToAckEvent extends Event {
private final ContactId contactId; private final ContactId contactId;
public MessagesToAckEvent(ContactId contactId) { public MessageToAckEvent(ContactId contactId) {
this.contactId = contactId; this.contactId = contactId;
} }

View File

@@ -0,0 +1,26 @@
package org.briarproject.bramble.api.sync.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a message is offered by a contact and needs
* to be requested.
*/
@Immutable
@NotNullByDefault
public class MessageToRequestEvent extends Event {
private final ContactId contactId;
public MessageToRequestEvent(ContactId contactId) {
this.contactId = contactId;
}
public ContactId getContactId() {
return contactId;
}
}

View File

@@ -1,39 +0,0 @@
package org.briarproject.bramble.api.sync.event;
import org.briarproject.bramble.api.Consumable;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.Collection;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when one or more messages are offered by a
* contact and need to be requested.
*/
@Immutable
@NotNullByDefault
public class MessagesToRequestEvent extends Event {
private final ContactId contactId;
private final Consumable<Collection<MessageId>> ids;
public MessagesToRequestEvent(ContactId contactId,
Collection<MessageId> ids) {
this.contactId = contactId;
this.ids = new Consumable<>(ids);
}
public ContactId getContactId() {
return contactId;
}
@Nullable
public Collection<MessageId> consumeIds() {
return ids.consume();
}
}

View File

@@ -116,27 +116,25 @@ class ClientHelperImpl implements ClientHelper {
} }
@Override @Override
public Message getSmallMessage(MessageId m) throws DbException { public Message getMessage(MessageId m) throws DbException {
return db.transactionWithResult(true, txn -> getSmallMessage(txn, m)); return db.transactionWithResult(true, txn -> getMessage(txn, m));
} }
@Override @Override
public Message getSmallMessage(Transaction txn, MessageId m) public Message getMessage(Transaction txn, MessageId m) throws DbException {
throws DbException { return db.getMessage(txn, m);
return db.getSmallMessage(txn, m);
} }
@Override @Override
public BdfList getSmallMessageAsList(MessageId m) public BdfList getMessageAsList(MessageId m) throws DbException,
FormatException {
return db.transactionWithResult(true, txn -> getMessageAsList(txn, m));
}
@Override
public BdfList getMessageAsList(Transaction txn, MessageId m)
throws DbException, FormatException { throws DbException, FormatException {
return db.transactionWithResult(true, txn -> return toList(db.getMessage(txn, m).getBody());
getSmallMessageAsList(txn, m));
}
@Override
public BdfList getSmallMessageAsList(Transaction txn, MessageId m)
throws DbException, FormatException {
return toList(db.getSmallMessage(txn, m).getBody());
} }
@Override @Override

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.db.DataTooOldException;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.MessageDeletedException; import org.briarproject.bramble.api.db.MessageDeletedException;
import org.briarproject.bramble.api.db.MessageTooLargeException;
import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.MigrationListener; import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
@@ -126,6 +125,11 @@ interface Database<T> {
void addMessageDependency(T txn, Message dependent, MessageId dependency, void addMessageDependency(T txn, Message dependent, MessageId dependency,
MessageState dependentState) throws DbException; MessageState dependentState) throws DbException;
/**
* Records that a message has been offered by the given contact.
*/
void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException;
/** /**
* Stores a pending contact. * Stores a pending contact.
*/ */
@@ -214,6 +218,13 @@ interface Database<T> {
boolean containsVisibleMessage(T txn, ContactId c, MessageId m) boolean containsVisibleMessage(T txn, ContactId c, MessageId m)
throws DbException; throws DbException;
/**
* Returns the number of messages offered by the given contact.
* <p/>
* Read-only.
*/
int countOfferedMessages(T txn, ContactId c) throws DbException;
/** /**
* Deletes the message with the given ID. Unlike * Deletes the message with the given ID. Unlike
* {@link #removeMessage(Object, MessageId)}, the message ID and any other * {@link #removeMessage(Object, MessageId)}, the message ID and any other
@@ -321,14 +332,13 @@ interface Database<T> {
Collection<Identity> getIdentities(T txn) throws DbException; Collection<Identity> getIdentities(T txn) throws DbException;
/** /**
* Returns the single-block message with the given ID. * Returns the message with the given ID.
* <p/> * <p/>
* Read-only. * Read-only.
* *
* @throws MessageDeletedException if the message has been deleted * @throws MessageDeletedException if the message has been deleted
* @throws MessageTooLargeException if the message has more than one block
*/ */
Message getSmallMessage(T txn, MessageId m) throws DbException; Message getMessage(T txn, MessageId m) throws DbException;
/** /**
* Returns the IDs and states of all dependencies of the given message. * Returns the IDs and states of all dependencies of the given message.
@@ -447,13 +457,13 @@ interface Database<T> {
int maxMessages, int maxLatency) throws DbException; int maxMessages, int maxLatency) throws DbException;
/** /**
* Returns the IDs of some single-block messages that are eligible to be * Returns the IDs of some messages that are eligible to be requested from
* offered to the given contact, up to the given number of messages. * the given contact, up to the given number of messages.
* <p/> * <p/>
* Read-only. * Read-only.
*/ */
Collection<MessageId> getSmallMessagesToOffer(T txn, ContactId c, Collection<MessageId> getMessagesToRequest(T txn, ContactId c,
int maxMessages, int maxLatency) throws DbException; int maxMessages) throws DbException;
/** /**
* Returns the IDs of some messages that are eligible to be sent to the * Returns the IDs of some messages that are eligible to be sent to the
@@ -464,15 +474,6 @@ interface Database<T> {
Collection<MessageId> getMessagesToSend(T txn, ContactId c, int maxLength, Collection<MessageId> getMessagesToSend(T txn, ContactId c, int maxLength,
int maxLatency) throws DbException; int maxLatency) throws DbException;
/**
* Returns the IDs of some single-block messages that are eligible to be
* sent to the given contact, up to the given total length.
* <p/>
* Read-only.
*/
Collection<MessageId> getSmallMessagesToSend(T txn, ContactId c,
int maxLength, int maxLatency) throws DbException;
/** /**
* Returns the IDs of any messages that need to be validated. * Returns the IDs of any messages that need to be validated.
* <p/> * <p/>
@@ -635,6 +636,13 @@ interface Database<T> {
*/ */
void removeMessage(T txn, MessageId m) throws DbException; void removeMessage(T txn, MessageId m) throws DbException;
/**
* Removes the given offered messages that were offered by the given
* contact.
*/
void removeOfferedMessages(T txn, ContactId c,
Collection<MessageId> requested) throws DbException;
/** /**
* Removes a pending contact (and all associated state) from the database. * Removes a pending contact (and all associated state) from the database.
*/ */

View File

@@ -61,10 +61,10 @@ import org.briarproject.bramble.api.sync.event.MessageAddedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent; import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent; import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent; import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent;
import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
import org.briarproject.bramble.api.sync.event.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.sync.event.MessagesToAckEvent;
import org.briarproject.bramble.api.sync.event.MessagesToRequestEvent;
import org.briarproject.bramble.api.sync.event.SyncVersionsUpdatedEvent; import org.briarproject.bramble.api.sync.event.SyncVersionsUpdatedEvent;
import org.briarproject.bramble.api.sync.validation.MessageState; import org.briarproject.bramble.api.sync.validation.MessageState;
import org.briarproject.bramble.api.transport.KeySetId; import org.briarproject.bramble.api.transport.KeySetId;
@@ -91,6 +91,7 @@ import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN; import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
@@ -405,22 +406,19 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Nullable @Nullable
@Override @Override
public Collection<Message> generateBatch(Transaction transaction, public Collection<Message> generateBatch(Transaction transaction,
ContactId c, int maxLength, int maxLatency, boolean small) ContactId c, int maxLength, int maxLatency) throws DbException {
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
Collection<MessageId> ids; Collection<MessageId> ids =
if (small) db.getMessagesToSend(txn, c, maxLength, maxLatency);
ids = db.getSmallMessagesToSend(txn, c, maxLength, maxLatency);
else ids = db.getMessagesToSend(txn, c, maxLength, maxLatency);
if (ids.isEmpty()) return null;
List<Message> messages = new ArrayList<>(ids.size()); List<Message> messages = new ArrayList<>(ids.size());
for (MessageId m : ids) { for (MessageId m : ids) {
messages.add(db.getSmallMessage(txn, m)); messages.add(db.getMessage(txn, m));
db.updateExpiryTimeAndEta(txn, c, m, maxLatency); db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
} }
if (ids.isEmpty()) return null;
db.lowerRequestedFlag(txn, c, ids); db.lowerRequestedFlag(txn, c, ids);
transaction.attach(new MessagesSentEvent(c, ids)); transaction.attach(new MessagesSentEvent(c, ids));
return messages; return messages;
@@ -429,21 +427,34 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Nullable @Nullable
@Override @Override
public Offer generateOffer(Transaction transaction, ContactId c, public Offer generateOffer(Transaction transaction, ContactId c,
int maxMessages, int maxLatency, boolean small) throws DbException { int maxMessages, int maxLatency) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
Collection<MessageId> ids; Collection<MessageId> ids =
if (small) db.getMessagesToOffer(txn, c, maxMessages, maxLatency);
ids = db.getSmallMessagesToOffer(txn, c, maxMessages, maxLatency);
else ids = db.getMessagesToOffer(txn, c, maxMessages, maxLatency);
if (ids.isEmpty()) return null; if (ids.isEmpty()) return null;
for (MessageId m : ids) for (MessageId m : ids)
db.updateExpiryTimeAndEta(txn, c, m, maxLatency); db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
return new Offer(ids); return new Offer(ids);
} }
@Nullable
@Override
public Request generateRequest(Transaction transaction, ContactId c,
int maxMessages) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsContact(txn, c))
throw new NoSuchContactException();
Collection<MessageId> ids = db.getMessagesToRequest(txn, c,
maxMessages);
if (ids.isEmpty()) return null;
db.removeOfferedMessages(txn, c, ids);
return new Request(ids);
}
@Nullable @Nullable
@Override @Override
public Collection<Message> generateRequestedBatch(Transaction transaction, public Collection<Message> generateRequestedBatch(Transaction transaction,
@@ -454,12 +465,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
throw new NoSuchContactException(); throw new NoSuchContactException();
Collection<MessageId> ids = Collection<MessageId> ids =
db.getRequestedMessagesToSend(txn, c, maxLength, maxLatency); db.getRequestedMessagesToSend(txn, c, maxLength, maxLatency);
if (ids.isEmpty()) return null;
List<Message> messages = new ArrayList<>(ids.size()); List<Message> messages = new ArrayList<>(ids.size());
for (MessageId m : ids) { for (MessageId m : ids) {
messages.add(db.getSmallMessage(txn, m)); messages.add(db.getMessage(txn, m));
db.updateExpiryTimeAndEta(txn, c, m, maxLatency); db.updateExpiryTimeAndEta(txn, c, m, maxLatency);
} }
if (ids.isEmpty()) return null;
db.lowerRequestedFlag(txn, c, ids); db.lowerRequestedFlag(txn, c, ids);
transaction.attach(new MessagesSentEvent(c, ids)); transaction.attach(new MessagesSentEvent(c, ids));
return messages; return messages;
@@ -548,12 +559,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
} }
@Override @Override
public Message getSmallMessage(Transaction transaction, MessageId m) public Message getMessage(Transaction transaction, MessageId m)
throws DbException { throws DbException {
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsMessage(txn, m)) if (!db.containsMessage(txn, m))
throw new NoSuchMessageException(); throw new NoSuchMessageException();
return db.getSmallMessage(txn, m); return db.getMessage(txn, m);
} }
@Override @Override
@@ -808,7 +819,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
db.addMessage(txn, m, UNKNOWN, false, false, c); db.addMessage(txn, m, UNKNOWN, false, false, c);
transaction.attach(new MessageAddedEvent(m, c)); transaction.attach(new MessageAddedEvent(m, c));
} }
transaction.attach(new MessagesToAckEvent(c)); transaction.attach(new MessageToAckEvent(c));
} }
} }
@@ -819,20 +830,21 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsContact(txn, c)) if (!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
boolean ack = false; boolean ack = false, request = false;
List<MessageId> request = new ArrayList<>(o.getMessageIds().size()); int count = db.countOfferedMessages(txn, c);
for (MessageId m : o.getMessageIds()) { for (MessageId m : o.getMessageIds()) {
if (db.containsVisibleMessage(txn, c, m)) { if (db.containsVisibleMessage(txn, c, m)) {
db.raiseSeenFlag(txn, c, m); db.raiseSeenFlag(txn, c, m);
db.raiseAckFlag(txn, c, m); db.raiseAckFlag(txn, c, m);
ack = true; ack = true;
} else { } else if (count < MAX_OFFERED_MESSAGES) {
request.add(m); db.addOfferedMessage(txn, c, m);
request = true;
count++;
} }
} }
if (ack) transaction.attach(new MessagesToAckEvent(c)); if (ack) transaction.attach(new MessageToAckEvent(c));
if (!request.isEmpty()) if (request) transaction.attach(new MessageToRequestEvent(c));
transaction.attach(new MessagesToRequestEvent(c, request));
} }
@Override @Override

View File

@@ -6,6 +6,13 @@ import static java.util.concurrent.TimeUnit.DAYS;
interface DatabaseConstants { interface DatabaseConstants {
/**
* The maximum number of offered messages from each contact that will be
* stored. If offers arrive more quickly than requests can be sent and this
* limit is reached, additional offers will not be stored.
*/
int MAX_OFFERED_MESSAGES = 1000;
/** /**
* The namespace of the {@link Settings} where the database schema version * The namespace of the {@link Settings} where the database schema version
* is stored. * is stored.

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventExecutor; import org.briarproject.bramble.api.event.EventExecutor;
import org.briarproject.bramble.api.lifecycle.ShutdownManager; import org.briarproject.bramble.api.lifecycle.ShutdownManager;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import java.sql.Connection; import java.sql.Connection;
@@ -21,8 +22,9 @@ public class DatabaseModule {
@Provides @Provides
@Singleton @Singleton
Database<Connection> provideDatabase(DatabaseConfig config, Clock clock) { Database<Connection> provideDatabase(DatabaseConfig config,
return new H2Database(config, clock); MessageFactory messageFactory, Clock clock) {
return new H2Database(config, messageFactory, clock);
} }
@Provides @Provides

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.DbClosedException;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.MigrationListener; import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
@@ -50,8 +51,9 @@ class H2Database extends JdbcDatabase {
private volatile SecretKey key = null; private volatile SecretKey key = null;
@Inject @Inject
H2Database(DatabaseConfig config, Clock clock) { H2Database(DatabaseConfig config, MessageFactory messageFactory,
super(dbTypes, clock); Clock clock) {
super(dbTypes, messageFactory, clock);
this.config = config; this.config = config;
File dir = config.getDatabaseDirectory(); File dir = config.getDatabaseDirectory();
String path = new File(dir, "db").getAbsolutePath(); String path = new File(dir, "db").getAbsolutePath();

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.DbClosedException;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.MigrationListener; import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
@@ -50,8 +51,9 @@ class HyperSqlDatabase extends JdbcDatabase {
private volatile SecretKey key = null; private volatile SecretKey key = null;
@Inject @Inject
HyperSqlDatabase(DatabaseConfig config, Clock clock) { HyperSqlDatabase(DatabaseConfig config, MessageFactory messageFactory,
super(dbTypes, clock); Clock clock) {
super(dbTypes, messageFactory, clock);
this.config = config; this.config = config;
File dir = config.getDatabaseDirectory(); File dir = config.getDatabaseDirectory();
String path = new File(dir, "db").getAbsolutePath(); String path = new File(dir, "db").getAbsolutePath();

View File

@@ -16,7 +16,6 @@ import org.briarproject.bramble.api.db.DataTooOldException;
import org.briarproject.bramble.api.db.DbClosedException; import org.briarproject.bramble.api.db.DbClosedException;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.MessageDeletedException; import org.briarproject.bramble.api.db.MessageDeletedException;
import org.briarproject.bramble.api.db.MessageTooLargeException;
import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.MigrationListener; import org.briarproject.bramble.api.db.MigrationListener;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
@@ -31,6 +30,7 @@ import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.Group.Visibility; import org.briarproject.bramble.api.sync.Group.Visibility;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.validation.MessageState; import org.briarproject.bramble.api.sync.validation.MessageState;
@@ -76,7 +76,6 @@ import static org.briarproject.bramble.api.db.Metadata.REMOVE;
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_BLOCK_LENGTH;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING; import static org.briarproject.bramble.api.sync.validation.MessageState.PENDING;
@@ -99,7 +98,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
abstract class JdbcDatabase implements Database<Connection> { abstract class JdbcDatabase implements Database<Connection> {
// Package access for testing // Package access for testing
static final int CODE_SCHEMA_VERSION = 49; static final int CODE_SCHEMA_VERSION = 47;
// Time period offsets for incoming transport keys // Time period offsets for incoming transport keys
private static final int OFFSET_PREV = -1; private static final int OFFSET_PREV = -1;
@@ -181,9 +180,8 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " state INT NOT NULL," + " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL," + " shared BOOLEAN NOT NULL,"
+ " temporary BOOLEAN NOT NULL," + " temporary BOOLEAN NOT NULL,"
+ " length INT NOT NULL," // Includes message header + " length INT NOT NULL,"
+ " deleted BOOLEAN NOT NULL," + " raw BLOB," // Null if message has been deleted
+ " blockCount INT NOT NULL,"
+ " PRIMARY KEY (messageId)," + " PRIMARY KEY (messageId),"
+ " FOREIGN KEY (groupId)" + " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)" + " REFERENCES groups (groupId)"
@@ -220,15 +218,13 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " REFERENCES messages (messageId)" + " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String CREATE_BLOCKS = private static final String CREATE_OFFERS =
"CREATE TABLE blocks" "CREATE TABLE offers"
+ " (messageId _HASH NOT NULL," + " (messageId _HASH NOT NULL," // Not a foreign key
+ " blockNumber INT NOT NULL," + " contactId INT NOT NULL,"
+ " blockLength INT NOT NULL," // Excludes block header + " PRIMARY KEY (messageId, contactId),"
+ " data BLOB," // Null if message has been deleted + " FOREIGN KEY (contactId)"
+ " PRIMARY KEY (messageId, blockNumber)," + " REFERENCES contacts (contactId)"
+ " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String CREATE_STATUSES = private static final String CREATE_STATUSES =
@@ -343,6 +339,7 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final Logger LOG = private static final Logger LOG =
getLogger(JdbcDatabase.class.getName()); getLogger(JdbcDatabase.class.getName());
private final MessageFactory messageFactory;
private final Clock clock; private final Clock clock;
private final DatabaseTypes dbTypes; private final DatabaseTypes dbTypes;
@@ -362,8 +359,10 @@ abstract class JdbcDatabase implements Database<Connection> {
protected abstract void compactAndClose() throws DbException; protected abstract void compactAndClose() throws DbException;
JdbcDatabase(DatabaseTypes databaseTypes, Clock clock) { JdbcDatabase(DatabaseTypes databaseTypes, MessageFactory messageFactory,
Clock clock) {
this.dbTypes = databaseTypes; this.dbTypes = databaseTypes;
this.messageFactory = messageFactory;
this.clock = clock; this.clock = clock;
} }
@@ -441,12 +440,10 @@ abstract class JdbcDatabase implements Database<Connection> {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Migrating from schema " + start + " to " + end); LOG.info("Migrating from schema " + start + " to " + end);
if (listener != null) listener.onDatabaseMigration(); if (listener != null) listener.onDatabaseMigration();
long startTime = now();
// Apply the migration // Apply the migration
m.migrate(txn); m.migrate(txn);
// Store the new schema version // Store the new schema version
storeSchemaVersion(txn, end); storeSchemaVersion(txn, end);
logDuration(LOG, "Migration", startTime);
dataSchemaVersion = end; dataSchemaVersion = end;
} }
} }
@@ -466,9 +463,7 @@ abstract class JdbcDatabase implements Database<Connection> {
new Migration43_44(dbTypes), new Migration43_44(dbTypes),
new Migration44_45(), new Migration44_45(),
new Migration45_46(), new Migration45_46(),
new Migration46_47(dbTypes), new Migration46_47(dbTypes)
new Migration47_48(dbTypes),
new Migration48_49()
); );
} }
@@ -513,7 +508,7 @@ abstract class JdbcDatabase implements Database<Connection> {
s.executeUpdate(dbTypes.replaceTypes(CREATE_MESSAGES)); s.executeUpdate(dbTypes.replaceTypes(CREATE_MESSAGES));
s.executeUpdate(dbTypes.replaceTypes(CREATE_MESSAGE_METADATA)); s.executeUpdate(dbTypes.replaceTypes(CREATE_MESSAGE_METADATA));
s.executeUpdate(dbTypes.replaceTypes(CREATE_MESSAGE_DEPENDENCIES)); s.executeUpdate(dbTypes.replaceTypes(CREATE_MESSAGE_DEPENDENCIES));
s.executeUpdate(dbTypes.replaceTypes(CREATE_BLOCKS)); s.executeUpdate(dbTypes.replaceTypes(CREATE_OFFERS));
s.executeUpdate(dbTypes.replaceTypes(CREATE_STATUSES)); s.executeUpdate(dbTypes.replaceTypes(CREATE_STATUSES));
s.executeUpdate(dbTypes.replaceTypes(CREATE_TRANSPORTS)); s.executeUpdate(dbTypes.replaceTypes(CREATE_TRANSPORTS));
s.executeUpdate(dbTypes.replaceTypes(CREATE_PENDING_CONTACTS)); s.executeUpdate(dbTypes.replaceTypes(CREATE_PENDING_CONTACTS));
@@ -731,7 +726,7 @@ abstract class JdbcDatabase implements Database<Connection> {
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT messageId, timestamp, state, shared," String sql = "SELECT messageId, timestamp, state, shared,"
+ " length, deleted" + " length, raw IS NULL"
+ " FROM messages" + " FROM messages"
+ " WHERE groupId = ?"; + " WHERE groupId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
@@ -744,8 +739,9 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean messageShared = rs.getBoolean(4); boolean messageShared = rs.getBoolean(4);
int length = rs.getInt(5); int length = rs.getInt(5);
boolean deleted = rs.getBoolean(6); boolean deleted = rs.getBoolean(6);
boolean seen = removeOfferedMessage(txn, c, id);
addStatus(txn, id, c, g, timestamp, length, state, groupShared, addStatus(txn, id, c, g, timestamp, length, state, groupShared,
messageShared, deleted, false); messageShared, deleted, seen);
} }
rs.close(); rs.close();
ps.close(); ps.close();
@@ -792,8 +788,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "INSERT INTO messages (messageId, groupId, timestamp," String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
+ " state, shared, temporary, length, deleted, blockCount)" + " state, shared, temporary, length, raw)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, FALSE, 1)"; + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes()); ps.setBytes(1, m.getId().getBytes());
ps.setBytes(2, m.getGroupId().getBytes()); ps.setBytes(2, m.getGroupId().getBytes());
@@ -801,29 +797,21 @@ abstract class JdbcDatabase implements Database<Connection> {
ps.setInt(4, state.getValue()); ps.setInt(4, state.getValue());
ps.setBoolean(5, shared); ps.setBoolean(5, shared);
ps.setBoolean(6, temporary); ps.setBoolean(6, temporary);
ps.setInt(7, m.getRawLength()); byte[] raw = messageFactory.getRawMessage(m);
ps.setInt(7, raw.length);
ps.setBytes(8, raw);
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException(); if (affected != 1) throw new DbStateException();
ps.close(); ps.close();
sql = "INSERT INTO blocks (messageId, blockNumber, blockLength,"
+ " data)"
+ " VALUES (?, 0, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getId().getBytes());
ps.setInt(2, m.getBody().length);
ps.setBytes(3, m.getBody());
affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
// Create a status row for each contact that can see the group // Create a status row for each contact that can see the group
Map<ContactId, Boolean> visibility = Map<ContactId, Boolean> visibility =
getGroupVisibility(txn, m.getGroupId()); getGroupVisibility(txn, m.getGroupId());
for (Entry<ContactId, Boolean> e : visibility.entrySet()) { for (Entry<ContactId, Boolean> e : visibility.entrySet()) {
ContactId c = e.getKey(); ContactId c = e.getKey();
boolean seen = c.equals(sender); boolean offered = removeOfferedMessage(txn, c, m.getId());
boolean seen = offered || c.equals(sender);
addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(), addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(),
m.getRawLength(), state, e.getValue(), shared, false, raw.length, state, e.getValue(), shared, false, seen);
seen);
} }
// Update denormalised column in messageDependencies if dependency // Update denormalised column in messageDependencies if dependency
// is in same group as dependent // is in same group as dependent
@@ -842,6 +830,37 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public void addOfferedMessage(Connection txn, ContactId c, MessageId m)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT NULL FROM offers"
+ " WHERE messageId = ? AND contactId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
rs = ps.executeQuery();
boolean found = rs.next();
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
if (found) return;
sql = "INSERT INTO offers (messageId, contactId) VALUES (?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
ps.setInt(2, c.getInt());
int affected = ps.executeUpdate();
if (affected != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
private void addStatus(Connection txn, MessageId m, ContactId c, GroupId g, private void addStatus(Connection txn, MessageId m, ContactId c, GroupId g,
long timestamp, int length, MessageState state, boolean groupShared, long timestamp, int length, MessageState state, boolean groupShared,
boolean messageShared, boolean deleted, boolean seen) boolean messageShared, boolean deleted, boolean seen)
@@ -1243,22 +1262,40 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public int countOfferedMessages(Connection txn, ContactId c)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT COUNT (messageId) FROM offers "
+ " WHERE contactId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
rs = ps.executeQuery();
if (!rs.next()) throw new DbException();
int count = rs.getInt(1);
if (rs.next()) throw new DbException();
rs.close();
ps.close();
return count;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public void deleteMessage(Connection txn, MessageId m) throws DbException { public void deleteMessage(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE messages SET deleted = TRUE" String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?";
+ " WHERE messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, m.getBytes());
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
sql = "UPDATE blocks SET data = NULL WHERE messageId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException(); if (affected < 0) throw new DbStateException();
if (affected > 1) throw new DbStateException();
ps.close(); ps.close();
// Update denormalised column in statuses // Update denormalised column in statuses
sql = "UPDATE statuses SET deleted = TRUE WHERE messageId = ?"; sql = "UPDATE statuses SET deleted = TRUE WHERE messageId = ?";
@@ -1651,13 +1688,11 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public Message getSmallMessage(Connection txn, MessageId m) public Message getMessage(Connection txn, MessageId m) throws DbException {
throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT groupId, timestamp, deleted, blockCount" String sql = "SELECT groupId, timestamp, raw FROM messages"
+ " FROM messages"
+ " WHERE messageId = ?"; + " WHERE messageId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, m.getBytes()); ps.setBytes(1, m.getBytes());
@@ -1665,25 +1700,15 @@ abstract class JdbcDatabase implements Database<Connection> {
if (!rs.next()) throw new DbStateException(); if (!rs.next()) throw new DbStateException();
GroupId g = new GroupId(rs.getBytes(1)); GroupId g = new GroupId(rs.getBytes(1));
long timestamp = rs.getLong(2); long timestamp = rs.getLong(2);
boolean deleted = rs.getBoolean(3); byte[] raw = rs.getBytes(3);
int blockCount = rs.getInt(4);
if (rs.next()) throw new DbStateException(); if (rs.next()) throw new DbStateException();
rs.close(); rs.close();
ps.close(); ps.close();
if (deleted) throw new MessageDeletedException(); if (raw == null) throw new MessageDeletedException();
if (blockCount > 1) throw new MessageTooLargeException(); if (raw.length <= MESSAGE_HEADER_LENGTH) throw new AssertionError();
sql = "SELECT data FROM blocks" byte[] body = new byte[raw.length - MESSAGE_HEADER_LENGTH];
+ " WHERE messageId = ? AND blockNumber = 0"; System.arraycopy(raw, MESSAGE_HEADER_LENGTH, body, 0, body.length);
ps = txn.prepareStatement(sql); return new Message(m, g, timestamp, body);
ps.setBytes(1, m.getBytes());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
byte[] data = rs.getBytes(1);
if (data == null) throw new DbStateException();
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
return new Message(m, g, timestamp, data);
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(rs, LOG, WARNING); tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING); tryToClose(ps, LOG, WARNING);
@@ -2076,28 +2101,17 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
@Override @Override
public Collection<MessageId> getSmallMessagesToOffer(Connection txn, public Collection<MessageId> getMessagesToRequest(Connection txn,
ContactId c, int maxMessages, int maxLatency) throws DbException { ContactId c, int maxMessages) throws DbException {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT messageId FROM statuses" String sql = "SELECT messageId FROM offers"
+ " WHERE contactId = ? AND state = ?" + " WHERE contactId = ?"
+ " AND length <= ?" + " LIMIT ?";
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE AND requested = FALSE"
+ " AND (expiry <= ? OR eta > ?)"
+ " ORDER BY timestamp LIMIT ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue()); ps.setInt(2, maxMessages);
ps.setInt(3, MESSAGE_HEADER_LENGTH + MAX_BLOCK_LENGTH);
ps.setLong(4, now);
ps.setLong(5, eta);
ps.setInt(6, maxMessages);
rs = ps.executeQuery(); rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>(); List<MessageId> ids = new ArrayList<>();
while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
@@ -2150,47 +2164,6 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public Collection<MessageId> getSmallMessagesToSend(Connection txn,
ContactId c, int maxLength, int maxLatency) throws DbException {
long now = clock.currentTimeMillis();
long eta = now + maxLatency;
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT length, messageId FROM statuses"
+ " WHERE contactId = ? AND state = ?"
+ " AND length <= ?"
+ " AND groupShared = TRUE AND messageShared = TRUE"
+ " AND deleted = FALSE"
+ " AND seen = FALSE"
+ " AND (expiry <= ? OR eta > ?)"
+ " ORDER BY timestamp";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setInt(2, DELIVERED.getValue());
ps.setInt(3, MESSAGE_HEADER_LENGTH + MAX_BLOCK_LENGTH);
ps.setLong(4, now);
ps.setLong(5, eta);
rs = ps.executeQuery();
List<MessageId> ids = new ArrayList<>();
int total = 0;
while (rs.next()) {
int length = rs.getInt(1);
if (total + length > maxLength) break;
ids.add(new MessageId(rs.getBytes(2)));
total += length;
}
rs.close();
ps.close();
return ids;
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public Collection<MessageId> getMessagesToValidate(Connection txn) public Collection<MessageId> getMessagesToValidate(Connection txn)
throws DbException { throws DbException {
@@ -2209,7 +2182,7 @@ abstract class JdbcDatabase implements Database<Connection> {
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT messageId FROM messages" String sql = "SELECT messageId FROM messages"
+ " WHERE state = ? AND deleted = FALSE"; + " WHERE state = ? AND raw IS NOT NULL";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, state.getValue()); ps.setInt(1, state.getValue());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -2914,6 +2887,50 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
private boolean removeOfferedMessage(Connection txn, ContactId c,
MessageId m) throws DbException {
PreparedStatement ps = null;
try {
String sql = "DELETE FROM offers"
+ " WHERE contactId = ? AND messageId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, m.getBytes());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
return affected == 1;
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public void removeOfferedMessages(Connection txn, ContactId c,
Collection<MessageId> requested) throws DbException {
PreparedStatement ps = null;
try {
String sql = "DELETE FROM offers"
+ " WHERE contactId = ? AND messageId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
for (MessageId m : requested) {
ps.setBytes(2, m.getBytes());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if (batchAffected.length != requested.size())
throw new DbStateException();
for (int rows : batchAffected)
if (rows != 1) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public void removePendingContact(Connection txn, PendingContactId p) public void removePendingContact(Connection txn, PendingContactId p)
throws DbException { throws DbException {

View File

@@ -37,7 +37,6 @@ class Migration38_39 implements Migration<Connection> {
s.execute("ALTER TABLE incomingKeys" s.execute("ALTER TABLE incomingKeys"
+ " ALTER COLUMN contactId" + " ALTER COLUMN contactId"
+ " SET NOT NULL"); + " SET NOT NULL");
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -36,7 +36,6 @@ class Migration39_40 implements Migration<Connection> {
s.execute("ALTER TABLE statuses" s.execute("ALTER TABLE statuses"
+ " ALTER COLUMN eta" + " ALTER COLUMN eta"
+ " SET NOT NULL"); + " SET NOT NULL");
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -38,7 +38,6 @@ class Migration40_41 implements Migration<Connection> {
s = txn.createStatement(); s = txn.createStatement();
s.execute("ALTER TABLE contacts" s.execute("ALTER TABLE contacts"
+ dbTypes.replaceTypes(" ADD alias _STRING")); + dbTypes.replaceTypes(" ADD alias _STRING"));
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -89,7 +89,6 @@ class Migration41_42 implements Migration<Connection> {
+ " FOREIGN KEY (keySetId)" + " FOREIGN KEY (keySetId)"
+ " REFERENCES outgoingHandshakeKeys (keySetId)" + " REFERENCES outgoingHandshakeKeys (keySetId)"
+ " ON DELETE CASCADE)")); + " ON DELETE CASCADE)"));
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -44,7 +44,6 @@ class Migration42_43 implements Migration<Connection> {
+ " ADD COLUMN handshakePublicKey _BINARY")); + " ADD COLUMN handshakePublicKey _BINARY"));
s.execute("ALTER TABLE contacts" s.execute("ALTER TABLE contacts"
+ " DROP COLUMN active"); + " DROP COLUMN active");
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -50,7 +50,6 @@ class Migration43_44 implements Migration<Connection> {
+ " ADD COLUMN rootKey _SECRET")); + " ADD COLUMN rootKey _SECRET"));
s.execute("ALTER TABLE outgoingKeys" s.execute("ALTER TABLE outgoingKeys"
+ " ADD COLUMN alice BOOLEAN"); + " ADD COLUMN alice BOOLEAN");
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -31,7 +31,6 @@ class Migration44_45 implements Migration<Connection> {
try { try {
s = txn.createStatement(); s = txn.createStatement();
s.execute("ALTER TABLE pendingContacts DROP COLUMN state"); s.execute("ALTER TABLE pendingContacts DROP COLUMN state");
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -32,7 +32,6 @@ class Migration45_46 implements Migration<Connection> {
s = txn.createStatement(); s = txn.createStatement();
s.execute("ALTER TABLE messages" s.execute("ALTER TABLE messages"
+ " ADD COLUMN temporary BOOLEAN DEFAULT FALSE NOT NULL"); + " ADD COLUMN temporary BOOLEAN DEFAULT FALSE NOT NULL");
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -39,7 +39,6 @@ class Migration46_47 implements Migration<Connection> {
s.execute(dbTypes.replaceTypes("ALTER TABLE contacts" s.execute(dbTypes.replaceTypes("ALTER TABLE contacts"
+ " ADD COLUMN syncVersions" + " ADD COLUMN syncVersions"
+ " _BINARY DEFAULT '00' NOT NULL")); + " _BINARY DEFAULT '00' NOT NULL"));
s.close();
} catch (SQLException e) { } catch (SQLException e) {
tryToClose(s, LOG, WARNING); tryToClose(s, LOG, WARNING);
throw new DbException(e); throw new DbException(e);

View File

@@ -1,95 +0,0 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import static java.lang.System.arraycopy;
import static java.sql.Types.BINARY;
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.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
class Migration47_48 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration47_48.class.getName());
private final DatabaseTypes dbTypes;
Migration47_48(DatabaseTypes dbTypes) {
this.dbTypes = dbTypes;
}
@Override
public int getStartVersion() {
return 47;
}
@Override
public int getEndVersion() {
return 48;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
ResultSet rs = null;
PreparedStatement ps = null;
try {
s = txn.createStatement();
s.execute("ALTER TABLE messages"
+ " ADD COLUMN deleted BOOLEAN DEFAULT FALSE NOT NULL");
s.execute("UPDATE messages SET deleted = TRUE WHERE raw IS NULL");
s.execute("ALTER TABLE messages"
+ " ADD COLUMN blockCount INT DEFAULT 1 NOT NULL");
s.execute(dbTypes.replaceTypes("CREATE TABLE blocks"
+ " (messageId _HASH NOT NULL,"
+ " blockNumber INT NOT NULL,"
+ " blockLength INT NOT NULL," // Excludes block header
+ " data BLOB," // Null if message has been deleted
+ " PRIMARY KEY (messageId, blockNumber),"
+ " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE)"));
rs = s.executeQuery("SELECT messageId, length, raw FROM messages");
ps = txn.prepareStatement("INSERT INTO blocks"
+ " (messageId, blockNumber, blockLength, data)"
+ " VALUES (?, 0, ?, ?)");
int migrated = 0;
while (rs.next()) {
byte[] id = rs.getBytes(1);
int length = rs.getInt(2);
byte[] raw = rs.getBytes(3);
ps.setBytes(1, id);
ps.setInt(2, length - MESSAGE_HEADER_LENGTH);
if (raw == null) {
ps.setNull(3, BINARY);
} else {
byte[] data = new byte[raw.length - MESSAGE_HEADER_LENGTH];
arraycopy(raw, MESSAGE_HEADER_LENGTH, data, 0, data.length);
ps.setBytes(3, data);
}
if (ps.executeUpdate() != 1) throw new DbStateException();
migrated++;
}
ps.close();
rs.close();
s.execute("ALTER TABLE messages DROP COLUMN raw");
s.close();
if (LOG.isLoggable(INFO))
LOG.info("Migrated " + migrated + " messages");
} catch (SQLException e) {
tryToClose(ps, LOG, WARNING);
tryToClose(rs, LOG, WARNING);
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -1,40 +0,0 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DbException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
class Migration48_49 implements Migration<Connection> {
private static final Logger LOG = getLogger(Migration48_49.class.getName());
@Override
public int getStartVersion() {
return 48;
}
@Override
public int getEndVersion() {
return 49;
}
@Override
public void migrate(Connection txn) throws DbException {
Statement s = null;
try {
s = txn.createStatement();
s.execute("DROP TABLE offers");
s.close();
} catch (SQLException e) {
tryToClose(s, LOG, WARNING);
throw new DbException(e);
}
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.bramble.keyagreement; package org.briarproject.bramble.keyagreement;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.crypto.KeyAgreementCrypto; import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
import org.briarproject.bramble.api.crypto.KeyPair; import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
@@ -9,8 +8,6 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.keyagreement.Payload; import org.briarproject.bramble.api.keyagreement.Payload;
import org.briarproject.bramble.api.keyagreement.TransportDescriptor; import org.briarproject.bramble.api.keyagreement.TransportDescriptor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; 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.Plugin; import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.PluginManager; import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
@@ -22,9 +19,7 @@ import org.briarproject.bramble.api.record.RecordWriterFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@@ -33,10 +28,8 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@@ -48,10 +41,7 @@ class KeyAgreementConnector {
} }
private static final Logger LOG = private static final Logger LOG =
getLogger(KeyAgreementConnector.class.getName()); Logger.getLogger(KeyAgreementConnector.class.getName());
private static final List<TransportId> PREFERRED_TRANSPORTS =
asList(BluetoothConstants.ID, LanTcpConstants.ID);
private final Callbacks callbacks; private final Callbacks callbacks;
private final KeyAgreementCrypto keyAgreementCrypto; private final KeyAgreementCrypto keyAgreementCrypto;
@@ -115,33 +105,22 @@ class KeyAgreementConnector {
this.alice = alice; this.alice = alice;
aliceLatch.countDown(); aliceLatch.countDown();
// Start connecting over supported transports in order of preference // Start connecting over supported transports
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("Starting outgoing BQP connections as " LOG.info("Starting outgoing BQP connections as "
+ (alice ? "Alice" : "Bob")); + (alice ? "Alice" : "Bob"));
} }
Map<TransportId, TransportDescriptor> descriptors = new HashMap<>();
for (TransportDescriptor d : remotePayload.getTransportDescriptors()) { for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
descriptors.put(d.getId(), d); Plugin p = pluginManager.getPlugin(d.getId());
} if (p instanceof DuplexPlugin) {
List<Pair<DuplexPlugin, BdfList>> transports = new ArrayList<>();
for (TransportId id : PREFERRED_TRANSPORTS) {
TransportDescriptor d = descriptors.get(id);
Plugin p = pluginManager.getPlugin(id);
if (d != null && p instanceof DuplexPlugin) {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Connecting via " + id); LOG.info("Connecting via " + d.getId());
transports.add(new Pair<>((DuplexPlugin) p, d.getDescriptor())); DuplexPlugin plugin = (DuplexPlugin) p;
}
}
// TODO: If we don't have any transports in common with the peer,
// warn the user and give up (#1224)
if (!transports.isEmpty()) {
byte[] commitment = remotePayload.getCommitment(); byte[] commitment = remotePayload.getCommitment();
connectionChooser.submit(new ReadableTask(new ConnectorTask( BdfList descriptor = d.getDescriptor();
transports, commitment))); connectionChooser.submit(new ReadableTask(
new ConnectorTask(plugin, commitment, descriptor)));
}
} }
// Get chosen connection // Get chosen connection
@@ -169,13 +148,15 @@ class KeyAgreementConnector {
private class ConnectorTask implements Callable<KeyAgreementConnection> { private class ConnectorTask implements Callable<KeyAgreementConnection> {
private final List<Pair<DuplexPlugin, BdfList>> transports;
private final byte[] commitment; private final byte[] commitment;
private final BdfList descriptor;
private final DuplexPlugin plugin;
private ConnectorTask(List<Pair<DuplexPlugin, BdfList>> transports, private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
byte[] commitment) { BdfList descriptor) {
this.transports = transports; this.plugin = plugin;
this.commitment = commitment; this.commitment = commitment;
this.descriptor = descriptor;
} }
@Nullable @Nullable
@@ -183,10 +164,6 @@ class KeyAgreementConnector {
public KeyAgreementConnection call() throws Exception { public KeyAgreementConnection call() throws Exception {
// Repeat attempts until we connect, get stopped, or get interrupted // Repeat attempts until we connect, get stopped, or get interrupted
while (!stopped) { while (!stopped) {
for (Pair<DuplexPlugin, BdfList> pair : transports) {
if (stopped) return null;
DuplexPlugin plugin = pair.getFirst();
BdfList descriptor = pair.getSecond();
DuplexTransportConnection conn = DuplexTransportConnection conn =
plugin.createKeyAgreementConnection(commitment, plugin.createKeyAgreementConnection(commitment,
descriptor); descriptor);
@@ -195,7 +172,6 @@ class KeyAgreementConnector {
LOG.info(plugin.getId() + ": Outgoing connection"); LOG.info(plugin.getId() + ": Outgoing connection");
return new KeyAgreementConnection(conn, plugin.getId()); return new KeyAgreementConnection(conn, plugin.getId());
} }
}
// Wait 2s before retry (to circumvent transient failures) // Wait 2s before retry (to circumvent transient failures)
Thread.sleep(2000); Thread.sleep(2000);
} }

View File

@@ -436,10 +436,8 @@ abstract class BluetoothPlugin<S, SS> implements DuplexPlugin, EventListener {
String uuid = UUID.nameUUIDFromBytes(commitment).toString(); String uuid = UUID.nameUUIDFromBytes(commitment).toString();
DuplexTransportConnection conn; DuplexTransportConnection conn;
if (descriptor.size() == 1) { if (descriptor.size() == 1) {
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO))
LOG.info("Discovering address for key agreement UUID " + LOG.info("Discovering address for key agreement UUID " + uuid);
uuid);
}
conn = discoverAndConnect(uuid); conn = discoverAndConnect(uuid);
} else { } else {
String address; String address;

View File

@@ -132,7 +132,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private final CircumventionProvider circumventionProvider; private final CircumventionProvider circumventionProvider;
private final ResourceProvider resourceProvider; private final ResourceProvider resourceProvider;
private final int maxLatency, maxIdleTime, socketTimeout; private final int maxLatency, maxIdleTime, socketTimeout;
private final File torDirectory, geoIpFile, configFile; private final File torDirectory, torFile, geoIpFile, obfs4File, configFile;
private final File doneFile, cookieFile; private final File doneFile, cookieFile;
private final AtomicBoolean used = new AtomicBoolean(false); private final AtomicBoolean used = new AtomicBoolean(false);
@@ -181,7 +181,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
socketTimeout = Integer.MAX_VALUE; socketTimeout = Integer.MAX_VALUE;
else socketTimeout = maxIdleTime * 2; else socketTimeout = maxIdleTime * 2;
this.torDirectory = torDirectory; this.torDirectory = torDirectory;
torFile = new File(torDirectory, "tor");
geoIpFile = new File(torDirectory, "geoip"); geoIpFile = new File(torDirectory, "geoip");
obfs4File = new File(torDirectory, "obfs4proxy");
configFile = new File(torDirectory, "torrc"); configFile = new File(torDirectory, "torrc");
doneFile = new File(torDirectory, "done"); doneFile = new File(torDirectory, "done");
cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
@@ -190,14 +192,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
new PoliteExecutor("TorPlugin", ioExecutor, 1); new PoliteExecutor("TorPlugin", ioExecutor, 1);
} }
protected File getTorExecutableFile() {
return new File(torDirectory, "tor");
}
protected File getObfs4ExecutableFile() {
return new File(torDirectory, "obfs4proxy");
}
@Override @Override
public TransportId getId() { public TransportId getId() {
return TorConstants.ID; return TorConstants.ID;
@@ -230,7 +224,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
LOG.warning("Old auth cookie not deleted"); LOG.warning("Old auth cookie not deleted");
// Start a new Tor process // Start a new Tor process
LOG.info("Starting Tor"); LOG.info("Starting Tor");
File torFile = getTorExecutableFile();
String torPath = torFile.getAbsolutePath(); String torPath = torFile.getAbsolutePath();
String configPath = configFile.getAbsolutePath(); String configPath = configFile.getAbsolutePath();
String pid = String.valueOf(getProcessId()); String pid = String.valueOf(getProcessId());
@@ -329,43 +322,44 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
private void installAssets() throws PluginException { private void installAssets() throws PluginException {
InputStream in = null;
OutputStream out = null;
try { try {
// The done file may already exist from a previous installation // The done file may already exist from a previous installation
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
doneFile.delete(); doneFile.delete();
installTorExecutable(); // Unzip the Tor binary to the filesystem
installObfs4Executable(); in = getTorInputStream();
extract(getGeoIpInputStream(), geoIpFile); out = new FileOutputStream(torFile);
extract(getConfigInputStream(), configFile); copyAndClose(in, out);
// Make the Tor binary executable
if (!torFile.setExecutable(true, true)) throw new IOException();
// Unzip the GeoIP database to the filesystem
in = getGeoIpInputStream();
out = new FileOutputStream(geoIpFile);
copyAndClose(in, out);
// Unzip the Obfs4 proxy to the filesystem
in = getObfs4InputStream();
out = new FileOutputStream(obfs4File);
copyAndClose(in, out);
// Make the Obfs4 proxy executable
if (!obfs4File.setExecutable(true, true)) throw new IOException();
// Copy the config file to the filesystem
in = getConfigInputStream();
out = new FileOutputStream(configFile);
copyAndClose(in, out);
if (!doneFile.createNewFile()) if (!doneFile.createNewFile())
LOG.warning("Failed to create done file"); LOG.warning("Failed to create done file");
} catch (IOException e) { } catch (IOException e) {
tryToClose(in, LOG, WARNING);
tryToClose(out, LOG, WARNING);
throw new PluginException(e); throw new PluginException(e);
} }
} }
protected void extract(InputStream in, File dest) throws IOException { private InputStream getTorInputStream() throws IOException {
OutputStream out = new FileOutputStream(dest);
copyAndClose(in, out);
}
protected void installTorExecutable() throws IOException {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Installing Tor binary for " + architecture); LOG.info("Installing Tor binary for " + architecture);
File torFile = getTorExecutableFile();
extract(getTorInputStream(), torFile);
if (!torFile.setExecutable(true, true)) throw new IOException();
}
protected void installObfs4Executable() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing obfs4proxy binary for " + architecture);
File obfs4File = getObfs4ExecutableFile();
extract(getObfs4InputStream(), obfs4File);
if (!obfs4File.setExecutable(true, true)) throw new IOException();
}
private InputStream getTorInputStream() throws IOException {
InputStream in = resourceProvider InputStream in = resourceProvider
.getResourceInputStream("tor_" + architecture, ".zip"); .getResourceInputStream("tor_" + architecture, ".zip");
ZipInputStream zin = new ZipInputStream(in); ZipInputStream zin = new ZipInputStream(in);
@@ -382,6 +376,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
} }
private InputStream getObfs4InputStream() throws IOException { private InputStream getObfs4InputStream() throws IOException {
if (LOG.isLoggable(INFO))
LOG.info("Installing obfs4proxy binary for " + architecture);
InputStream in = resourceProvider InputStream in = resourceProvider
.getResourceInputStream("obfs4proxy_" + architecture, ".zip"); .getResourceInputStream("obfs4proxy_" + architecture, ".zip");
ZipInputStream zin = new ZipInputStream(in); ZipInputStream zin = new ZipInputStream(in);
@@ -573,7 +569,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (enable) { if (enable) {
Collection<String> conf = new ArrayList<>(); Collection<String> conf = new ArrayList<>();
conf.add("UseBridges 1"); conf.add("UseBridges 1");
File obfs4File = getObfs4ExecutableFile();
if (needsMeek) { if (needsMeek) {
conf.add("ClientTransportPlugin meek_lite exec " + conf.add("ClientTransportPlugin meek_lite exec " +
obfs4File.getAbsolutePath()); obfs4File.getAbsolutePath());

View File

@@ -199,7 +199,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
Map<TransportId, LatestUpdate> latest = findLatestLocal(txn); Map<TransportId, LatestUpdate> latest = findLatestLocal(txn);
// Retrieve and parse the latest local properties // Retrieve and parse the latest local properties
for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) { for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
BdfList message = clientHelper.getSmallMessageAsList(txn, BdfList message = clientHelper.getMessageAsList(txn,
e.getValue().messageId); e.getValue().messageId);
local.put(e.getKey(), parseProperties(message)); local.put(e.getKey(), parseProperties(message));
} }
@@ -220,7 +220,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
true); true);
if (latest != null) { if (latest != null) {
// Retrieve and parse the latest local properties // Retrieve and parse the latest local properties
BdfList message = clientHelper.getSmallMessageAsList(txn, BdfList message = clientHelper.getMessageAsList(txn,
latest.messageId); latest.messageId);
p = parseProperties(message); p = parseProperties(message);
} }
@@ -250,7 +250,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
if (latest == null) { if (latest == null) {
local = new TransportProperties(); local = new TransportProperties();
} else { } else {
BdfList message = clientHelper.getSmallMessageAsList(txn, BdfList message = clientHelper.getMessageAsList(txn,
latest.messageId); latest.messageId);
local = parseProperties(message); local = parseProperties(message);
} }
@@ -271,8 +271,8 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
remote = new TransportProperties(); remote = new TransportProperties();
} else { } else {
// Retrieve and parse the latest remote properties // Retrieve and parse the latest remote properties
BdfList message = clientHelper.getSmallMessageAsList(txn, BdfList message =
latest.messageId); clientHelper.getMessageAsList(txn, latest.messageId);
remote = parseProperties(message); remote = parseProperties(message);
} }
// Merge in any discovered properties // Merge in any discovered properties
@@ -315,7 +315,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
} }
changed = true; changed = true;
} else { } else {
BdfList message = clientHelper.getSmallMessageAsList(txn, BdfList message = clientHelper.getMessageAsList(txn,
latest.messageId); latest.messageId);
TransportProperties old = parseProperties(message); TransportProperties old = parseProperties(message);
merged = new TransportProperties(old); merged = new TransportProperties(old);

View File

@@ -15,7 +15,6 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Ack;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.Offer; import org.briarproject.bramble.api.sync.Offer;
import org.briarproject.bramble.api.sync.Priority; import org.briarproject.bramble.api.sync.Priority;
import org.briarproject.bramble.api.sync.Request; import org.briarproject.bramble.api.sync.Request;
@@ -26,8 +25,8 @@ import org.briarproject.bramble.api.sync.event.CloseSyncConnectionsEvent;
import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent; import org.briarproject.bramble.api.sync.event.GroupVisibilityUpdatedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent; import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent; import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
import org.briarproject.bramble.api.sync.event.MessagesToAckEvent; import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
import org.briarproject.bramble.api.sync.event.MessagesToRequestEvent; import org.briarproject.bramble.api.sync.event.MessageToRequestEvent;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.transport.StreamWriter; import org.briarproject.bramble.api.transport.StreamWriter;
@@ -88,6 +87,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
private final AtomicBoolean generateAckQueued = new AtomicBoolean(false); private final AtomicBoolean generateAckQueued = new AtomicBoolean(false);
private final AtomicBoolean generateBatchQueued = new AtomicBoolean(false); private final AtomicBoolean generateBatchQueued = new AtomicBoolean(false);
private final AtomicBoolean generateOfferQueued = new AtomicBoolean(false); private final AtomicBoolean generateOfferQueued = new AtomicBoolean(false);
private final AtomicBoolean generateRequestQueued =
new AtomicBoolean(false);
private final AtomicLong nextSendTime = new AtomicLong(Long.MAX_VALUE); private final AtomicLong nextSendTime = new AtomicLong(Long.MAX_VALUE);
private volatile boolean interrupted = false; private volatile boolean interrupted = false;
@@ -124,6 +125,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
generateAck(); generateAck();
generateBatch(); generateBatch();
generateOffer(); generateOffer();
generateRequest();
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
long nextKeepalive = now + maxIdleTime; long nextKeepalive = now + maxIdleTime;
boolean dataToFlush = true; boolean dataToFlush = true;
@@ -195,6 +197,11 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
dbExecutor.execute(new GenerateOffer()); dbExecutor.execute(new GenerateOffer());
} }
private void generateRequest() {
if (generateRequestQueued.compareAndSet(false, true))
dbExecutor.execute(new GenerateRequest());
}
private void setNextSendTime(long time) { private void setNextSendTime(long time) {
long old = nextSendTime.getAndSet(time); long old = nextSendTime.getAndSet(time);
if (time < old) writerTasks.add(NEXT_SEND_TIME_DECREASED); if (time < old) writerTasks.add(NEXT_SEND_TIME_DECREASED);
@@ -220,16 +227,12 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
} else if (e instanceof MessageRequestedEvent) { } else if (e instanceof MessageRequestedEvent) {
if (((MessageRequestedEvent) e).getContactId().equals(contactId)) if (((MessageRequestedEvent) e).getContactId().equals(contactId))
generateBatch(); generateBatch();
} else if (e instanceof MessagesToAckEvent) { } else if (e instanceof MessageToAckEvent) {
if (((MessagesToAckEvent) e).getContactId().equals(contactId)) if (((MessageToAckEvent) e).getContactId().equals(contactId))
generateAck(); generateAck();
} else if (e instanceof MessagesToRequestEvent) { } else if (e instanceof MessageToRequestEvent) {
MessagesToRequestEvent m = (MessagesToRequestEvent) e; if (((MessageToRequestEvent) e).getContactId().equals(contactId))
if (m.getContactId().equals(contactId)) { generateRequest();
Collection<MessageId> ids = m.consumeIds();
if (ids != null)
writerTasks.add(new WriteRequest(new Request(ids)));
}
} else if (e instanceof LifecycleEvent) { } else if (e instanceof LifecycleEvent) {
LifecycleEvent l = (LifecycleEvent) e; LifecycleEvent l = (LifecycleEvent) e;
if (l.getLifecycleState() == STOPPING) interrupt(); if (l.getLifecycleState() == STOPPING) interrupt();
@@ -337,7 +340,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
try { try {
Offer o = db.transactionWithNullableResult(false, txn -> { Offer o = db.transactionWithNullableResult(false, txn -> {
Offer offer = db.generateOffer(txn, contactId, Offer offer = db.generateOffer(txn, contactId,
MAX_MESSAGE_IDS, maxLatency, true); MAX_MESSAGE_IDS, maxLatency);
setNextSendTime(db.getNextSendTime(txn, contactId)); setNextSendTime(db.getNextSendTime(txn, contactId));
return offer; return offer;
}); });
@@ -369,6 +372,27 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
} }
} }
private class GenerateRequest implements Runnable {
@DatabaseExecutor
@Override
public void run() {
if (interrupted) return;
if (!generateRequestQueued.getAndSet(false))
throw new AssertionError();
try {
Request r = db.transactionWithNullableResult(false, txn ->
db.generateRequest(txn, contactId, MAX_MESSAGE_IDS));
if (LOG.isLoggable(INFO))
LOG.info("Generated request: " + (r != null));
if (r != null) writerTasks.add(new WriteRequest(r));
} catch (DbException e) {
logException(LOG, WARNING, e);
interrupt();
}
}
}
private class WriteRequest implements ThrowingRunnable<IOException> { private class WriteRequest implements ThrowingRunnable<IOException> {
private final Request request; private final Request request;
@@ -383,6 +407,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
if (interrupted) return; if (interrupted) return;
recordWriter.writeRequest(request); recordWriter.writeRequest(request);
LOG.info("Sent request"); LOG.info("Sent request");
generateRequest();
} }
} }
} }

View File

@@ -186,8 +186,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
Collection<Message> b = Collection<Message> b =
db.transactionWithNullableResult(false, txn -> db.transactionWithNullableResult(false, txn ->
db.generateBatch(txn, contactId, db.generateBatch(txn, contactId,
MAX_RECORD_PAYLOAD_BYTES, maxLatency, MAX_RECORD_PAYLOAD_BYTES, maxLatency));
true));
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Generated batch: " + (b != null)); LOG.info("Generated batch: " + (b != null));
if (b == null) decrementOutstandingQueries(); if (b == null) decrementOutstandingQueries();

View File

@@ -35,6 +35,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe; import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject; import javax.inject.Inject;
@@ -120,12 +121,11 @@ class ValidationManagerImpl implements ValidationManager, Service,
Pair<Message, Group> mg = db.transactionWithResult(true, txn -> { Pair<Message, Group> mg = db.transactionWithResult(true, txn -> {
MessageId id = unvalidated.poll(); MessageId id = unvalidated.poll();
if (id == null) throw new AssertionError(); if (id == null) throw new AssertionError();
Message m = db.getSmallMessage(txn, id); Message m = db.getMessage(txn, id);
Group g = db.getGroup(txn, m.getGroupId()); Group g = db.getGroup(txn, m.getGroupId());
return new Pair<>(m, g); return new Pair<>(m, g);
}); });
validateMessageAsync(mg.getFirst(), mg.getSecond()); validateMessageAsync(mg.getFirst(), mg.getSecond(), unvalidated);
validateNextMessageAsync(unvalidated);
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
LOG.info("Message removed before validation"); LOG.info("Message removed before validation");
validateNextMessageAsync(unvalidated); validateNextMessageAsync(unvalidated);
@@ -179,7 +179,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
invalidateMessage(txn, id); invalidateMessage(txn, id);
addDependentsToInvalidate(txn, id, invalidate); addDependentsToInvalidate(txn, id, invalidate);
} else if (allDelivered) { } else if (allDelivered) {
Message m = db.getSmallMessage(txn, id); Message m = db.getMessage(txn, id);
Group g = db.getGroup(txn, m.getGroupId()); Group g = db.getGroup(txn, m.getGroupId());
ClientId c = g.getClientId(); ClientId c = g.getClientId();
int majorVersion = g.getMajorVersion(); int majorVersion = g.getMajorVersion();
@@ -213,12 +213,14 @@ class ValidationManagerImpl implements ValidationManager, Service,
} }
} }
private void validateMessageAsync(Message m, Group g) { private void validateMessageAsync(Message m, Group g,
validationExecutor.execute(() -> validateMessage(m, g)); @Nullable Queue<MessageId> unvalidated) {
validationExecutor.execute(() -> validateMessage(m, g, unvalidated));
} }
@ValidationExecutor @ValidationExecutor
private void validateMessage(Message m, Group g) { private void validateMessage(Message m, Group g,
@Nullable Queue<MessageId> unvalidated) {
ClientMajorVersion cv = ClientMajorVersion cv =
new ClientMajorVersion(g.getClientId(), g.getMajorVersion()); new ClientMajorVersion(g.getClientId(), g.getMajorVersion());
MessageValidator v = validators.get(cv); MessageValidator v = validators.get(cv);
@@ -234,6 +236,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
Queue<MessageId> invalidate = new LinkedList<>(); Queue<MessageId> invalidate = new LinkedList<>();
invalidate.add(m.getId()); invalidate.add(m.getId());
invalidateNextMessageAsync(invalidate); invalidateNextMessageAsync(invalidate);
} finally {
if (unvalidated != null) validateNextMessageAsync(unvalidated);
} }
} }
} }
@@ -440,7 +444,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
try { try {
Group g = db.transactionWithResult(true, txn -> Group g = db.transactionWithResult(true, txn ->
db.getGroup(txn, m.getGroupId())); db.getGroup(txn, m.getGroupId()));
validateMessageAsync(m, g); validateMessageAsync(m, g, null);
} catch (NoSuchGroupException e) { } catch (NoSuchGroupException e) {
LOG.info("Group removed before validation"); LOG.info("Group removed before validation");
} catch (DbException e) { } catch (DbException e) {

View File

@@ -298,8 +298,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
private List<ClientVersion> loadClientVersions(Transaction txn, private List<ClientVersion> loadClientVersions(Transaction txn,
MessageId m) throws DbException { MessageId m) throws DbException {
try { try {
return parseClientVersions( return parseClientVersions(clientHelper.getMessageAsList(txn, m));
clientHelper.getSmallMessageAsList(txn, m));
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} }
@@ -392,7 +391,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
private Update loadUpdate(Transaction txn, MessageId m) throws DbException { private Update loadUpdate(Transaction txn, MessageId m) throws DbException {
try { try {
return parseUpdate(clientHelper.getSmallMessageAsList(txn, m)); return parseUpdate(clientHelper.getMessageAsList(txn, m));
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} }

View File

@@ -122,11 +122,11 @@ public class ClientHelperImplTest extends BrambleTestCase {
expectToList(true); expectToList(true);
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getSmallMessage(txn, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
}}); }});
clientHelper.getSmallMessageAsList(messageId); clientHelper.getMessageAsList(messageId);
context.assertIsSatisfied(); context.assertIsSatisfied();
} }

View File

@@ -44,10 +44,10 @@ import org.briarproject.bramble.api.sync.event.MessageAddedEvent;
import org.briarproject.bramble.api.sync.event.MessageRequestedEvent; import org.briarproject.bramble.api.sync.event.MessageRequestedEvent;
import org.briarproject.bramble.api.sync.event.MessageSharedEvent; import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent; import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent;
import org.briarproject.bramble.api.sync.event.MessageToAckEvent;
import org.briarproject.bramble.api.sync.event.MessageToRequestEvent;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.api.sync.event.MessagesToAckEvent;
import org.briarproject.bramble.api.sync.event.MessagesToRequestEvent;
import org.briarproject.bramble.api.transport.IncomingKeys; import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.KeySetId; import org.briarproject.bramble.api.transport.KeySetId;
import org.briarproject.bramble.api.transport.OutgoingKeys; import org.briarproject.bramble.api.transport.OutgoingKeys;
@@ -76,6 +76,7 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH
import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN; import static org.briarproject.bramble.api.sync.validation.MessageState.UNKNOWN;
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE; import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
import static org.briarproject.bramble.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey; import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey;
import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey; import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey;
import static org.briarproject.bramble.test.TestUtils.getAuthor; import static org.briarproject.bramble.test.TestUtils.getAuthor;
@@ -294,11 +295,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not) // Check whether the contact is in the DB (which it's not)
exactly(17).of(database).startTransaction(); exactly(18).of(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
exactly(17).of(database).containsContact(txn, contactId); exactly(18).of(database).containsContact(txn, contactId);
will(returnValue(false)); will(returnValue(false));
exactly(17).of(database).abortTransaction(txn); exactly(18).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
@@ -322,7 +323,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
try { try {
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.generateBatch(transaction, contactId, 123, 456, true)); db.generateBatch(transaction, contactId, 123, 456));
fail(); fail();
} catch (NoSuchContactException expected) { } catch (NoSuchContactException expected) {
// Expected // Expected
@@ -330,7 +331,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
try { try {
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.generateOffer(transaction, contactId, 123, 456, true)); db.generateOffer(transaction, contactId, 123, 456));
fail();
} catch (NoSuchContactException expected) {
// Expected
}
try {
db.transaction(false, transaction ->
db.generateRequest(transaction, contactId, 123));
fail(); fail();
} catch (NoSuchContactException expected) { } catch (NoSuchContactException expected) {
// Expected // Expected
@@ -615,7 +624,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
try { try {
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.getSmallMessage(transaction, messageId)); db.getMessage(transaction, messageId));
fail(); fail();
} catch (NoSuchMessageException expected) { } catch (NoSuchMessageException expected) {
// Expected // Expected
@@ -856,14 +865,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(txn)); will(returnValue(txn));
oneOf(database).containsContact(txn, contactId); oneOf(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
oneOf(database).getSmallMessagesToSend(txn, contactId, oneOf(database).getMessagesToSend(txn, contactId,
MAX_MESSAGE_LENGTH * 2, maxLatency); MAX_MESSAGE_LENGTH * 2, maxLatency);
will(returnValue(ids)); will(returnValue(ids));
oneOf(database).getSmallMessage(txn, messageId); oneOf(database).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId, oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId,
maxLatency); maxLatency);
oneOf(database).getSmallMessage(txn, messageId1); oneOf(database).getMessage(txn, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1, oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1,
maxLatency); maxLatency);
@@ -876,7 +885,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
db.transaction(false, transaction -> db.transaction(false, transaction ->
assertEquals(messages, db.generateBatch(transaction, contactId, assertEquals(messages, db.generateBatch(transaction, contactId,
MAX_MESSAGE_LENGTH * 2, maxLatency, true))); MAX_MESSAGE_LENGTH * 2, maxLatency)));
} }
@Test @Test
@@ -888,8 +897,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
will(returnValue(txn)); will(returnValue(txn));
oneOf(database).containsContact(txn, contactId); oneOf(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
oneOf(database).getSmallMessagesToOffer(txn, contactId, 123, oneOf(database).getMessagesToOffer(txn, contactId, 123, maxLatency);
maxLatency);
will(returnValue(ids)); will(returnValue(ids));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId, oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId,
maxLatency); maxLatency);
@@ -901,13 +909,36 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
db.transaction(false, transaction -> { db.transaction(false, transaction -> {
Offer o = db.generateOffer(transaction, contactId, 123, maxLatency, Offer o = db.generateOffer(transaction, contactId, 123, maxLatency);
true);
assertNotNull(o); assertNotNull(o);
assertEquals(ids, o.getMessageIds()); assertEquals(ids, o.getMessageIds());
}); });
} }
@Test
public void testGenerateRequest() throws Exception {
MessageId messageId1 = new MessageId(getRandomId());
Collection<MessageId> ids = asList(messageId, messageId1);
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsContact(txn, contactId);
will(returnValue(true));
oneOf(database).getMessagesToRequest(txn, contactId, 123);
will(returnValue(ids));
oneOf(database).removeOfferedMessages(txn, contactId, ids);
oneOf(database).commitTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
db.transaction(false, transaction -> {
Request r = db.generateRequest(transaction, contactId, 123);
assertNotNull(r);
assertEquals(ids, r.getMessageIds());
});
}
@Test @Test
public void testGenerateRequestedBatch() throws Exception { public void testGenerateRequestedBatch() throws Exception {
Collection<MessageId> ids = asList(messageId, messageId1); Collection<MessageId> ids = asList(messageId, messageId1);
@@ -920,11 +951,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).getRequestedMessagesToSend(txn, contactId, oneOf(database).getRequestedMessagesToSend(txn, contactId,
MAX_MESSAGE_LENGTH * 2, maxLatency); MAX_MESSAGE_LENGTH * 2, maxLatency);
will(returnValue(ids)); will(returnValue(ids));
oneOf(database).getSmallMessage(txn, messageId); oneOf(database).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId, oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId,
maxLatency); maxLatency);
oneOf(database).getSmallMessage(txn, messageId1); oneOf(database).getMessage(txn, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1, oneOf(database).updateExpiryTimeAndEta(txn, contactId, messageId1,
maxLatency); maxLatency);
@@ -987,10 +1018,10 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).raiseAckFlag(txn, contactId, messageId); oneOf(database).raiseAckFlag(txn, contactId, messageId);
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// First time: the message was received and added // First time: the message was received and added
oneOf(eventBus).broadcast(with(any(MessagesToAckEvent.class))); oneOf(eventBus).broadcast(with(any(MessageToAckEvent.class)));
oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
// Second time: the message needs to be acked // Second time: the message needs to be acked
oneOf(eventBus).broadcast(with(any(MessagesToAckEvent.class))); oneOf(eventBus).broadcast(with(any(MessageToAckEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
@@ -1018,7 +1049,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
oneOf(database).raiseAckFlag(txn, contactId, messageId); oneOf(database).raiseAckFlag(txn, contactId, messageId);
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// The message was received but not added // The message was received but not added
oneOf(eventBus).broadcast(with(any(MessagesToAckEvent.class))); oneOf(eventBus).broadcast(with(any(MessageToAckEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
@@ -1049,15 +1080,19 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
public void testReceiveOffer() throws Exception { public void testReceiveOffer() throws Exception {
MessageId messageId1 = new MessageId(getRandomId()); MessageId messageId1 = new MessageId(getRandomId());
MessageId messageId2 = new MessageId(getRandomId()); MessageId messageId2 = new MessageId(getRandomId());
MessageId messageId3 = new MessageId(getRandomId());
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(database).startTransaction(); oneOf(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
oneOf(database).containsContact(txn, contactId); oneOf(database).containsContact(txn, contactId);
will(returnValue(true)); will(returnValue(true));
// There's room for two more offered messages
oneOf(database).countOfferedMessages(txn, contactId);
will(returnValue(MAX_OFFERED_MESSAGES - 2));
// The first message isn't visible - request it // The first message isn't visible - request it
oneOf(database).containsVisibleMessage(txn, contactId, messageId); oneOf(database).containsVisibleMessage(txn, contactId, messageId);
will(returnValue(false)); will(returnValue(false));
oneOf(database).addOfferedMessage(txn, contactId, messageId);
// The second message is visible - ack it // The second message is visible - ack it
oneOf(database).containsVisibleMessage(txn, contactId, messageId1); oneOf(database).containsVisibleMessage(txn, contactId, messageId1);
will(returnValue(true)); will(returnValue(true));
@@ -1066,14 +1101,19 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
// The third message isn't visible - request it // The third message isn't visible - request it
oneOf(database).containsVisibleMessage(txn, contactId, messageId2); oneOf(database).containsVisibleMessage(txn, contactId, messageId2);
will(returnValue(false)); will(returnValue(false));
oneOf(database).addOfferedMessage(txn, contactId, messageId2);
// The fourth message isn't visible, but there's no room to store it
oneOf(database).containsVisibleMessage(txn, contactId, messageId3);
will(returnValue(false));
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
oneOf(eventBus).broadcast(with(any(MessagesToAckEvent.class))); oneOf(eventBus).broadcast(with(any(MessageToAckEvent.class)));
oneOf(eventBus).broadcast(with(any(MessagesToRequestEvent.class))); oneOf(eventBus).broadcast(with(any(MessageToRequestEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
Offer o = new Offer(asList(messageId, messageId1, messageId2)); Offer o = new Offer(asList(messageId, messageId1,
messageId2, messageId3));
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.receiveOffer(transaction, contactId, o)); db.receiveOffer(transaction, contactId, o));
} }

View File

@@ -3,9 +3,11 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.system.SystemClock; import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.TestDatabaseConfig; import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestMessageFactory;
import org.briarproject.bramble.test.UTest; import org.briarproject.bramble.test.UTest;
import java.io.IOException; import java.io.IOException;
@@ -30,7 +32,8 @@ public abstract class DatabasePerformanceComparisonTest
private SecretKey databaseKey = getSecretKey(); private SecretKey databaseKey = getSecretKey();
abstract Database<Connection> createDatabase(boolean conditionA, abstract Database<Connection> createDatabase(boolean conditionA,
DatabaseConfig databaseConfig, Clock clock); DatabaseConfig databaseConfig, MessageFactory messageFactory,
Clock clock);
@Override @Override
protected void benchmark(String name, protected void benchmark(String name,
@@ -73,7 +76,8 @@ public abstract class DatabasePerformanceComparisonTest
private Database<Connection> openDatabase(boolean conditionA) private Database<Connection> openDatabase(boolean conditionA)
throws DbException { throws DbException {
Database<Connection> db = createDatabase(conditionA, Database<Connection> db = createDatabase(conditionA,
new TestDatabaseConfig(testDir), new SystemClock()); new TestDatabaseConfig(testDir), new TestMessageFactory(),
new SystemClock());
db.open(databaseKey, null); db.open(databaseKey, null);
return db; return db;
} }

View File

@@ -41,6 +41,7 @@ import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getIdentity; import static org.briarproject.bramble.test.TestUtils.getIdentity;
import static org.briarproject.bramble.test.TestUtils.getMessage; import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory; import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.briarproject.bramble.test.UTest.Result.INCONCLUSIVE; import static org.briarproject.bramble.test.UTest.Result.INCONCLUSIVE;
import static org.briarproject.bramble.test.UTest.Z_CRITICAL_0_1; import static org.briarproject.bramble.test.UTest.Z_CRITICAL_0_1;
@@ -80,6 +81,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
private static final int METADATA_KEYS_PER_MESSAGE = 5; private static final int METADATA_KEYS_PER_MESSAGE = 5;
private static final int METADATA_KEY_LENGTH = 10; private static final int METADATA_KEY_LENGTH = 10;
private static final int METADATA_VALUE_LENGTH = 100; private static final int METADATA_VALUE_LENGTH = 100;
private static final int OFFERED_MESSAGES_PER_CONTACT = 100;
/** /**
* How many benchmark iterations to run in each block. * How many benchmark iterations to run in each block.
@@ -190,6 +192,16 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
}); });
} }
@Test
public void testCountOfferedMessages() throws Exception {
String name = "countOfferedMessages(T, ContactId)";
benchmark(name, db -> {
Connection txn = db.startTransaction();
db.countOfferedMessages(txn, pickRandom(contacts).getId());
db.commitTransaction(txn);
});
}
@Test @Test
public void testGetContact() throws Exception { public void testGetContact() throws Exception {
String name = "getContact(T, ContactId)"; String name = "getContact(T, ContactId)";
@@ -442,6 +454,17 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
}); });
} }
@Test
public void testGetMessagesToRequest() throws Exception {
String name = "getMessagesToRequest(T, ContactId, int)";
benchmark(name, db -> {
Connection txn = db.startTransaction();
db.getMessagesToRequest(txn, pickRandom(contacts).getId(),
MAX_MESSAGE_IDS);
db.commitTransaction(txn);
});
}
@Test @Test
public void testGetMessagesToSend() throws Exception { public void testGetMessagesToSend() throws Exception {
String name = "getMessagesToSend(T, ContactId, int)"; String name = "getMessagesToSend(T, ContactId, int)";
@@ -484,11 +507,11 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
} }
@Test @Test
public void testGetSmallMessage() throws Exception { public void testGetMessage() throws Exception {
String name = "getSmallMessage(T, MessageId)"; String name = "getMessage(T, MessageId)";
benchmark(name, db -> { benchmark(name, db -> {
Connection txn = db.startTransaction(); Connection txn = db.startTransaction();
db.getSmallMessage(txn, pickRandom(messages).getId()); db.getMessage(txn, pickRandom(messages).getId());
db.commitTransaction(txn); db.commitTransaction(txn);
}); });
} }
@@ -560,6 +583,9 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
groupMessages.get(g.getId()).add(m.getId()); groupMessages.get(g.getId()).add(m.getId());
} }
} }
for (int j = 0; j < OFFERED_MESSAGES_PER_CONTACT; j++) {
db.addOfferedMessage(txn, c, new MessageId(getRandomId()));
}
} }
for (int i = 0; i < LOCAL_GROUPS; i++) { for (int i = 0; i < LOCAL_GROUPS; i++) {
Group g = getGroup(clientIds.get(i % CLIENTS), 123); Group g = getGroup(clientIds.get(i % CLIENTS), 123);

View File

@@ -3,9 +3,11 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.system.SystemClock; import org.briarproject.bramble.system.SystemClock;
import org.briarproject.bramble.test.TestDatabaseConfig; import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestMessageFactory;
import org.briarproject.bramble.util.IoUtils; import org.briarproject.bramble.util.IoUtils;
import java.io.File; import java.io.File;
@@ -24,7 +26,7 @@ public abstract class DatabaseTraceTest extends DatabasePerformanceTest {
private SecretKey databaseKey = getSecretKey(); private SecretKey databaseKey = getSecretKey();
abstract Database<Connection> createDatabase(DatabaseConfig databaseConfig, abstract Database<Connection> createDatabase(DatabaseConfig databaseConfig,
Clock clock); MessageFactory messageFactory, Clock clock);
@Nullable @Nullable
protected abstract File getTraceFile(); protected abstract File getTraceFile();
@@ -46,7 +48,8 @@ public abstract class DatabaseTraceTest extends DatabasePerformanceTest {
private Database<Connection> openDatabase() throws DbException { private Database<Connection> openDatabase() throws DbException {
Database<Connection> db = createDatabase( Database<Connection> db = createDatabase(
new TestDatabaseConfig(testDir), new SystemClock()); new TestDatabaseConfig(testDir), new TestMessageFactory(),
new SystemClock());
db.open(databaseKey, null); db.open(databaseKey, null);
return db; return db;
} }

View File

@@ -16,6 +16,6 @@ public class H2DatabasePerformanceTest extends SingleDatabasePerformanceTest {
@Override @Override
protected JdbcDatabase createDatabase(DatabaseConfig config, protected JdbcDatabase createDatabase(DatabaseConfig config,
MessageFactory messageFactory, Clock clock) { MessageFactory messageFactory, Clock clock) {
return new H2Database(config, clock); return new H2Database(config, messageFactory, clock);
} }
} }

View File

@@ -9,6 +9,6 @@ public class H2DatabaseTest extends JdbcDatabaseTest {
@Override @Override
protected JdbcDatabase createDatabase(DatabaseConfig config, protected JdbcDatabase createDatabase(DatabaseConfig config,
MessageFactory messageFactory, Clock clock) { MessageFactory messageFactory, Clock clock) {
return new H2Database(config, clock); return new H2Database(config, messageFactory, clock);
} }
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.db; package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.junit.Ignore; import org.junit.Ignore;
@@ -14,8 +15,8 @@ public class H2DatabaseTraceTest extends DatabaseTraceTest {
@Override @Override
Database<Connection> createDatabase(DatabaseConfig databaseConfig, Database<Connection> createDatabase(DatabaseConfig databaseConfig,
Clock clock) { MessageFactory messageFactory, Clock clock) {
return new H2Database(databaseConfig, clock) { return new H2Database(databaseConfig, messageFactory, clock) {
@Override @Override
@Nonnull @Nonnull
String getUrl() { String getUrl() {

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.db; package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.junit.Ignore; import org.junit.Ignore;
@@ -12,11 +13,11 @@ public class H2HyperSqlDatabasePerformanceComparisonTest
@Override @Override
Database<Connection> createDatabase(boolean conditionA, Database<Connection> createDatabase(boolean conditionA,
DatabaseConfig databaseConfig, DatabaseConfig databaseConfig, MessageFactory messageFactory,
Clock clock) { Clock clock) {
if (conditionA) if (conditionA)
return new H2Database(databaseConfig, clock); return new H2Database(databaseConfig, messageFactory, clock);
else return new HyperSqlDatabase(databaseConfig, clock); else return new HyperSqlDatabase(databaseConfig, messageFactory, clock);
} }
@Override @Override

View File

@@ -11,7 +11,7 @@ public class H2MigrationTest extends DatabaseMigrationTest {
@Override @Override
Database<Connection> createDatabase( Database<Connection> createDatabase(
List<Migration<Connection>> migrations) { List<Migration<Connection>> migrations) {
return new H2Database(config, clock) { return new H2Database(config, messageFactory, clock) {
@Override @Override
List<Migration<Connection>> getMigrations() { List<Migration<Connection>> getMigrations() {
return migrations; return migrations;

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.db; package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.junit.Ignore; import org.junit.Ignore;
@@ -17,9 +18,9 @@ public class H2SelfDatabasePerformanceComparisonTest
@Override @Override
Database<Connection> createDatabase(boolean conditionA, Database<Connection> createDatabase(boolean conditionA,
DatabaseConfig databaseConfig, DatabaseConfig databaseConfig, MessageFactory messageFactory,
Clock clock) { Clock clock) {
return new H2Database(databaseConfig, clock); return new H2Database(databaseConfig, messageFactory, clock);
} }
@Override @Override

View File

@@ -3,6 +3,7 @@ package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.junit.Ignore; import org.junit.Ignore;
@@ -19,11 +20,12 @@ public class H2SleepDatabasePerformanceComparisonTest
@Override @Override
Database<Connection> createDatabase(boolean conditionA, Database<Connection> createDatabase(boolean conditionA,
DatabaseConfig databaseConfig, Clock clock) { DatabaseConfig databaseConfig, MessageFactory messageFactory,
Clock clock) {
if (conditionA) { if (conditionA) {
return new H2Database(databaseConfig, clock); return new H2Database(databaseConfig, messageFactory, clock);
} else { } else {
return new H2Database(databaseConfig, clock) { return new H2Database(databaseConfig, messageFactory, clock) {
@Override @Override
@NotNullByDefault @NotNullByDefault
public void commitTransaction(Connection txn) public void commitTransaction(Connection txn)

View File

@@ -17,6 +17,6 @@ public class HyperSqlDatabasePerformanceTest
@Override @Override
protected JdbcDatabase createDatabase(DatabaseConfig config, protected JdbcDatabase createDatabase(DatabaseConfig config,
MessageFactory messageFactory, Clock clock) { MessageFactory messageFactory, Clock clock) {
return new HyperSqlDatabase(config, clock); return new HyperSqlDatabase(config, messageFactory, clock);
} }
} }

View File

@@ -9,6 +9,6 @@ public class HyperSqlDatabaseTest extends JdbcDatabaseTest {
@Override @Override
protected JdbcDatabase createDatabase(DatabaseConfig config, protected JdbcDatabase createDatabase(DatabaseConfig config,
MessageFactory messageFactory, Clock clock) { MessageFactory messageFactory, Clock clock) {
return new HyperSqlDatabase(config, clock); return new HyperSqlDatabase(config, messageFactory ,clock);
} }
} }

View File

@@ -11,7 +11,7 @@ public class HyperSqlMigrationTest extends DatabaseMigrationTest {
@Override @Override
Database<Connection> createDatabase( Database<Connection> createDatabase(
List<Migration<Connection>> migrations) { List<Migration<Connection>> migrations) {
return new HyperSqlDatabase(config, clock) { return new HyperSqlDatabase(config, messageFactory, clock) {
@Override @Override
List<Migration<Connection>> getMigrations() { List<Migration<Connection>> getMigrations() {
return migrations; return migrations;

View File

@@ -41,6 +41,7 @@ import org.junit.Test;
import java.io.File; import java.io.File;
import java.sql.Connection; import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -165,7 +166,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertTrue(db.containsGroup(txn, groupId)); assertTrue(db.containsGroup(txn, groupId));
assertTrue(db.containsMessage(txn, messageId)); assertTrue(db.containsMessage(txn, messageId));
assertArrayEquals(message.getBody(), assertArrayEquals(message.getBody(),
db.getSmallMessage(txn, messageId).getBody()); db.getMessage(txn, messageId).getBody());
// Delete the records // Delete the records
db.removeMessage(txn, messageId); db.removeMessage(txn, messageId);
@@ -1186,6 +1187,35 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close(); db.close();
} }
@Test
public void testOfferedMessages() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact - initially there should be no offered messages
db.addIdentity(txn, identity);
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), null, true));
assertEquals(0, db.countOfferedMessages(txn, contactId));
// Add some offered messages and count them
List<MessageId> ids = new ArrayList<>();
for (int i = 0; i < 10; i++) {
MessageId m = new MessageId(getRandomId());
db.addOfferedMessage(txn, contactId, m);
ids.add(m);
}
assertEquals(10, db.countOfferedMessages(txn, contactId));
// Remove some of the offered messages and count again
List<MessageId> half = ids.subList(0, 5);
db.removeOfferedMessages(txn, contactId, half);
assertEquals(5, db.countOfferedMessages(txn, contactId));
db.commitTransaction(txn);
db.close();
}
@Test @Test
public void testGroupMetadata() throws Exception { public void testGroupMetadata() throws Exception {
Database<Connection> db = open(false); Database<Connection> db = open(false);
@@ -1899,7 +1929,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(singletonList(messageId), ids); assertEquals(singletonList(messageId), ids);
// The message should be available // The message should be available
Message m = db.getSmallMessage(txn, messageId); Message m = db.getMessage(txn, messageId);
assertEquals(messageId, m.getId()); assertEquals(messageId, m.getId());
assertEquals(groupId, m.getGroupId()); assertEquals(groupId, m.getGroupId());
assertEquals(message.getTimestamp(), m.getTimestamp()); assertEquals(message.getTimestamp(), m.getTimestamp());
@@ -1919,7 +1949,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
// Requesting the message should throw an exception // Requesting the message should throw an exception
try { try {
db.getSmallMessage(txn, messageId); db.getMessage(txn, messageId);
fail(); fail();
} catch (MessageDeletedException expected) { } catch (MessageDeletedException expected) {
// Expected // Expected
@@ -2057,7 +2087,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
Connection txn = db.startTransaction(); Connection txn = db.startTransaction();
try { try {
// Ask for a nonexistent message - an exception should be thrown // Ask for a nonexistent message - an exception should be thrown
db.getSmallMessage(txn, messageId); db.getMessage(txn, messageId);
fail(); fail();
} catch (DbException expected) { } catch (DbException expected) {
// It should be possible to abort the transaction without error // It should be possible to abort the transaction without error

View File

@@ -402,7 +402,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
localGroup.getId()); localGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, fooUpdateId); oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict); fooPropertiesDict);
@@ -469,7 +469,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup2.getId()); contactGroup2.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, fooUpdateId); oneOf(clientHelper).getMessageAsList(txn, fooUpdateId);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict); fooPropertiesDict);
@@ -524,7 +524,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, updateId); oneOf(clientHelper).getMessageAsList(txn, updateId);
will(returnValue(update)); will(returnValue(update));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict); fooPropertiesDict);
@@ -562,7 +562,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
localGroup.getId()); localGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, updateId); oneOf(clientHelper).getMessageAsList(txn, updateId);
will(returnValue(update)); will(returnValue(update));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict); fooPropertiesDict);
@@ -693,7 +693,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
localGroup.getId()); localGroup.getId());
will(returnValue(localGroupMessageMetadata)); will(returnValue(localGroupMessageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, localGroupUpdateId); oneOf(clientHelper).getMessageAsList(txn, localGroupUpdateId);
will(returnValue(oldUpdate)); will(returnValue(oldUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
oldPropertiesDict); oldPropertiesDict);
@@ -758,7 +758,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
localGroup.getId()); localGroup.getId());
will(returnValue(localGroupMessageMetadata)); will(returnValue(localGroupMessageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, localGroupUpdateId); oneOf(clientHelper).getMessageAsList(txn, localGroupUpdateId);
will(returnValue(oldUpdate)); will(returnValue(oldUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
oldPropertiesDict); oldPropertiesDict);
@@ -817,12 +817,12 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
localGroup.getId()); localGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
// Retrieve and parse the latest local properties // Retrieve and parse the latest local properties
oneOf(clientHelper).getSmallMessageAsList(txn, fooVersion999); oneOf(clientHelper).getMessageAsList(txn, fooVersion999);
will(returnValue(fooUpdate)); will(returnValue(fooUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
fooPropertiesDict); fooPropertiesDict);
will(returnValue(fooProperties)); will(returnValue(fooProperties));
oneOf(clientHelper).getSmallMessageAsList(txn, barVersion3); oneOf(clientHelper).getMessageAsList(txn, barVersion3);
will(returnValue(barUpdate)); will(returnValue(barUpdate));
oneOf(clientHelper).parseAndValidateTransportProperties( oneOf(clientHelper).parseAndValidateTransportProperties(
barPropertiesDict); barPropertiesDict);

View File

@@ -64,7 +64,7 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
oneOf(db).transactionWithNullableResult(with(false), oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(noMsgTxn)); withNullableDbCallable(noMsgTxn));
oneOf(db).generateBatch(with(noMsgTxn), with(contactId), oneOf(db).generateBatch(with(noMsgTxn), with(contactId),
with(any(int.class)), with(MAX_LATENCY), with(true)); with(any(int.class)), with(MAX_LATENCY));
will(returnValue(null)); will(returnValue(null));
// Send the end of stream marker // Send the end of stream marker
oneOf(streamWriter).sendEndOfStream(); oneOf(streamWriter).sendEndOfStream();
@@ -101,7 +101,7 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
oneOf(db).transactionWithNullableResult(with(false), oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(msgTxn)); withNullableDbCallable(msgTxn));
oneOf(db).generateBatch(with(msgTxn), with(contactId), oneOf(db).generateBatch(with(msgTxn), with(contactId),
with(any(int.class)), with(MAX_LATENCY), with(true)); with(any(int.class)), with(MAX_LATENCY));
will(returnValue(singletonList(message))); will(returnValue(singletonList(message)));
oneOf(recordWriter).writeMessage(message); oneOf(recordWriter).writeMessage(message);
// No more acks // No more acks
@@ -113,7 +113,7 @@ public class SimplexOutgoingSessionTest extends BrambleMockTestCase {
oneOf(db).transactionWithNullableResult(with(false), oneOf(db).transactionWithNullableResult(with(false),
withNullableDbCallable(noMsgTxn)); withNullableDbCallable(noMsgTxn));
oneOf(db).generateBatch(with(noMsgTxn), with(contactId), oneOf(db).generateBatch(with(noMsgTxn), with(contactId),
with(any(int.class)), with(MAX_LATENCY), with(true)); with(any(int.class)), with(MAX_LATENCY));
will(returnValue(null)); will(returnValue(null));
// Send the end of stream marker // Send the end of stream marker
oneOf(streamWriter).sendEndOfStream(); oneOf(streamWriter).sendEndOfStream();

View File

@@ -99,7 +99,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
// Load the first raw message and group // Load the first raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getSmallMessage(txn, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
oneOf(db).getGroup(txn, groupId); oneOf(db).getGroup(txn, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -118,7 +118,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
will(returnValue(emptyMap())); will(returnValue(emptyMap()));
// Load the second raw message and group // Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn2)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn2));
oneOf(db).getSmallMessage(txn2, messageId1); oneOf(db).getMessage(txn2, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn2, groupId); oneOf(db).getGroup(txn2, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -159,7 +159,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
oneOf(db).getMessageDependencies(txn, messageId); oneOf(db).getMessageDependencies(txn, messageId);
will(returnValue(singletonMap(messageId1, DELIVERED))); will(returnValue(singletonMap(messageId1, DELIVERED)));
// Get the message and its metadata to deliver // Get the message and its metadata to deliver
oneOf(db).getSmallMessage(txn, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
oneOf(db).getGroup(txn, groupId); oneOf(db).getGroup(txn, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -179,7 +179,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
oneOf(db).getMessageDependencies(txn1, messageId2); oneOf(db).getMessageDependencies(txn1, messageId2);
will(returnValue(singletonMap(messageId1, DELIVERED))); will(returnValue(singletonMap(messageId1, DELIVERED)));
// Get the dependent and its metadata to deliver // Get the dependent and its metadata to deliver
oneOf(db).getSmallMessage(txn1, messageId2); oneOf(db).getMessage(txn1, messageId2);
will(returnValue(message2)); will(returnValue(message2));
oneOf(db).getGroup(txn1, groupId); oneOf(db).getGroup(txn1, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -276,11 +276,11 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
// Load the first raw message - *gasp* it's gone! // Load the first raw message - *gasp* it's gone!
oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getSmallMessage(txn, messageId); oneOf(db).getMessage(txn, messageId);
will(throwException(new NoSuchMessageException())); will(throwException(new NoSuchMessageException()));
// Load the second raw message and group // Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getSmallMessage(txn1, messageId1); oneOf(db).getMessage(txn1, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn1, groupId); oneOf(db).getGroup(txn1, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -317,14 +317,14 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
// Load the first raw message // Load the first raw message
oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
oneOf(db).getSmallMessage(txn, messageId); oneOf(db).getMessage(txn, messageId);
will(returnValue(message)); will(returnValue(message));
// Load the group - *gasp* it's gone! // Load the group - *gasp* it's gone!
oneOf(db).getGroup(txn, groupId); oneOf(db).getGroup(txn, groupId);
will(throwException(new NoSuchGroupException())); will(throwException(new NoSuchGroupException()));
// Load the second raw message and group // Load the second raw message and group
oneOf(db).transactionWithResult(with(true), withDbCallable(txn1)); oneOf(db).transactionWithResult(with(true), withDbCallable(txn1));
oneOf(db).getSmallMessage(txn1, messageId1); oneOf(db).getMessage(txn1, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn1, groupId); oneOf(db).getGroup(txn1, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -614,7 +614,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
oneOf(db).getMessageDependencies(txn2, messageId1); oneOf(db).getMessageDependencies(txn2, messageId1);
will(returnValue(singletonMap(messageId, DELIVERED))); will(returnValue(singletonMap(messageId, DELIVERED)));
// Get message 1 and its metadata // Get message 1 and its metadata
oneOf(db).getSmallMessage(txn2, messageId1); oneOf(db).getMessage(txn2, messageId1);
will(returnValue(message1)); will(returnValue(message1));
oneOf(db).getGroup(txn2, groupId); oneOf(db).getGroup(txn2, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -634,7 +634,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
oneOf(db).getMessageDependencies(txn3, messageId2); oneOf(db).getMessageDependencies(txn3, messageId2);
will(returnValue(singletonMap(messageId, DELIVERED))); will(returnValue(singletonMap(messageId, DELIVERED)));
// Get message 2 and its metadata // Get message 2 and its metadata
oneOf(db).getSmallMessage(txn3, messageId2); oneOf(db).getMessage(txn3, messageId2);
will(returnValue(message2)); will(returnValue(message2));
oneOf(db).getGroup(txn3, groupId); oneOf(db).getGroup(txn3, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -654,7 +654,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
oneOf(db).getMessageDependencies(txn4, messageId3); oneOf(db).getMessageDependencies(txn4, messageId3);
will(returnValue(twoDependencies)); will(returnValue(twoDependencies));
// Get message 3 and its metadata // Get message 3 and its metadata
oneOf(db).getSmallMessage(txn4, messageId3); oneOf(db).getMessage(txn4, messageId3);
will(returnValue(message3)); will(returnValue(message3));
oneOf(db).getGroup(txn4, groupId); oneOf(db).getGroup(txn4, groupId);
will(returnValue(group)); will(returnValue(group));
@@ -677,7 +677,7 @@ public class ValidationManagerImplTest extends BrambleMockTestCase {
oneOf(db).getMessageDependencies(txn6, messageId4); oneOf(db).getMessageDependencies(txn6, messageId4);
will(returnValue(singletonMap(messageId3, DELIVERED))); will(returnValue(singletonMap(messageId3, DELIVERED)));
// Get message 4 and its metadata // Get message 4 and its metadata
oneOf(db).getSmallMessage(txn6, messageId4); oneOf(db).getMessage(txn6, messageId4);
will(returnValue(message4)); will(returnValue(message4));
oneOf(db).getGroup(txn6, groupId); oneOf(db).getGroup(txn6, groupId);
will(returnValue(group)); will(returnValue(group));

View File

@@ -184,7 +184,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
contactGroup.getId()); contactGroup.getId());
will(returnValue(singletonMap(localUpdateId, localUpdateMeta))); will(returnValue(singletonMap(localUpdateId, localUpdateMeta)));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, localUpdateId); oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody)); will(returnValue(localUpdateBody));
// Latest local update is up-to-date, no visibilities have changed // Latest local update is up-to-date, no visibilities have changed
}}); }});
@@ -206,7 +206,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the old client versions // Load the old client versions
oneOf(db).getMessageIds(txn, localGroup.getId()); oneOf(db).getMessageIds(txn, localGroup.getId());
will(returnValue(singletonList(localVersionsId))); will(returnValue(singletonList(localVersionsId)));
oneOf(clientHelper).getSmallMessageAsList(txn, localVersionsId); oneOf(clientHelper).getMessageAsList(txn, localVersionsId);
will(returnValue(localVersionsBody)); will(returnValue(localVersionsBody));
// Client versions are up-to-date // Client versions are up-to-date
}}); }});
@@ -248,7 +248,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the old client versions // Load the old client versions
oneOf(db).getMessageIds(txn, localGroup.getId()); oneOf(db).getMessageIds(txn, localGroup.getId());
will(returnValue(singletonList(oldLocalVersionsId))); will(returnValue(singletonList(oldLocalVersionsId)));
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalVersionsId); oneOf(clientHelper).getMessageAsList(txn, oldLocalVersionsId);
will(returnValue(oldLocalVersionsBody)); will(returnValue(oldLocalVersionsBody));
// Delete the old client versions // Delete the old client versions
oneOf(db).removeMessage(txn, oldLocalVersionsId); oneOf(db).removeMessage(txn, oldLocalVersionsId);
@@ -272,7 +272,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
will(returnValue(singletonMap(oldLocalUpdateId, will(returnValue(singletonMap(oldLocalUpdateId,
oldLocalUpdateMeta))); oldLocalUpdateMeta)));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Delete the latest local update // Delete the latest local update
oneOf(db).deleteMessage(txn, oldLocalUpdateId); oneOf(db).deleteMessage(txn, oldLocalUpdateId);
@@ -344,7 +344,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
// Load the old client versions // Load the old client versions
oneOf(db).getMessageIds(txn, localGroup.getId()); oneOf(db).getMessageIds(txn, localGroup.getId());
will(returnValue(singletonList(oldLocalVersionsId))); will(returnValue(singletonList(oldLocalVersionsId)));
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalVersionsId); oneOf(clientHelper).getMessageAsList(txn, oldLocalVersionsId);
will(returnValue(oldLocalVersionsBody)); will(returnValue(oldLocalVersionsBody));
// Delete the old client versions // Delete the old client versions
oneOf(db).removeMessage(txn, oldLocalVersionsId); oneOf(db).removeMessage(txn, oldLocalVersionsId);
@@ -367,10 +367,10 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Load the latest remote update // Load the latest remote update
oneOf(clientHelper).getSmallMessageAsList(txn, oldRemoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
will(returnValue(oldRemoteUpdateBody)); will(returnValue(oldRemoteUpdateBody));
// Delete the latest local update // Delete the latest local update
oneOf(db).deleteMessage(txn, oldLocalUpdateId); oneOf(db).deleteMessage(txn, oldLocalUpdateId);
@@ -451,10 +451,10 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Load the latest remote update // Load the latest remote update
oneOf(clientHelper).getSmallMessageAsList(txn, oldRemoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
will(returnValue(oldRemoteUpdateBody)); will(returnValue(oldRemoteUpdateBody));
// Delete the old remote update // Delete the old remote update
oneOf(db).deleteMessage(txn, oldRemoteUpdateId); oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
@@ -490,7 +490,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
will(returnValue(singletonMap(oldLocalUpdateId, will(returnValue(singletonMap(oldLocalUpdateId,
oldLocalUpdateMeta))); oldLocalUpdateMeta)));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Get client ID // Get client ID
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
@@ -557,10 +557,10 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Load the latest remote update // Load the latest remote update
oneOf(clientHelper).getSmallMessageAsList(txn, oldRemoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
will(returnValue(oldRemoteUpdateBody)); will(returnValue(oldRemoteUpdateBody));
// Delete the old remote update // Delete the old remote update
oneOf(db).deleteMessage(txn, oldRemoteUpdateId); oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
@@ -630,10 +630,10 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
// Load the latest local update // Load the latest local update
oneOf(clientHelper).getSmallMessageAsList(txn, oldLocalUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
will(returnValue(oldLocalUpdateBody)); will(returnValue(oldLocalUpdateBody));
// Load the latest remote update // Load the latest remote update
oneOf(clientHelper).getSmallMessageAsList(txn, oldRemoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, oldRemoteUpdateId);
will(returnValue(oldRemoteUpdateBody)); will(returnValue(oldRemoteUpdateBody));
// Delete the old remote update // Delete the old remote update
oneOf(db).deleteMessage(txn, oldRemoteUpdateId); oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
@@ -734,9 +734,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, localUpdateId); oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody)); will(returnValue(localUpdateBody));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});
@@ -769,9 +769,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, localUpdateId); oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody)); will(returnValue(localUpdateBody));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});
@@ -804,9 +804,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, localUpdateId); oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody)); will(returnValue(localUpdateBody));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});
@@ -839,9 +839,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, localUpdateId); oneOf(clientHelper).getMessageAsList(txn, localUpdateId);
will(returnValue(localUpdateBody)); will(returnValue(localUpdateBody));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});
@@ -901,7 +901,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});
@@ -933,7 +933,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});
@@ -965,7 +965,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
oneOf(clientHelper).getMessageMetadataAsDictionary(txn, oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
will(returnValue(messageMetadata)); will(returnValue(messageMetadata));
oneOf(clientHelper).getSmallMessageAsList(txn, remoteUpdateId); oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId);
will(returnValue(remoteUpdateBody)); will(returnValue(remoteUpdateBody));
}}); }});

View File

@@ -16,14 +16,14 @@ def getStdout = { command, defaultValue ->
} }
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion 29
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion '29.0.2'
defaultConfig { defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion 16
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion 28
versionCode rootProject.ext.versionCode versionCode 10209
versionName rootProject.ext.versionName versionName "1.2.9"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
buildConfigField "String", "GitHash", buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\"" "\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""

View File

@@ -13,9 +13,8 @@ import java.util.Random;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import static androidx.test.InstrumentationRegistry.getContext; import static androidx.test.InstrumentationRegistry.getContext;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@@ -28,7 +27,7 @@ public class AttachmentRetrieverIntegrationTest {
private final ImageHelper imageHelper = new ImageHelperImpl(); private final ImageHelper imageHelper = new ImageHelperImpl();
private final AttachmentRetriever retriever = private final AttachmentRetriever retriever =
new AttachmentRetrieverImpl(null, null, dimensions, imageHelper, new AttachmentRetrieverImpl(null, dimensions, imageHelper,
new ImageSizeCalculator(imageHelper)); new ImageSizeCalculator(imageHelper));
@Test @Test
@@ -36,7 +35,7 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("kitten_small.jpg"); InputStream is = getAssetInputStream("kitten_small.jpg");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth()); assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight()); assertEquals(240, item.getHeight());
@@ -44,7 +43,7 @@ public class AttachmentRetrieverIntegrationTest {
assertEquals(240, item.getThumbnailHeight()); assertEquals(240, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType()); assertEquals("image/jpeg", item.getMimeType());
assertJpgOrJpeg(item.getExtension()); assertJpgOrJpeg(item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -52,7 +51,7 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("kitten_original.jpg"); InputStream is = getAssetInputStream("kitten_original.jpg");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth()); assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight()); assertEquals(2592, item.getHeight());
@@ -60,7 +59,7 @@ public class AttachmentRetrieverIntegrationTest {
assertEquals(dimensions.maxHeight, item.getThumbnailHeight()); assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType()); assertEquals("image/jpeg", item.getMimeType());
assertJpgOrJpeg(item.getExtension()); assertJpgOrJpeg(item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -68,7 +67,7 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getAssetInputStream("kitten.png"); InputStream is = getAssetInputStream("kitten.png");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(msgId, item.getMessageId()); assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth()); assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight()); assertEquals(510, item.getHeight());
@@ -76,7 +75,7 @@ public class AttachmentRetrieverIntegrationTest {
assertEquals(138, item.getThumbnailHeight()); assertEquals(138, item.getThumbnailHeight());
assertEquals("image/png", item.getMimeType()); assertEquals("image/png", item.getMimeType());
assertEquals("png", item.getExtension()); assertEquals("png", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -84,14 +83,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("uber.gif"); InputStream is = getAssetInputStream("uber.gif");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType()); assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension()); assertEquals("gif", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -99,14 +98,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("lottapixel.jpg"); InputStream is = getAssetInputStream("lottapixel.jpg");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(64250, item.getWidth()); assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight()); assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType()); assertEquals("image/jpeg", item.getMimeType());
assertJpgOrJpeg(item.getExtension()); assertJpgOrJpeg(item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -114,14 +113,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getAssetInputStream("image_io_crash.png"); InputStream is = getAssetInputStream("image_io_crash.png");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1184, item.getWidth()); assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight()); assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/png", item.getMimeType()); assertEquals("image/png", item.getMimeType());
assertEquals("png", item.getExtension()); assertEquals("png", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -129,14 +128,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("gimp_crash.gif"); InputStream is = getAssetInputStream("gimp_crash.gif");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType()); assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension()); assertEquals("gif", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -144,14 +143,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("opti_png_afl.gif"); InputStream is = getAssetInputStream("opti_png_afl.gif");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(32, item.getWidth()); assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight()); assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth()); assertEquals(dimensions.minHeight, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType()); assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension()); assertEquals("gif", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -159,8 +158,8 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("libraw_error.jpg"); InputStream is = getAssetInputStream("libraw_error.jpg");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(ERROR, item.getState()); assertTrue(item.hasError());
} }
@Test @Test
@@ -168,14 +167,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif"); InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(65535, item.getWidth()); assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight()); assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType()); assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension()); assertEquals("gif", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -183,14 +182,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif"); InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(10000, item.getWidth()); assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight()); assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType()); assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension()); assertEquals("gif", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -198,14 +197,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif"); InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(16384, item.getWidth()); assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight()); assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxWidth, item.getThumbnailHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailHeight());
assertEquals("image/gif", item.getMimeType()); assertEquals("image/gif", item.getMimeType());
assertEquals("gif", item.getExtension()); assertEquals("gif", item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -213,14 +212,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg"); InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1, item.getWidth()); assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight()); assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth()); assertEquals(dimensions.minWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxHeight, item.getThumbnailHeight()); assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType()); assertEquals("image/jpeg", item.getMimeType());
assertJpgOrJpeg(item.getExtension()); assertJpgOrJpeg(item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
@Test @Test
@@ -228,14 +227,14 @@ public class AttachmentRetrieverIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg"); InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(h, is); Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.createAttachmentItem(a, true); AttachmentItem item = retriever.getAttachmentItem(a, true);
assertEquals(1920, item.getWidth()); assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight()); assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.minHeight, item.getThumbnailHeight()); assertEquals(dimensions.minHeight, item.getThumbnailHeight());
assertEquals("image/jpeg", item.getMimeType()); assertEquals("image/jpeg", item.getMimeType());
assertJpgOrJpeg(item.getExtension()); assertJpgOrJpeg(item.getExtension());
assertEquals(AVAILABLE, item.getState()); assertFalse(item.hasError());
} }
private InputStream getAssetInputStream(String name) throws Exception { private InputStream getAssetInputStream(String name) throws Exception {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -34,7 +34,6 @@ import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@@ -76,12 +75,8 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@UiThread @UiThread
public LiveData<AttachmentResult> storeAttachments( public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) { LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || result != null || !uris.isEmpty()) { if (task != null || result != null || !uris.isEmpty())
if (task != null) LOG.warning("Task already exists!");
if (result != null) LOG.warning("Result already exists!");
if (!uris.isEmpty()) LOG.warning("Uris available: " + uris);
throw new IllegalStateException(); throw new IllegalStateException();
}
MutableLiveData<AttachmentResult> result = new MutableLiveData<>(); MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
this.result = result; this.result = result;
uris.addAll(newUris); uris.addAll(newUris);
@@ -100,12 +95,8 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@UiThread @UiThread
public LiveData<AttachmentResult> getLiveAttachments() { public LiveData<AttachmentResult> getLiveAttachments() {
MutableLiveData<AttachmentResult> result = this.result; MutableLiveData<AttachmentResult> result = this.result;
if (task == null || result == null || uris.isEmpty()) { if (task == null || result == null || uris.isEmpty())
if (task == null) LOG.warning("No Task!");
if (result == null) LOG.warning("No Result!");
if (uris.isEmpty()) LOG.warning("Uris empty!");
throw new IllegalStateException(); throw new IllegalStateException();
}
// A task is already running. It will update the result LiveData. // A task is already running. It will update the result LiveData.
// So nothing more to do here. // So nothing more to do here.
return result; return result;
@@ -118,8 +109,8 @@ class AttachmentCreatorImpl implements AttachmentCreator {
// get and cache AttachmentItem for ImagePreview // get and cache AttachmentItem for ImagePreview
try { try {
Attachment a = retriever.getMessageAttachment(h); Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.createAttachmentItem(a, needsSize); AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.getState() == ERROR) throw new IOException(); if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult = AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item); new AttachmentItemResult(uri, item);
itemResults.add(itemResult); itemResults.add(itemResult);
@@ -176,13 +167,21 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@Override @Override
@UiThread @UiThread
public void onAttachmentsSent(MessageId id) { public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState(); resetState();
} }
@Override @Override
@UiThread @UiThread
public void cancel() { public void cancel() {
if (task != null) task.cancel(); if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments(); deleteUnsentAttachments();
resetState(); resetState();
} }

View File

@@ -7,33 +7,24 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import static java.lang.System.arraycopy;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.toHexString;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.MISSING;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class AttachmentItem implements Parcelable { public class AttachmentItem implements Parcelable {
public enum State {
LOADING, MISSING, AVAILABLE, ERROR;
public boolean isFinal() {
return this == AVAILABLE || this == ERROR;
}
}
private final AttachmentHeader header; private final AttachmentHeader header;
private final int width, height; private final int width, height;
private final String extension; private final String extension;
private final int thumbnailWidth, thumbnailHeight; private final int thumbnailWidth, thumbnailHeight;
private final State state; private final boolean hasError;
private final long instanceId;
public static final Creator<AttachmentItem> CREATOR = public static final Creator<AttachmentItem> CREATOR =
new Creator<AttachmentItem>() { new Creator<AttachmentItem>() {
@@ -48,33 +39,19 @@ public class AttachmentItem implements Parcelable {
} }
}; };
private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0);
AttachmentItem(AttachmentHeader header, int width, int height, AttachmentItem(AttachmentHeader header, int width, int height,
String extension, int thumbnailWidth, int thumbnailHeight, String extension, int thumbnailWidth, int thumbnailHeight,
State state) { boolean hasError) {
this.header = header; this.header = header;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.extension = extension; this.extension = extension;
this.thumbnailWidth = thumbnailWidth; this.thumbnailWidth = thumbnailWidth;
this.thumbnailHeight = thumbnailHeight; this.thumbnailHeight = thumbnailHeight;
this.state = state; this.hasError = hasError;
} instanceId = NEXT_INSTANCE_ID.getAndIncrement();
/**
* Use only for {@link State MISSING} or {@link State LOADING} items.
*/
AttachmentItem(AttachmentHeader header, int width, int height,
State state) {
this(header, width, height, "", width, height, state);
if (state != MISSING && state != LOADING)
throw new IllegalArgumentException();
}
/**
* Use when the item does not need a size.
*/
AttachmentItem(AttachmentHeader header, String extension, State state) {
this(header, 0, 0, extension, 0, 0, state);
} }
protected AttachmentItem(Parcel in) { protected AttachmentItem(Parcel in) {
@@ -87,7 +64,8 @@ public class AttachmentItem implements Parcelable {
extension = requireNonNull(in.readString()); extension = requireNonNull(in.readString());
thumbnailWidth = in.readInt(); thumbnailWidth = in.readInt();
thumbnailHeight = in.readInt(); thumbnailHeight = in.readInt();
state = State.valueOf(requireNonNull(in.readString())); hasError = in.readByte() != 0;
instanceId = in.readLong();
header = new AttachmentHeader(messageId, mimeType); header = new AttachmentHeader(messageId, mimeType);
} }
@@ -123,16 +101,12 @@ public class AttachmentItem implements Parcelable {
return thumbnailHeight; return thumbnailHeight;
} }
public State getState() { public boolean hasError() {
return state; return hasError;
} }
public String getTransitionName(MessageId conversationItemId) { public String getTransitionName() {
int len = MessageId.LENGTH; return String.valueOf(instanceId);
byte[] instanceId = new byte[len * 2];
arraycopy(header.getMessageId().getBytes(), 0, instanceId, 0, len);
arraycopy(conversationItemId.getBytes(), 0, instanceId, len, len);
return toHexString(instanceId);
} }
@Override @Override
@@ -149,23 +123,14 @@ public class AttachmentItem implements Parcelable {
dest.writeString(extension); dest.writeString(extension);
dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailWidth);
dest.writeInt(thumbnailHeight); dest.writeInt(thumbnailHeight);
dest.writeString(state.name()); dest.writeByte((byte) (hasError ? 1 : 0));
dest.writeLong(instanceId);
} }
/**
* This is used to identity if two items are the same,
* irrespective of their state or size.
*/
@Override @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
return o instanceof AttachmentItem && return o instanceof AttachmentItem &&
header.getMessageId().equals( instanceId == ((AttachmentItem) o).instanceId;
((AttachmentItem) o).header.getMessageId()
);
} }
@Override
public int hashCode() {
return header.getMessageId().hashCode();
}
} }

View File

@@ -1,63 +1,29 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import androidx.lifecycle.LiveData; import androidx.annotation.Nullable;
@NotNullByDefault @NotNullByDefault
public interface AttachmentRetriever { public interface AttachmentRetriever {
@DatabaseExecutor void cachePut(MessageId messageId, List<AttachmentItem> attachments);
@Nullable
List<AttachmentItem> cacheGet(MessageId messageId);
Attachment getMessageAttachment(AttachmentHeader h) throws DbException; Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
/**
* Returns a list of observable {@link LiveData}
* that get updated as the state of their {@link AttachmentItem}s changes.
*/
List<LiveData<AttachmentItem>> getAttachmentItems(
PrivateMessageHeader messageHeader);
/**
* Retrieves item size and adds the item to the cache, if available.
* <p>
* Use this to eagerly load the attachment size before it gets displayed.
* This is needed for messages containing a single attachment.
* Messages with more than one attachment use a standard size.
*/
@DatabaseExecutor
void cacheAttachmentItemWithSize(MessageId conversationMessageId,
AttachmentHeader h) throws DbException;
/** /**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s * Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns. * {@link InputStream} which will be closed when this method returns.
*/ */
AttachmentItem createAttachmentItem(Attachment a, boolean needsSize); AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
/**
* Loads an {@link AttachmentItem}
* that arrived via an {@link AttachmentReceivedEvent}
* and notifies the associated {@link LiveData}.
*
* Note that you need to call {@link #getAttachmentItems(PrivateMessageHeader)}
* first to get the LiveData.
*
* It is possible that no LiveData is available,
* because the message of the AttachmentItem did not arrive, yet.
* In this case, the load wil be deferred until the message arrives.
*/
@DatabaseExecutor
void loadAttachmentItem(MessageId attachmentId);
} }

View File

@@ -1,39 +1,25 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem.State;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.lifecycle.LiveData; import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.LOADING;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.MISSING;
@NotNullByDefault @NotNullByDefault
class AttachmentRetrieverImpl implements AttachmentRetriever { class AttachmentRetrieverImpl implements AttachmentRetriever {
@@ -41,8 +27,6 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
private static final Logger LOG = private static final Logger LOG =
getLogger(AttachmentRetrieverImpl.class.getName()); getLogger(AttachmentRetrieverImpl.class.getName());
@DatabaseExecutor
private final Executor dbExecutor;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ImageHelper imageHelper; private final ImageHelper imageHelper;
private final ImageSizeCalculator imageSizeCalculator; private final ImageSizeCalculator imageSizeCalculator;
@@ -50,17 +34,13 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
private final int minWidth, maxWidth; private final int minWidth, maxWidth;
private final int minHeight, maxHeight; private final int minHeight, maxHeight;
private final ConcurrentMap<MessageId, MutableLiveData<AttachmentItem>> private final Map<MessageId, List<AttachmentItem>> attachmentCache =
itemsWithSize = new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private final ConcurrentMap<MessageId, MutableLiveData<AttachmentItem>>
itemsWithoutSize = new ConcurrentHashMap<>();
@Inject @Inject
AttachmentRetrieverImpl(@DatabaseExecutor Executor dbExecutor, AttachmentRetrieverImpl(MessagingManager messagingManager,
MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper, AttachmentDimensions dimensions, ImageHelper imageHelper,
ImageSizeCalculator imageSizeCalculator) { ImageSizeCalculator imageSizeCalculator) {
this.dbExecutor = dbExecutor;
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.imageHelper = imageHelper; this.imageHelper = imageHelper;
this.imageSizeCalculator = imageSizeCalculator; this.imageSizeCalculator = imageSizeCalculator;
@@ -72,143 +52,40 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
} }
@Override @Override
@DatabaseExecutor public void cachePut(MessageId messageId,
List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Override
@Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
@Override
public Attachment getMessageAttachment(AttachmentHeader h) public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException { throws DbException {
return messagingManager.getAttachment(h); return messagingManager.getAttachment(h);
} }
@Override @Override
public List<LiveData<AttachmentItem>> getAttachmentItems( public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
PrivateMessageHeader messageHeader) {
List<AttachmentHeader> headers = messageHeader.getAttachmentHeaders();
List<LiveData<AttachmentItem>> items = new ArrayList<>(headers.size());
boolean needsSize = headers.size() == 1;
for (AttachmentHeader h : headers) {
// try cache for existing item live data
MutableLiveData<AttachmentItem> liveData;
if (needsSize) liveData = itemsWithSize.get(h.getMessageId());
else {
// try items with size first, as they work as well
liveData = itemsWithSize.get(h.getMessageId());
if (liveData == null)
liveData = itemsWithoutSize.get(h.getMessageId());
}
// create new live data with LOADING item if cache miss
if (liveData == null) {
AttachmentItem item = new AttachmentItem(h,
defaultSize, defaultSize, LOADING);
liveData = new MutableLiveData<>(item);
// add new LiveData to cache, checking for concurrent updates
MutableLiveData<AttachmentItem> oldLiveData;
if (needsSize) {
oldLiveData = itemsWithSize.putIfAbsent(h.getMessageId(),
liveData);
} else {
oldLiveData = itemsWithoutSize.putIfAbsent(h.getMessageId(),
liveData);
}
if (oldLiveData == null) {
// kick-off loading of attachment, will post to live data
MutableLiveData<AttachmentItem> finalLiveData = liveData;
dbExecutor.execute(() ->
loadAttachmentItem(h, needsSize, finalLiveData));
} else {
// Concurrent cache update - use the existing live data
liveData = oldLiveData;
}
}
items.add(liveData);
}
return items;
}
@Override
@DatabaseExecutor
public void cacheAttachmentItemWithSize(MessageId conversationMessageId,
AttachmentHeader h) throws DbException {
// If a live data is already cached we don't need to do anything
if (itemsWithSize.containsKey(h.getMessageId())) return;
try {
Attachment a = messagingManager.getAttachment(h);
AttachmentItem item = createAttachmentItem(a, true);
MutableLiveData<AttachmentItem> liveData =
new MutableLiveData<>(item);
// If a live data was concurrently cached, don't replace it
itemsWithSize.putIfAbsent(h.getMessageId(), liveData);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
}
}
@Override
@DatabaseExecutor
public void loadAttachmentItem(MessageId attachmentId) {
// try to find LiveData for attachment in both caches
MutableLiveData<AttachmentItem> liveData;
boolean needsSize = true;
liveData = itemsWithSize.get(attachmentId);
if (liveData == null) {
needsSize = false;
liveData = itemsWithoutSize.get(attachmentId);
}
// If no LiveData for the attachment exists,
// its message did not yet arrive and we can ignore it for now.
if (liveData == null) return;
// actually load the attachment item
AttachmentHeader h = requireNonNull(liveData.getValue()).getHeader();
loadAttachmentItem(h, needsSize, liveData);
}
/**
* Loads an {@link AttachmentItem} from the database
* and notifies the given {@link LiveData}.
*/
@DatabaseExecutor
private void loadAttachmentItem(AttachmentHeader h, boolean needsSize,
MutableLiveData<AttachmentItem> liveData) {
Attachment a;
AttachmentItem item;
try {
a = messagingManager.getAttachment(h);
item = createAttachmentItem(a, needsSize);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
item = new AttachmentItem(h, defaultSize, defaultSize, MISSING);
} catch (DbException e) {
logException(LOG, WARNING, e);
item = new AttachmentItem(h, "", ERROR);
}
liveData.postValue(item);
}
@Override
public AttachmentItem createAttachmentItem(Attachment a,
boolean needsSize) {
AttachmentItem item;
AttachmentHeader h = a.getHeader(); AttachmentHeader h = a.getHeader();
if (needsSize) { if (!needsSize) {
InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
tryToClose(is, LOG, WARNING);
item = createAttachmentItem(h, size);
} else {
String extension = String extension =
imageHelper.getExtensionFromMimeType(h.getContentType()); imageHelper.getExtensionFromMimeType(h.getContentType());
State state = AVAILABLE; boolean hasError = false;
if (extension == null) { if (extension == null) {
extension = ""; extension = "";
state = ERROR; hasError = true;
} }
item = new AttachmentItem(h, extension, state); return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
return item;
} }
private AttachmentItem createAttachmentItem(AttachmentHeader h, Size size) { InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
// calculate thumbnail size // calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType); Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) { if (!size.error) {
@@ -227,9 +104,8 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
hasError = true; hasError = true;
} }
if (extension == null) extension = ""; if (extension == null) extension = "";
State state = hasError ? ERROR : AVAILABLE; return new AttachmentItem(h, size.width, size.height, extension,
return new AttachmentItem(h, size.width, size.height, thumbnailSize.width, thumbnailSize.height, hasError);
extension, thumbnailSize.width, thumbnailSize.height, state);
} }
private Size getThumbnailSize(int width, int height, String mimeType) { private Size getThumbnailSize(int width, int height, String mimeType) {

View File

@@ -28,6 +28,7 @@ import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
@@ -64,16 +65,17 @@ import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationManager; import org.briarproject.briar.api.conversation.ConversationManager;
import org.briarproject.briar.api.conversation.ConversationMessageHeader; import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.conversation.ConversationMessageVisitor;
import org.briarproject.briar.api.conversation.ConversationRequest; import org.briarproject.briar.api.conversation.ConversationRequest;
import org.briarproject.briar.api.conversation.ConversationResponse; import org.briarproject.briar.api.conversation.ConversationResponse;
import org.briarproject.briar.api.conversation.DeletionResult; import org.briarproject.briar.api.conversation.DeletionResult;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent; import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList; import java.util.ArrayList;
@@ -95,7 +97,6 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat; import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
@@ -117,6 +118,8 @@ import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimati
import static androidx.core.view.ViewCompat.setTransitionName; import static androidx.core.view.ViewCompat.setTransitionName;
import static androidx.lifecycle.Lifecycle.State.STARTED; import static androidx.lifecycle.Lifecycle.State.STARTED;
import static androidx.recyclerview.widget.SortedList.INVALID_POSITION; import static androidx.recyclerview.widget.SortedList.INVALID_POSITION;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort; import static java.util.Collections.sort;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
@@ -133,7 +136,6 @@ import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRO
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
import static org.briarproject.briar.android.conversation.ImageActivity.DATE; import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
import static org.briarproject.briar.android.conversation.ImageActivity.ITEM_ID;
import static org.briarproject.briar.android.conversation.ImageActivity.NAME; import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
@@ -183,6 +185,8 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager; volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>(); private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> { private final Observer<String> contactNameObserver = name -> {
requireNonNull(name); requireNonNull(name);
loadMessages(); loadMessages();
@@ -536,7 +540,6 @@ public class ConversationActivity extends BriarActivity
}); });
} }
@DatabaseExecutor
private void eagerlyLoadMessageSize(PrivateMessageHeader h) { private void eagerlyLoadMessageSize(PrivateMessageHeader h) {
try { try {
MessageId id = h.getId(); MessageId id = h.getId();
@@ -553,11 +556,21 @@ public class ConversationActivity extends BriarActivity
// images we use a grid so the size is fixed // images we use a grid so the size is fixed
List<AttachmentHeader> headers = h.getAttachmentHeaders(); List<AttachmentHeader> headers = h.getAttachmentHeaders();
if (headers.size() == 1) { if (headers.size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message"); LOG.info("Eagerly loading image size for latest message");
AttachmentHeader header = headers.get(0); AttachmentHeader header = headers.get(0);
// get the item to retrieve its size try {
attachmentRetriever Attachment a = attachmentRetriever
.cacheAttachmentItemWithSize(h.getId(), header); .getMessageAttachment(header);
AttachmentItem item =
attachmentRetriever.getAttachmentItem(a, true);
attachmentRetriever.cachePut(id, singletonList(item));
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
}
}
} }
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
@@ -638,18 +651,59 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager); && adapter.isScrolledToBottom(layoutManager);
} }
@UiThread private void loadMessageAttachments(PrivateMessageHeader h) {
private void updateMessageAttachment(MessageId m, AttachmentItem item) { // TODO: Use placeholders for missing/invalid attachments
Pair<Integer, ConversationMessageItem> pair = adapter.getMessageItem(m); runOnDbThread(() -> {
if (pair != null && pair.getSecond().updateAttachments(item)) { try {
// TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentHeader> headers = h.getAttachmentHeaders();
boolean needsSize = headers.size() == 1;
List<AttachmentItem> items = new ArrayList<>(headers.size());
for (AttachmentHeader header : headers) {
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item = attachmentRetriever
.getAttachmentItem(a, needsSize);
items.add(item);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
return;
}
}
// Don't cache items unless all are present and valid
attachmentRetriever.cachePut(h.getId(), items);
displayMessageAttachments(h.getId(), items);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void displayMessageAttachments(MessageId m,
List<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> {
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
boolean scroll = shouldScrollWhenUpdatingMessage(); boolean scroll = shouldScrollWhenUpdatingMessage();
pair.getSecond().setAttachments(items);
adapter.notifyItemChanged(pair.getFirst()); adapter.notifyItemChanged(pair.getFirst());
if (scroll) scrollToBottom(); if (scroll) scrollToBottom();
} }
});
} }
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
onAttachmentReceived(a.getMessageId());
}
}
if (e instanceof ContactRemovedEvent) { if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) { if (c.getContactId().equals(contactId)) {
@@ -709,6 +763,15 @@ public class ConversationActivity extends BriarActivity
scrollToBottom(); scrollToBottom();
} }
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread @UiThread
private void onNewConversationMessage(ConversationMessageHeader h) { private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest || if (h instanceof ConversationRequest ||
@@ -717,7 +780,7 @@ public class ConversationActivity extends BriarActivity
observeOnce(viewModel.getContactDisplayName(), this, observeOnce(viewModel.getContactDisplayName(), this,
name -> addConversationItem(h.accept(visitor))); name -> addConversationItem(h.accept(visitor)));
} else { } else {
// visitor also loads message text and attachments (if existing) // visitor also loads message text (if existing)
addConversationItem(h.accept(visitor)); addConversationItem(h.accept(visitor));
} }
} }
@@ -1044,9 +1107,8 @@ public class ConversationActivity extends BriarActivity
i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item)); i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item));
i.putExtra(NAME, name); i.putExtra(NAME, name);
i.putExtra(DATE, messageItem.getTime()); i.putExtra(DATE, messageItem.getTime());
i.putExtra(ITEM_ID, messageItem.getId().getBytes());
// restoring list position should not trigger android bug #224270 // restoring list position should not trigger android bug #224270
String transitionName = item.getTransitionName(messageItem.getId()); String transitionName = item.getTransitionName();
ActivityOptionsCompat options = ActivityOptionsCompat options =
makeSceneTransitionAnimation(this, view, transitionName); makeSceneTransitionAnimation(this, view, transitionName);
ActivityCompat.startActivity(this, i, options.toBundle()); ActivityCompat.startActivity(this, i, options.toBundle());
@@ -1085,41 +1147,15 @@ public class ConversationActivity extends BriarActivity
return text; return text;
} }
/**
* Called by {@link PrivateMessageHeader#accept(ConversationMessageVisitor)}
*/
@Override @Override
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) { public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
List<LiveData<AttachmentItem>> liveDataList = List<AttachmentItem> attachments =
attachmentRetriever.getAttachmentItems(h); attachmentRetriever.cacheGet(h.getId());
List<AttachmentItem> items = new ArrayList<>(liveDataList.size()); if (attachments == null) {
for (LiveData<AttachmentItem> liveData : liveDataList) { loadMessageAttachments(h);
// first remove all our observers to avoid having more than one return emptyList();
// in case we reload the conversation, e.g. after deleting messages
liveData.removeObservers(this);
// add a new observer
liveData.observe(this, new AttachmentObserver(h.getId(), liveData));
items.add(requireNonNull(liveData.getValue()));
}
return items;
}
private class AttachmentObserver implements Observer<AttachmentItem> {
private final MessageId conversationMessageId;
private final LiveData<AttachmentItem> liveData;
private AttachmentObserver(MessageId conversationMessageId,
LiveData<AttachmentItem> liveData) {
this.conversationMessageId = conversationMessageId;
this.liveData = liveData;
}
@Override
public void onChanged(AttachmentItem attachmentItem) {
updateMessageAttachment(conversationMessageId, attachmentItem);
if (attachmentItem.getState().isFinal())
liveData.removeObserver(this);
} }
return attachments;
} }
} }

View File

@@ -9,13 +9,12 @@ import java.util.List;
import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread;
@NotThreadSafe @NotThreadSafe
@NotNullByDefault @NotNullByDefault
class ConversationMessageItem extends ConversationItem { class ConversationMessageItem extends ConversationItem {
private final List<AttachmentItem> attachments; private List<AttachmentItem> attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h, ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
List<AttachmentItem> attachments) { List<AttachmentItem> attachments) {
@@ -27,14 +26,8 @@ class ConversationMessageItem extends ConversationItem {
return attachments; return attachments;
} }
@UiThread void setAttachments(List<AttachmentItem> attachments) {
boolean updateAttachments(AttachmentItem item) { this.attachments = attachments;
int pos = attachments.indexOf(item);
if (pos != -1 && attachments.get(pos).getState() != item.getState()) {
attachments.set(pos, item);
return true;
}
return false;
} }
} }

View File

@@ -11,9 +11,6 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.db.TransactionManager;
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.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
@@ -33,7 +30,6 @@ import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -60,7 +56,7 @@ import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends AndroidViewModel public class ConversationViewModel extends AndroidViewModel
implements EventListener, AttachmentManager { implements AttachmentManager {
private static Logger LOG = private static Logger LOG =
getLogger(ConversationViewModel.class.getName()); getLogger(ConversationViewModel.class.getName());
@@ -73,7 +69,6 @@ public class ConversationViewModel extends AndroidViewModel
@DatabaseExecutor @DatabaseExecutor
private final Executor dbExecutor; private final Executor dbExecutor;
private final TransactionManager db; private final TransactionManager db;
private final EventBus eventBus;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final SettingsManager settingsManager; private final SettingsManager settingsManager;
@@ -106,7 +101,6 @@ public class ConversationViewModel extends AndroidViewModel
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
TransactionManager db, TransactionManager db,
EventBus eventBus,
MessagingManager messagingManager, MessagingManager messagingManager,
ContactManager contactManager, ContactManager contactManager,
SettingsManager settingsManager, SettingsManager settingsManager,
@@ -116,7 +110,6 @@ public class ConversationViewModel extends AndroidViewModel
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.db = db; this.db = db;
this.eventBus = eventBus;
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.contactManager = contactManager; this.contactManager = contactManager;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@@ -126,27 +119,12 @@ public class ConversationViewModel extends AndroidViewModel
messagingGroupId = Transformations messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId()); .map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false); contactDeleted.setValue(false);
eventBus.addListener(this);
} }
@Override @Override
protected void onCleared() { protected void onCleared() {
super.onCleared(); super.onCleared();
attachmentCreator.cancel(); // also deletes unsent attachments attachmentCreator.deleteUnsentAttachments();
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
dbExecutor.execute(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId()));
}
}
} }
/** /**
@@ -274,7 +252,6 @@ public class ConversationViewModel extends AndroidViewModel
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} }
@UiThread
private void createMessage(GroupId groupId, @Nullable String text, private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> headers, long timestamp, List<AttachmentHeader> headers, long timestamp,
boolean hasImageSupport) { boolean hasImageSupport) {
@@ -293,7 +270,6 @@ public class ConversationViewModel extends AndroidViewModel
} }
} }
@UiThread
private void storeMessage(PrivateMessage m) { private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId()); attachmentCreator.onAttachmentsSent(m.getMessage().getId());
dbExecutor.execute(() -> { dbExecutor.execute(() -> {

View File

@@ -16,7 +16,6 @@ import com.google.android.material.appbar.AppBarLayout;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
@@ -68,7 +67,6 @@ public class ImageActivity extends BriarActivity
final static String ATTACHMENT_POSITION = "position"; final static String ATTACHMENT_POSITION = "position";
final static String NAME = "name"; final static String NAME = "name";
final static String DATE = "date"; final static String DATE = "date";
final static String ITEM_ID = "itemId";
@RequiresApi(api = 16) @RequiresApi(api = 16)
private final static int UI_FLAGS_DEFAULT = private final static int UI_FLAGS_DEFAULT =
@@ -82,7 +80,6 @@ public class ImageActivity extends BriarActivity
private AppBarLayout appBarLayout; private AppBarLayout appBarLayout;
private ViewPager viewPager; private ViewPager viewPager;
private List<AttachmentItem> attachments; private List<AttachmentItem> attachments;
private MessageId conversationMessageId;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -101,20 +98,9 @@ public class ImageActivity extends BriarActivity
setSceneTransitionAnimation(transition, null, transition); setSceneTransitionAnimation(transition, null, transition);
} }
// Intent Extras
Intent i = getIntent();
attachments =
requireNonNull(i.getParcelableArrayListExtra(ATTACHMENTS));
int position = i.getIntExtra(ATTACHMENT_POSITION, -1);
if (position == -1) throw new IllegalStateException();
String name = i.getStringExtra(NAME);
long time = i.getLongExtra(DATE, 0);
byte[] messageIdBytes = requireNonNull(i.getByteArrayExtra(ITEM_ID));
// get View Model // get View Model
viewModel = ViewModelProviders.of(this, viewModelFactory) viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ImageViewModel.class); .get(ImageViewModel.class);
viewModel.expectAttachments(attachments);
viewModel.getSaveState().observeEvent(this, viewModel.getSaveState().observeEvent(this,
this::onImageSaveStateChanged); this::onImageSaveStateChanged);
@@ -138,11 +124,16 @@ public class ImageActivity extends BriarActivity
TextView contactName = toolbar.findViewById(R.id.contactName); TextView contactName = toolbar.findViewById(R.id.contactName);
TextView dateView = toolbar.findViewById(R.id.dateView); TextView dateView = toolbar.findViewById(R.id.dateView);
// Set contact name and message time // Intent Extras
Intent i = getIntent();
attachments = i.getParcelableArrayListExtra(ATTACHMENTS);
int position = i.getIntExtra(ATTACHMENT_POSITION, -1);
if (position == -1) throw new IllegalStateException();
String name = i.getStringExtra(NAME);
long time = i.getLongExtra(DATE, 0);
String date = formatDateAbsolute(this, time); String date = formatDateAbsolute(this, time);
contactName.setText(name); contactName.setText(name);
dateView.setText(date); dateView.setText(date);
conversationMessageId = new MessageId(messageIdBytes);
// Set up image ViewPager // Set up image ViewPager
viewPager = findViewById(R.id.viewPager); viewPager = findViewById(R.id.viewPager);
@@ -329,8 +320,8 @@ public class ImageActivity extends BriarActivity
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
Fragment f = ImageFragment.newInstance( Fragment f = ImageFragment
attachments.get(position), conversationMessageId, isFirst); .newInstance(attachments.get(position), isFirst);
isFirst = false; isFirst = false;
return f; return f;
} }

View File

@@ -49,8 +49,7 @@ class ImageAdapter extends Adapter<ImageViewHolder> {
public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) { public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate( View v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_image, viewGroup, false); R.layout.list_item_image, viewGroup, false);
requireNonNull(conversationItem); return new ImageViewHolder(v, imageSize);
return new ImageViewHolder(v, imageSize, conversationItem.getId());
} }
@Override @Override

View File

@@ -15,7 +15,6 @@ import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView; import com.github.chrisbanes.photoview.PhotoView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
@@ -24,7 +23,6 @@ import org.briarproject.briar.android.conversation.glide.GlideApp;
import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@@ -34,36 +32,27 @@ import static android.os.Build.VERSION.SDK_INT;
import static android.widget.ImageView.ScaleType.FIT_START; import static android.widget.ImageView.ScaleType.FIT_START;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION;
import static org.briarproject.briar.android.conversation.ImageActivity.ITEM_ID;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersAreNonnullByDefault @ParametersAreNonnullByDefault
public class ImageFragment extends Fragment public class ImageFragment extends Fragment {
implements RequestListener<Drawable> {
private final static String IS_FIRST = "isFirst"; private final static String IS_FIRST = "isFirst";
@DrawableRes
private static final int ERROR_RES = R.drawable.ic_image_broken;
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
private AttachmentItem attachment; private AttachmentItem attachment;
private boolean isFirst; private boolean isFirst;
private MessageId conversationItemId;
private ImageViewModel viewModel; private ImageViewModel viewModel;
private PhotoView photoView; private PhotoView photoView;
static ImageFragment newInstance(AttachmentItem a, static ImageFragment newInstance(AttachmentItem a, boolean isFirst) {
MessageId conversationMessageId, boolean isFirst) {
ImageFragment f = new ImageFragment(); ImageFragment f = new ImageFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putParcelable(ATTACHMENT_POSITION, a); args.putParcelable(ATTACHMENT_POSITION, a);
args.putBoolean(IS_FIRST, isFirst); args.putBoolean(IS_FIRST, isFirst);
args.putByteArray(ITEM_ID, conversationMessageId.getBytes());
f.setArguments(args); f.setArguments(args);
return f; return f;
} }
@@ -81,8 +70,6 @@ public class ImageFragment extends Fragment
Bundle args = requireNonNull(getArguments()); Bundle args = requireNonNull(getArguments());
attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION)); attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION));
isFirst = args.getBoolean(IS_FIRST); isFirst = args.getBoolean(IS_FIRST);
conversationItemId =
new MessageId(requireNonNull(args.getByteArray(ITEM_ID)));
} }
@Nullable @Nullable
@@ -100,43 +87,15 @@ public class ImageFragment extends Fragment
photoView.setScaleLevels(1, 2, 4); photoView.setScaleLevels(1, 2, 4);
photoView.setOnClickListener(view -> viewModel.clickImage()); photoView.setOnClickListener(view -> viewModel.clickImage());
if (attachment.getState() == AVAILABLE) { // Request Listener
loadImage(); RequestListener<Drawable> listener = new RequestListener<Drawable>() {
// postponed transition will be started when Image was loaded
} else if (attachment.getState() == ERROR) {
photoView.setImageResource(ERROR_RES);
startPostponedTransition();
} else {
photoView.setImageResource(R.drawable.ic_image_missing);
startPostponedTransition();
// state is not final, so observe state changes
viewModel.getOnAttachmentReceived(attachment.getMessageId())
.observeEvent(this, this::onAttachmentReceived);
}
return v;
}
private void loadImage() {
GlideApp.with(this)
.load(attachment)
// TODO allow if size < maxTextureSize ?
// .override(SIZE_ORIGINAL)
.diskCacheStrategy(NONE)
.error(ERROR_RES)
.addListener(this)
.into(photoView);
}
private void onAttachmentReceived(Boolean received) {
if (received) loadImage();
}
@Override @Override
public boolean onLoadFailed(@Nullable GlideException e, public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target, Object model, Target<Drawable> target,
boolean isFirstResource) { boolean isFirstResource) {
startPostponedTransition(); if (getActivity() != null && isFirst)
getActivity().supportStartPostponedEnterTransition();
return false; return false;
} }
@@ -148,20 +107,30 @@ public class ImageFragment extends Fragment
// set transition name only when not animatable, // set transition name only when not animatable,
// because the animation won't start otherwise // because the animation won't start otherwise
photoView.setTransitionName( photoView.setTransitionName(
attachment.getTransitionName(conversationItemId)); attachment.getTransitionName());
} }
// Move image to the top if overlapping toolbar // Move image to the top if overlapping toolbar
if (viewModel.isOverlappingToolbar(photoView, resource)) { if (viewModel.isOverlappingToolbar(photoView, resource)) {
photoView.setScaleType(FIT_START); photoView.setScaleType(FIT_START);
} }
startPostponedTransition();
return false;
}
private void startPostponedTransition() {
if (getActivity() != null && isFirst) { if (getActivity() != null && isFirst) {
getActivity().supportStartPostponedEnterTransition(); getActivity().supportStartPostponedEnterTransition();
} }
return false;
}
};
// Load Image
GlideApp.with(this)
.load(attachment)
// TODO allow if size < maxTextureSize ?
// .override(SIZE_ORIGINAL)
.diskCacheStrategy(NONE)
.error(R.drawable.ic_image_broken)
.addListener(listener)
.into(photoView);
return v;
} }
} }

View File

@@ -7,7 +7,6 @@ import android.widget.ImageView;
import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.Transformation;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
@@ -19,12 +18,8 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams; import androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.widget.ImageView.ScaleType.CENTER_CROP;
import static android.widget.ImageView.ScaleType.FIT_CENTER;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
import static org.briarproject.briar.android.attachment.AttachmentItem.State.ERROR;
@NotNullByDefault @NotNullByDefault
class ImageViewHolder extends ViewHolder { class ImageViewHolder extends ViewHolder {
@@ -34,33 +29,25 @@ class ImageViewHolder extends ViewHolder {
protected final ImageView imageView; protected final ImageView imageView;
private final int imageSize; private final int imageSize;
private final MessageId conversationItemId;
ImageViewHolder(View v, int imageSize, MessageId conversationItemId) { ImageViewHolder(View v, int imageSize) {
super(v); super(v);
imageView = v.findViewById(R.id.imageView); imageView = v.findViewById(R.id.imageView);
this.imageSize = imageSize; this.imageSize = imageSize;
this.conversationItemId = conversationItemId;
} }
void bind(AttachmentItem attachment, Radii r, boolean single, void bind(AttachmentItem attachment, Radii r, boolean single,
boolean needsStretch) { boolean needsStretch) {
setImageViewDimensions(attachment, single, needsStretch); if (attachment.hasError()) {
if (attachment.getState() != AVAILABLE) { GlideApp.with(imageView)
GlideApp.with(imageView).clear(imageView); .clear(imageView);
if (attachment.getState() == ERROR) {
imageView.setImageResource(ERROR_RES); imageView.setImageResource(ERROR_RES);
} else { } else {
imageView.setImageResource(R.drawable.ic_image_missing); setImageViewDimensions(attachment, single, needsStretch);
}
imageView.setScaleType(FIT_CENTER);
} else {
loadImage(attachment, r); loadImage(attachment, r);
imageView.setScaleType(CENTER_CROP);
}
if (SDK_INT >= 21) { if (SDK_INT >= 21) {
imageView.setTransitionName( imageView.setTransitionName(attachment.getTransitionName());
attachment.getTransitionName(conversationItemId)); }
} }
} }

View File

@@ -7,18 +7,13 @@ import android.view.View;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; 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.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -27,8 +22,6 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -42,28 +35,22 @@ import androidx.lifecycle.AndroidViewModel;
import static android.media.MediaScannerConnection.scanFile; import static android.media.MediaScannerConnection.scanFile;
import static android.os.Environment.DIRECTORY_PICTURES; import static android.os.Environment.DIRECTORY_PICTURES;
import static android.os.Environment.getExternalStoragePublicDirectory; import static android.os.Environment.getExternalStoragePublicDirectory;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.copyAndClose; import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault @NotNullByDefault
public class ImageViewModel extends AndroidViewModel implements EventListener { public class ImageViewModel extends AndroidViewModel {
private static Logger LOG = getLogger(ImageViewModel.class.getName()); private static Logger LOG = getLogger(ImageViewModel.class.getName());
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final EventBus eventBus;
@DatabaseExecutor @DatabaseExecutor
private final Executor dbExecutor; private final Executor dbExecutor;
@IoExecutor @IoExecutor
private final Executor ioExecutor; private final Executor ioExecutor;
private boolean receivedAttachmentsInitialized = false;
private HashMap<MessageId, MutableLiveEvent<Boolean>> receivedAttachments =
new HashMap<>();
/** /**
* true means there was an error saving the image, false if image was saved. * true means there was an error saving the image, false if image was saved.
*/ */
@@ -75,60 +62,13 @@ public class ImageViewModel extends AndroidViewModel implements EventListener {
@Inject @Inject
ImageViewModel(Application application, ImageViewModel(Application application,
MessagingManager messagingManager, EventBus eventBus, MessagingManager messagingManager,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor) { @IoExecutor Executor ioExecutor) {
super(application); super(application);
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.eventBus = eventBus;
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
@UiThread
@Override
public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
MessageId id = ((AttachmentReceivedEvent) e).getMessageId();
MutableLiveEvent<Boolean> oldEvent;
if (receivedAttachmentsInitialized) {
oldEvent = receivedAttachments.get(id);
if (oldEvent != null) oldEvent.postEvent(true);
} else {
receivedAttachments.put(id, new MutableLiveEvent<>(true));
}
}
}
@UiThread
public void expectAttachments(List<AttachmentItem> attachments) {
for (AttachmentItem item : attachments) {
// no need to track items that are in a final state already
if (item.getState().isFinal()) continue;
// add new live events, if not already added by eventOccurred()
MessageId id = item.getMessageId();
if (!receivedAttachments.containsKey(id)) {
receivedAttachments.put(id, new MutableLiveEvent<>());
}
}
receivedAttachmentsInitialized = true;
}
/**
* Returns a LiveData for attachments in a non-final state.
* Note that you need to call {@link #expectAttachments(List)} first.
*/
@UiThread
LiveEvent<Boolean> getOnAttachmentReceived(MessageId messageId) {
return requireNonNull(receivedAttachments.get(messageId));
} }
void clickImage() { void clickImage() {

View File

@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
@@ -113,7 +114,9 @@ public class ContactExchangeActivity extends KeyAgreementActivity {
return getString(R.string.exchanging_contact_details); return getString(R.string.exchanging_contact_details);
} }
private void showErrorFragment() { protected void showErrorFragment() {
showNextFragment(new ContactExchangeErrorFragment()); String errorMsg = getString(R.string.connection_error_explanation);
BaseFragment f = ContactExchangeErrorFragment.newInstance(errorMsg);
showNextFragment(f);
} }
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.keyagreement; package org.briarproject.briar.android.keyagreement;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -19,10 +18,7 @@ import org.briarproject.briar.android.util.UiUtils;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.GONE;
import static org.briarproject.briar.android.util.UiUtils.onSingleLinkClick; import static org.briarproject.briar.android.util.UiUtils.onSingleLinkClick;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -62,12 +58,13 @@ public class ContactExchangeErrorFragment extends BaseFragment {
View v = inflater.inflate(R.layout.fragment_error_contact_exchange, View v = inflater.inflate(R.layout.fragment_error_contact_exchange,
container, false); container, false);
// set optional error message // set humanized error message
TextView explanation = v.findViewById(R.id.errorMessage); TextView explanation = v.findViewById(R.id.errorMessage);
Bundle args = getArguments(); Bundle args = getArguments();
String errorMessage = args == null ? null : args.getString(ERROR_MSG); if (args == null) {
if (errorMessage == null) explanation.setVisibility(GONE); throw new IllegalArgumentException("Use newInstance()");
else explanation.setText(args.getString(ERROR_MSG)); }
explanation.setText(args.getString(ERROR_MSG));
// make feedback link clickable // make feedback link clickable
TextView sendFeedback = v.findViewById(R.id.sendFeedback); TextView sendFeedback = v.findViewById(R.id.sendFeedback);
@@ -76,11 +73,7 @@ public class ContactExchangeErrorFragment extends BaseFragment {
// buttons // buttons
Button tryAgain = v.findViewById(R.id.tryAgainButton); Button tryAgain = v.findViewById(R.id.tryAgainButton);
tryAgain.setOnClickListener(view -> { tryAgain.setOnClickListener(view -> {
// Recreate the activity so we return to the intro fragment if (getActivity() != null) getActivity().onBackPressed();
FragmentActivity activity = requireActivity();
Intent i = new Intent(activity, ContactExchangeActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
activity.startActivity(i);
}); });
Button cancel = v.findViewById(R.id.cancelButton); Button cancel = v.findViewById(R.id.cancelButton);
cancel.setOnClickListener(view -> finish()); cancel.setOnClickListener(view -> finish());

View File

@@ -26,6 +26,7 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.keyagreement.IntroFragment.IntroScreenSeenListener; import org.briarproject.briar.android.keyagreement.IntroFragment.IntroScreenSeenListener;
import org.briarproject.briar.android.keyagreement.KeyAgreementFragment.KeyAgreementEventListener; import org.briarproject.briar.android.keyagreement.KeyAgreementFragment.KeyAgreementEventListener;
import org.briarproject.briar.android.util.UiUtils;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -45,7 +46,6 @@ import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
@@ -55,7 +55,6 @@ 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.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION;
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -134,8 +133,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
private Permission locationPermission = Permission.UNKNOWN; private Permission locationPermission = Permission.UNKNOWN;
private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN; private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
private BroadcastReceiver bluetoothReceiver = null; private BroadcastReceiver bluetoothReceiver = null;
private Plugin wifiPlugin = null, bluetoothPlugin = null;
private BluetoothAdapter bt = null;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -155,9 +152,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
bluetoothReceiver = new BluetoothStateReceiver(); bluetoothReceiver = new BluetoothStateReceiver();
registerReceiver(bluetoothReceiver, filter); registerReceiver(bluetoothReceiver, filter);
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
bt = BluetoothAdapter.getDefaultAdapter();
} }
@Override @Override
@@ -193,7 +187,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
showQrCodeFragmentIfAllowed(); showQrCodeFragmentIfAllowed();
} }
@SuppressWarnings("StatementWithEmptyBody")
private void showQrCodeFragmentIfAllowed() { private void showQrCodeFragmentIfAllowed() {
if (isResumed && continueClicked && areEssentialPermissionsGranted()) { if (isResumed && continueClicked && areEssentialPermissionsGranted()) {
if (isWifiReady() && isBluetoothReady()) { if (isWifiReady() && isBluetoothReady()) {
@@ -207,8 +200,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
} }
if (bluetoothDecision == BluetoothDecision.UNKNOWN) { if (bluetoothDecision == BluetoothDecision.UNKNOWN) {
requestBluetoothDiscoverable(); requestBluetoothDiscoverable();
} else if (bluetoothDecision == BluetoothDecision.REFUSED) {
// Ask again when the user clicks "continue"
} else if (shouldEnableBluetooth()) { } else if (shouldEnableBluetooth()) {
LOG.info("Enabling Bluetooth plugin"); LOG.info("Enabling Bluetooth plugin");
hasEnabledBluetooth = true; hasEnabledBluetooth = true;
@@ -219,50 +210,55 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
} }
private boolean areEssentialPermissionsGranted() { private boolean areEssentialPermissionsGranted() {
// If the camera permission has been granted, and the location
// permission has been granted or permanently denied, we can continue
return cameraPermission == Permission.GRANTED && return cameraPermission == Permission.GRANTED &&
(SDK_INT < 23 || locationPermission == Permission.GRANTED || (locationPermission == Permission.GRANTED ||
!isBluetoothSupported()); locationPermission == Permission.PERMANENTLY_DENIED);
}
private boolean isBluetoothSupported() {
return bt != null && bluetoothPlugin != null;
} }
private boolean isWifiReady() { private boolean isWifiReady() {
if (wifiPlugin == null) return true; // Continue without wifi Plugin p = pluginManager.getPlugin(LanTcpConstants.ID);
State state = wifiPlugin.getState(); if (p == null) return true; // Continue without wifi
State state = p.getState();
// Wait for plugin to become enabled // Wait for plugin to become enabled
return state == ACTIVE || state == INACTIVE; return state == ACTIVE || state == INACTIVE;
} }
private boolean isBluetoothReady() { private boolean isBluetoothReady() {
if (!isBluetoothSupported()) { if (bluetoothDecision == BluetoothDecision.UNKNOWN ||
bluetoothDecision == BluetoothDecision.WAITING) {
// Wait for decision
return false;
}
if (bluetoothDecision == BluetoothDecision.NO_ADAPTER
|| bluetoothDecision == BluetoothDecision.REFUSED) {
// Continue without Bluetooth // Continue without Bluetooth
return true; return true;
} }
if (bluetoothDecision == BluetoothDecision.UNKNOWN || BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
bluetoothDecision == BluetoothDecision.WAITING || if (bt == null) return true; // Continue without Bluetooth
bluetoothDecision == BluetoothDecision.REFUSED) {
// Wait for user to accept
return false;
}
if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) { if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
// Wait for adapter to become discoverable // Wait for adapter to become discoverable
return false; return false;
} }
Plugin p = pluginManager.getPlugin(BluetoothConstants.ID);
if (p == null) return true; // Continue without Bluetooth
// Wait for plugin to become active // Wait for plugin to become active
return bluetoothPlugin.getState() == ACTIVE; return p.getState() == ACTIVE;
} }
private boolean shouldEnableWifi() { private boolean shouldEnableWifi() {
if (hasEnabledWifi) return false; if (hasEnabledWifi) return false;
if (wifiPlugin == null) return false; Plugin p = pluginManager.getPlugin(LanTcpConstants.ID);
State state = wifiPlugin.getState(); if (p == null) return false;
State state = p.getState();
return state == STARTING_STOPPING || state == DISABLED; return state == STARTING_STOPPING || state == DISABLED;
} }
private void requestBluetoothDiscoverable() { private void requestBluetoothDiscoverable() {
if (!isBluetoothSupported()) { BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
if (bt == null) {
bluetoothDecision = BluetoothDecision.NO_ADAPTER; bluetoothDecision = BluetoothDecision.NO_ADAPTER;
showQrCodeFragmentIfAllowed(); showQrCodeFragmentIfAllowed();
} else { } else {
@@ -281,8 +277,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
private boolean shouldEnableBluetooth() { private boolean shouldEnableBluetooth() {
if (bluetoothDecision != BluetoothDecision.ACCEPTED) return false; if (bluetoothDecision != BluetoothDecision.ACCEPTED) return false;
if (hasEnabledBluetooth) return false; if (hasEnabledBluetooth) return false;
if (!isBluetoothSupported()) return false; Plugin p = pluginManager.getPlugin(BluetoothConstants.ID);
State state = bluetoothPlugin.getState(); if (p == null) return false;
State state = p.getState();
return state == STARTING_STOPPING || state == DISABLED; return state == STARTING_STOPPING || state == DISABLED;
} }
@@ -301,9 +298,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
@Override @Override
public void showNextScreen() { public void showNextScreen() {
continueClicked = true; continueClicked = true;
if (bluetoothDecision == BluetoothDecision.REFUSED) {
bluetoothDecision = BluetoothDecision.UNKNOWN; // Ask again
}
if (checkPermissions()) showQrCodeFragmentIfAllowed(); if (checkPermissions()) showQrCodeFragmentIfAllowed();
} }
@@ -347,17 +341,17 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
private boolean checkPermissions() { private boolean checkPermissions() {
if (areEssentialPermissionsGranted()) return true; if (areEssentialPermissionsGranted()) return true;
// If an essential permission has been permanently denied, ask the // If the camera permission has been permanently denied, ask the
// user to change the setting // user to change the setting
if (cameraPermission == Permission.PERMANENTLY_DENIED) { if (cameraPermission == Permission.PERMANENTLY_DENIED) {
showDenialDialog(R.string.permission_camera_title, Builder builder = new Builder(this, R.style.BriarDialogTheme);
R.string.permission_camera_denied_body); builder.setTitle(R.string.permission_camera_title);
return false; builder.setMessage(R.string.permission_camera_denied_body);
} builder.setPositiveButton(R.string.ok,
if (isBluetoothSupported() && UiUtils.getGoToSettingsListener(this));
locationPermission == Permission.PERMANENTLY_DENIED) { builder.setNegativeButton(R.string.cancel,
showDenialDialog(R.string.permission_location_title, (dialog, which) -> supportFinishAfterTransition());
R.string.permission_location_denied_body); builder.show();
return false; return false;
} }
// Should we show the rationale for one or both permissions? // Should we show the rationale for one or both permissions?
@@ -377,16 +371,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
return false; return false;
} }
private void showDenialDialog(@StringRes int title, @StringRes int body) {
Builder builder = new Builder(this, R.style.BriarDialogTheme);
builder.setTitle(title);
builder.setMessage(body);
builder.setPositiveButton(R.string.ok, getGoToSettingsListener(this));
builder.setNegativeButton(R.string.cancel,
(dialog, which) -> supportFinishAfterTransition());
builder.show();
}
private void showRationale(@StringRes int title, @StringRes int body) { private void showRationale(@StringRes int title, @StringRes int body) {
Builder builder = new Builder(this, R.style.BriarDialogTheme); Builder builder = new Builder(this, R.style.BriarDialogTheme);
builder.setTitle(title); builder.setTitle(title);
@@ -397,13 +381,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
} }
private void requestPermissions() { private void requestPermissions() {
String[] permissions; ActivityCompat.requestPermissions(this,
if (isBluetoothSupported()) { new String[] {CAMERA, ACCESS_FINE_LOCATION},
permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION};
} else {
permissions = new String[] {CAMERA};
}
ActivityCompat.requestPermissions(this, permissions,
REQUEST_PERMISSION_CAMERA_LOCATION); REQUEST_PERMISSION_CAMERA_LOCATION);
} }
@@ -420,16 +399,13 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
} else { } else {
cameraPermission = Permission.PERMANENTLY_DENIED; cameraPermission = Permission.PERMANENTLY_DENIED;
} }
if (isBluetoothSupported()) { if (gotPermission(ACCESS_FINE_LOCATION, permissions, grantResults)) {
if (gotPermission(ACCESS_FINE_LOCATION, permissions,
grantResults)) {
locationPermission = Permission.GRANTED; locationPermission = Permission.GRANTED;
} else if (shouldShowRationale(ACCESS_FINE_LOCATION)) { } else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
locationPermission = Permission.SHOW_RATIONALE; locationPermission = Permission.SHOW_RATIONALE;
} else { } else {
locationPermission = Permission.PERMANENTLY_DENIED; locationPermission = Permission.PERMANENTLY_DENIED;
} }
}
// If a permission dialog has been shown, showing the QR code fragment // If a permission dialog has been shown, showing the QR code fragment
// on this call path would cause a crash due to // on this call path would cause a crash due to
// https://code.google.com/p/android/issues/detail?id=190966. // https://code.google.com/p/android/issues/detail?id=190966.

View File

@@ -79,7 +79,7 @@ public class ImagePreview extends ConstraintLayout {
((ImagePreviewAdapter) imageList.getAdapter()); ((ImagePreviewAdapter) imageList.getAdapter());
int pos = requireNonNull(adapter).loadItemPreview(result); int pos = requireNonNull(adapter).loadItemPreview(result);
if (pos != NO_POSITION) { if (pos != NO_POSITION) {
imageList.scrollToPosition(pos); imageList.smoothScrollToPosition(pos);
} }
} }

View File

@@ -12,22 +12,6 @@ import androidx.lifecycle.Observer;
@NotNullByDefault @NotNullByDefault
public class LiveEvent<T> extends LiveData<LiveEvent.ConsumableEvent<T>> { public class LiveEvent<T> extends LiveData<LiveEvent.ConsumableEvent<T>> {
/**
* Creates a LiveEvent initialized with the given {@code value}.
*
* @param value initial value
*/
public LiveEvent(T value) {
super(new ConsumableEvent<>(value));
}
/**
* Creates a LiveEvent with no value assigned to it.
*/
public LiveEvent() {
super();
}
public void observeEvent(LifecycleOwner owner, public void observeEvent(LifecycleOwner owner,
LiveEventHandler<T> handler) { LiveEventHandler<T> handler) {
LiveEventObserver<T> observer = new LiveEventObserver<>(handler); LiveEventObserver<T> observer = new LiveEventObserver<>(handler);

View File

@@ -5,22 +5,6 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault @NotNullByDefault
public class MutableLiveEvent<T> extends LiveEvent<T> { public class MutableLiveEvent<T> extends LiveEvent<T> {
/**
* Creates a MutableLiveEvent initialized with the given {@code value}.
*
* @param value initial value
*/
public MutableLiveEvent(T value) {
super(value);
}
/**
* Creates a MutableLiveEvent with no value assigned to it.
*/
public MutableLiveEvent() {
super();
}
public void postEvent(T value) { public void postEvent(T value) {
super.postValue(new ConsumableEvent<>(value)); super.postValue(new ConsumableEvent<>(value));
} }

View File

@@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="115dp" android:width="200dp"
android:height="115dp" android:height="200dp"
android:viewportHeight="24.0" android:viewportHeight="24.0"
android:viewportWidth="24.0"> android:viewportWidth="24.0">
<path <path

View File

@@ -1,10 +0,0 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="115dp"
android:height="115dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#808080"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@@ -1,43 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.20807062"
android:scaleY="0.20807062"
android:translateX="19.73077"
android:translateY="19.73077">
<path
android:pathData="M94,144.5V264c0,9.7 7.9,17.7 17.7,17.7h8.3c9.7,0 17.7,-8 17.7,-17.7V144.5Z"
android:fillColor="#87c214"
android:strokeColor="#00000000"/>
<path
android:pathData="M137.7,86.8V64.3c0,-9.7 -8,-17.7 -17.7,-17.7h-8.3C102,46.6 94,54.6 94,64.3v22.5z"
android:fillColor="#87c214"
android:strokeColor="#00000000"/>
<path
android:pathData="M234.7,183.8V64.3c0,-9.7 -7.9,-17.7 -17.7,-17.7h-8.3c-9.7,0 -17.7,8 -17.7,17.7v119.5z"
android:fillColor="#87c214"
android:strokeColor="#00000000"/>
<path
android:pathData="M191,241.5V264c0,9.7 8,17.7 17.7,17.7h8.3c9.7,0 17.7,-8 17.7,-17.7v-22.5z"
android:fillColor="#87c214"
android:strokeColor="#00000000"/>
<path
android:pathData="M87,190.8H64.5c-9.7,0 -17.7,7.9 -17.7,17.7v8.3c0,9.7 7.9,17.7 17.7,17.7H87Z"
android:fillColor="#95d220"
android:strokeColor="#00000000"/>
<path
android:pathData="M264.2,190.8H144.7v43.7h119.5c9.7,0 17.7,-8 17.7,-17.7v-8.3c-0.1,-9.7 -8,-17.7 -17.7,-17.7z"
android:fillColor="#95d220"
android:strokeColor="#00000000"/>
<path
android:pathData="M184,93.8H64.5c-9.7,0 -17.7,7.9 -17.7,17.7v8.3c0,9.7 7.9,17.7 17.7,17.7H184Z"
android:fillColor="#95d220"
android:strokeColor="#00000000"/>
<path
android:pathData="m264.2,93.8h-22.5v43.7h22.5c9.7,0 17.7,-7.9 17.7,-17.7v-8.3c-0.1,-9.7 -8,-17.7 -17.7,-17.7z"
android:fillColor="#95d220"
android:strokeColor="#00000000"/>
</group>
</vector>

View File

@@ -24,7 +24,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include_in_report" app:layout_constraintTop_toBottomOf="@+id/include_in_report"

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -27,7 +27,6 @@
<string name="enter_password">‮كلمة السّر</string> <string name="enter_password">‮كلمة السّر</string>
<string name="try_again">كلمة السرّ خاطئة, الرجاء المحاولة مجدّدا</string> <string name="try_again">كلمة السرّ خاطئة, الرجاء المحاولة مجدّدا</string>
<string name="dialog_title_cannot_check_password">لا يمكن التحقق من كلمة السر</string> <string name="dialog_title_cannot_check_password">لا يمكن التحقق من كلمة السر</string>
<string name="dialog_message_cannot_check_password">Briar لم يتمكن من التحقق من كلمة المرور. الرجاء إعادة تشغيل جهازك من أجل جل المشكلة</string>
<string name="sign_in_button">تسجيل الدخول</string> <string name="sign_in_button">تسجيل الدخول</string>
<string name="forgotten_password">نسيتُ كلمة السر</string> <string name="forgotten_password">نسيتُ كلمة السر</string>
<string name="dialog_title_lost_password">فقدت كلمة السر</string> <string name="dialog_title_lost_password">فقدت كلمة السر</string>
@@ -65,36 +64,12 @@
<string name="lock_button">قفل التطبيق</string> <string name="lock_button">قفل التطبيق</string>
<string name="settings_button">الإعدادات</string> <string name="settings_button">الإعدادات</string>
<string name="sign_out_button">تسجيل الخروج</string> <string name="sign_out_button">تسجيل الخروج</string>
<string name="transports_onboarding_text">إلمس هنا من أجل التحكم بطريقةالربط مع جهات الاتصال.</string>
<!--Transports: Tor--> <!--Transports: Tor-->
<string name="transport_tor">إنترنت</string> <string name="transport_tor">إنترنت</string>
<string name="tor_device_status_online_wifi">جهازك لديه ولوج لشبكة الانترنت عبر ال Wi-Fi </string>
<string name="tor_device_status_online_mobile">جهازك لديه ولوج لشبكة الانترنت عبر بيانات الهاتف</string>
<string name="tor_device_status_offline">جهازك ليس متصل بالانترنت</string>
<string name="tor_plugin_status_enabling">جاري اتصال Briar بالانترنت</string>
<string name="tor_plugin_status_active">Briar متصل بالانترنت</string>
<string name="tor_plugin_status_inactive">Briar لم يتمكن من الاتصال بالانترنت </string>
<string name="tor_plugin_status_disabled">إعدادات Briar لاتسمح بالاتصال بالانترنت</string>
<string name="tor_plugin_status_disabled_mobile_data">إعدادات Briar لاتسمح بالاتصال عن طريق بيانات الهاتف</string>
<string name="tor_plugin_status_disabled_battery">إعدادات Briar لاتسمح بالاتصال بالانترنت عند استخدام بطارية الهاتف</string>
<string name="tor_plugin_status_disabled_country_blocked">لايمكن استخدام Briar في هذا البلد</string>
<!--Transports: Wi-Fi--> <!--Transports: Wi-Fi-->
<string name="transport_lan">واي-فاي</string> <string name="transport_lan">واي-فاي</string>
<string name="transport_lan_long">نفس شبكة الWi-Fi </string>
<string name="lan_device_status_on">جهازك متصل بشبكة الWi-Fi</string>
<string name="lan_device_status_off">جهازك ليس متصل بشبكة الWi-Fi</string>
<string name="lan_plugin_status_enabling">جاري ايصال Briar بشبكة الWi-Fi</string>
<string name="lan_plugin_status_active"> Briar متصل بشبكة الWi-Fi</string>
<string name="lan_plugin_status_inactive">لم يتمكن Briar من الاتصال بشبكة ال Wi-Fi</string>
<string name="lan_plugin_status_disabled">إعدادات Briar لاتسمح بالاتصال بشبكة ال Wi-Fi</string>
<!--Transports: Bluetooth--> <!--Transports: Bluetooth-->
<string name="transport_bt">بلوتوث</string> <string name="transport_bt">بلوتوث</string>
<string name="bt_device_status_on"> البلوتوث مفعّل </string>
<string name="bt_device_status_off">البلوتوث مفعّل</string>
<string name="bt_plugin_status_enabling">جاري اتصال Briar بالبلوتوث</string>
<string name="bt_plugin_status_active">Briar متصل بالبلوتوث</string>
<string name="bt_plugin_status_inactive">لم يتمكن Briar من الاتصال بالانترنت</string>
<string name="bt_plugin_status_disabled">إعدادات Briar لاتسمح بالاتصال بالبلوتوث</string>
<!--Notifications--> <!--Notifications-->
<string name="reminder_notification_title">تم تسجيل الخروج من Briar (براير)</string> <string name="reminder_notification_title">تم تسجيل الخروج من Briar (براير)</string>
<string name="reminder_notification_text">الرجاء اللمس لإعادة الدخول</string> <string name="reminder_notification_text">الرجاء اللمس لإعادة الدخول</string>
@@ -215,6 +190,7 @@
<string name="connecting_to_device">يتم الإتصال بالجهاز\u2026</string> <string name="connecting_to_device">يتم الإتصال بالجهاز\u2026</string>
<string name="authenticating_with_device">يتم التوثيق مع الجهاز\u2026</string> <string name="authenticating_with_device">يتم التوثيق مع الجهاز\u2026</string>
<string name="connection_error_title">لم يمكن الإتصال بجهة إتصالك</string> <string name="connection_error_title">لم يمكن الإتصال بجهة إتصالك</string>
<string name="connection_error_explanation">رجاءًا التأكد أن كليكما متصل بنفس شبكة الواي فاي.</string>
<string name="connection_error_feedback">إذا إستمرت المشكلة، رجاءًا <a href="feedback">أرسل تقرير </a> لمساعدتنا على تحسين التطبيق.</string> <string name="connection_error_feedback">إذا إستمرت المشكلة، رجاءًا <a href="feedback">أرسل تقرير </a> لمساعدتنا على تحسين التطبيق.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">إضافة جهة اتصال عن بعد </string> <string name="add_contact_remotely_title_case">إضافة جهة اتصال عن بعد </string>
@@ -483,8 +459,6 @@
<string name="tor_enable_summary">كل جهات الاتصال تمر عبر شبكة تور من أجل الخصوصية</string> <string name="tor_enable_summary">كل جهات الاتصال تمر عبر شبكة تور من أجل الخصوصية</string>
<string name="tor_network_setting">وسيلة الاتصال لشبكة تور</string> <string name="tor_network_setting">وسيلة الاتصال لشبكة تور</string>
<string name="tor_network_setting_automatic">تلقائيًا حسب الموقع</string> <string name="tor_network_setting_automatic">تلقائيًا حسب الموقع</string>
<string name="tor_network_setting_without_bridges">استخدام شبكة تور من دون جسور</string>
<string name="tor_network_setting_with_bridges">استخدام شبكة تور مع جسور</string>
<string name="tor_network_setting_never">لا يمكن الاتصال بالإنترنت</string> <string name="tor_network_setting_never">لا يمكن الاتصال بالإنترنت</string>
<!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"--> <!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"-->
<string name="tor_network_setting_summary">تلقائيا: %1$s (في %2$s)</string> <string name="tor_network_setting_summary">تلقائيا: %1$s (في %2$s)</string>
@@ -600,7 +574,6 @@
<string name="lock_is_locked">Briar (براير) مقفل</string> <string name="lock_is_locked">Briar (براير) مقفل</string>
<string name="lock_tap_to_unlock">الرجاء اللمس لفك القفل</string> <string name="lock_tap_to_unlock">الرجاء اللمس لفك القفل</string>
<!--Connections Screen--> <!--Connections Screen-->
<string name="transports_help_text">يمكن ل Briar التواصل مع جهات الاتصال عن طريق الانترنت, شكبة ال Wi-Fi أو البلوتوث.n\n\كل وسائل الاتصال عن طريق الانترنت تمر عبر شبكة تور من أجل الخصوصية.n\n\إذا كان من الممكن الوصول إلى شبكة إتصال بعدة طرق فإن Briar سوف يستعملهم جميعاً بالتوازي.</string>
<!--Screenshots--> <!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.--> <!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">آليس</string> <string name="screenshot_alice">آليس</string>

View File

@@ -162,6 +162,7 @@
<string name="connecting_to_device">Cihaza qoşulma /2026</string> <string name="connecting_to_device">Cihaza qoşulma /2026</string>
<string name="authenticating_with_device">Cihazla təsdiqlənir \u2026</string> <string name="authenticating_with_device">Cihazla təsdiqlənir \u2026</string>
<string name="connection_error_title">Kontaktınıza qoşula bilmədi</string> <string name="connection_error_title">Kontaktınıza qoşula bilmədi</string>
<string name="connection_error_explanation">Həmin Wi-Fi şəbəkəsinə qoşulduğunuzu yoxlayın.</string>
<string name="connection_error_feedback"><a href="feedback">Bu problem davam edərsə, tətbiqin təkmilləşdirilməsinə kömək etmək üçün rəy göndərin.</a></string> <string name="connection_error_feedback"><a href="feedback">Bu problem davam edərsə, tətbiqin təkmilləşdirilməsinə kömək etmək üçün rəy göndərin.</a></string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Məsafədə kontakt əlavə etmək </string> <string name="add_contact_remotely_title_case">Məsafədə kontakt əlavə etmək </string>

View File

@@ -134,6 +134,7 @@
<string name="connecting_to_device">Povezujem se sa uređajem\u2026</string> <string name="connecting_to_device">Povezujem se sa uređajem\u2026</string>
<string name="authenticating_with_device">Autentikacija sa uređajem\u2026</string> <string name="authenticating_with_device">Autentikacija sa uređajem\u2026</string>
<string name="connection_error_title">Nije moguće povezivanje sa vašim kontaktom</string> <string name="connection_error_title">Nije moguće povezivanje sa vašim kontaktom</string>
<string name="connection_error_explanation">Provjerite jeste li oboje povezani na ist Wi-Fi mrežu.</string>
<string name="connection_error_feedback">Ako se problem ponavlja, Molimo vas <a href="feedback">pošaljite povratne informacije</a> kako bi pomogli poboljšanju aplikacije.</string> <string name="connection_error_feedback">Ako se problem ponavlja, Molimo vas <a href="feedback">pošaljite povratne informacije</a> kako bi pomogli poboljšanju aplikacije.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Dodaj udaljeni konktakt</string> <string name="add_contact_remotely_title_case">Dodaj udaljeni konktakt</string>

View File

@@ -27,7 +27,7 @@
<string name="enter_password">Contrasenya</string> <string name="enter_password">Contrasenya</string>
<string name="try_again">La contrasenya és incorrecta, torneu a escriure-la</string> <string name="try_again">La contrasenya és incorrecta, torneu a escriure-la</string>
<string name="dialog_title_cannot_check_password">No es pot verificar la contrasenya</string> <string name="dialog_title_cannot_check_password">No es pot verificar la contrasenya</string>
<string name="dialog_message_cannot_check_password">Briar no pot verificar la contrasenya. Proveu de reiniciar el vostre aparell per a solucionar aquest problema.</string> <string name="dialog_message_cannot_check_password">El Briar no pot verificar la contrasenya. Proveu de reiniciar el vostre aparell per a solucionar aquest problema.</string>
<string name="sign_in_button">Inicia la sessió</string> <string name="sign_in_button">Inicia la sessió</string>
<string name="forgotten_password">No recordo la contrasenya</string> <string name="forgotten_password">No recordo la contrasenya</string>
<string name="dialog_title_lost_password">Contrasenya perduda</string> <string name="dialog_title_lost_password">Contrasenya perduda</string>
@@ -44,9 +44,9 @@
<item quantity="other">Aquesta és una versió de prova de Briar. El vostre compte caducarà en %d dies i no es podrà renovar.</item> <item quantity="other">Aquesta és una versió de prova de Briar. El vostre compte caducarà en %d dies i no es podrà renovar.</item>
</plurals> </plurals>
<string name="expiry_date_reached">Aquesta versió de Briar ha caducat.\nGràcies per haver-lo provat!</string> <string name="expiry_date_reached">Aquesta versió de Briar ha caducat.\nGràcies per haver-lo provat!</string>
<string name="download_briar">Per continuar utilitzant Briar, baixeu la darrera versió.</string> <string name="download_briar">Per continuar utilitzant el Briar, baixeu la darrera versió.</string>
<string name="create_new_account">Haureu de crear un compte nou, però podeu utilitzar el mateix sobrenom.</string> <string name="create_new_account">Haureu de crear un compte nou, però podeu utilitzar el mateix sobrenom.</string>
<string name="download_briar_button">Descarrega la darrera versió</string> <string name="download_briar_button">Baixa l\'última versió</string>
<string name="startup_open_database">S\'està desxifrant la base de dades...</string> <string name="startup_open_database">S\'està desxifrant la base de dades...</string>
<string name="startup_migrate_database">S\'està actualitzant la base de dades...</string> <string name="startup_migrate_database">S\'està actualitzant la base de dades...</string>
<string name="startup_compact_database">S\'està compactant la base de dades...</string> <string name="startup_compact_database">S\'està compactant la base de dades...</string>
@@ -61,36 +61,12 @@
<string name="lock_button">Bloqueja l\'aplicació</string> <string name="lock_button">Bloqueja l\'aplicació</string>
<string name="settings_button">Configuració</string> <string name="settings_button">Configuració</string>
<string name="sign_out_button">Tanca la sessió </string> <string name="sign_out_button">Tanca la sessió </string>
<string name="transports_onboarding_text">Toqueu per decidir com es connecta Briar als vostres contactes.</string>
<!--Transports: Tor--> <!--Transports: Tor-->
<string name="transport_tor">Internet</string> <string name="transport_tor">Internet</string>
<string name="tor_device_status_online_wifi">El mòbil té accés a Internet via WiFi</string>
<string name="tor_device_status_online_mobile">El mòbil té accés a Internet via dades</string>
<string name="tor_device_status_offline">El mòbil no té accés a Internet</string>
<string name="tor_plugin_status_enabling">Briar s\'està connectant a Internet</string>
<string name="tor_plugin_status_active">Briar està connectat a Internet</string>
<string name="tor_plugin_status_inactive">Briar no pot connectar-se a Internet</string>
<string name="tor_plugin_status_disabled">Briar està configurat per a no emprar Internet</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar està configurat per a no emprar dades mòbils</string>
<string name="tor_plugin_status_disabled_battery">Briar està configurat per a no usar Internet mentre funciona amb bateria</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar està configurat per a no emprar Internet en aquest país</string>
<!--Transports: Wi-Fi--> <!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string> <string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">La mateixa xarxa WiFi</string>
<string name="lan_device_status_on">El mòbil està connectat a WiFi</string>
<string name="lan_device_status_off">El mòbil no està connectat a WiFi</string>
<string name="lan_plugin_status_enabling">Briar s\'està connectant a la xarxa WiFi</string>
<string name="lan_plugin_status_active">Briar està connectat a la xarxa WiFi</string>
<string name="lan_plugin_status_inactive">Briar no pot connectar-se a la xarxa WiFi</string>
<string name="lan_plugin_status_disabled">Briar està configurat per a no emprar la xarxa WiFi</string>
<!--Transports: Bluetooth--> <!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string> <string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">El Bluetooth del mòbil està apagat</string>
<string name="bt_device_status_off">El Bluetooth del mòbil està engegat</string>
<string name="bt_plugin_status_enabling">Briar s\'està connectant a Bluetooth</string>
<string name="bt_plugin_status_active">Briar està connectat a Bluetooth</string>
<string name="bt_plugin_status_inactive">Briar no pot connectar-se a Bluetooth</string>
<string name="bt_plugin_status_disabled">Briar està configurat per a no emprar Bluetooth</string>
<!--Notifications--> <!--Notifications-->
<string name="reminder_notification_title">Heu sortit de Briar</string> <string name="reminder_notification_title">Heu sortit de Briar</string>
<string name="reminder_notification_text">Toqueu per a reiniciar la sessió.</string> <string name="reminder_notification_text">Toqueu per a reiniciar la sessió.</string>
@@ -136,7 +112,7 @@
<string name="fix">Corregeix</string> <string name="fix">Corregeix</string>
<string name="help">Ajuda</string> <string name="help">Ajuda</string>
<string name="sorry">Ens sap greu</string> <string name="sorry">Ens sap greu</string>
<string name="error_start_activity">No està disponible en el vostre sistema</string> <string name="error_start_activity">No disponible al vostre sistema</string>
<string name="status_heading">Estat</string> <string name="status_heading">Estat</string>
<!--Contacts and Private Conversations--> <!--Contacts and Private Conversations-->
<string name="no_contacts">No hi ha cap contacte per mostrar</string> <string name="no_contacts">No hi ha cap contacte per mostrar</string>
@@ -152,16 +128,16 @@
<string name="set_contact_alias">Canvia el nom del contacte</string> <string name="set_contact_alias">Canvia el nom del contacte</string>
<string name="set_contact_alias_hint">Nom del contacte</string> <string name="set_contact_alias_hint">Nom del contacte</string>
<string name="set_alias_button">Canvia</string> <string name="set_alias_button">Canvia</string>
<string name="delete_all_messages">Esborra tots els missatges</string> <string name="delete_all_messages">Suprimeix tots els missatges</string>
<string name="dialog_title_delete_all_messages">Confirmeu l\'esborrat dels missatges</string> <string name="dialog_title_delete_all_messages">Confirmeu la supressió dels missatges</string>
<string name="dialog_message_delete_all_messages">Segur que voleu esborrar tots els missatges?</string> <string name="dialog_message_delete_all_messages">Esteu segur que voleu suprimir tots els missatges?</string>
<string name="dialog_title_not_all_messages_deleted">No s\'han pogut esborrar tots els missatges</string> <string name="dialog_title_not_all_messages_deleted">No s\'ha pogut suprimir tots els missatges</string>
<string name="dialog_message_not_deleted_ongoing_both">Els missatges relacionats amb presentacions i invitacions pendents no es poden suprimir fins que finalitzen.</string> <string name="dialog_message_not_deleted_ongoing_both">Els missatges relacionats amb introduccions i invitacions pendents no es poden suprimir fins que es finalitzin.</string>
<string name="dialog_message_not_deleted_ongoing_introductions">Els missatges relacionats amb presentacions pendents no es poden suprimir fins que finalitzen.</string> <string name="dialog_message_not_deleted_ongoing_introductions">Els missatges relacionats amb introduccions pendents no es poden suprimir fins que es finalitzin.</string>
<string name="dialog_message_not_deleted_ongoing_invitations">Els missatges relacionats amb invitacions pendents no es poden suprimir fins que finalitzen.</string> <string name="dialog_message_not_deleted_ongoing_invitations">Els missatges relacionats amb invitacions pendents no es poden suprimir fins que es finalitzin.</string>
<string name="dialog_message_not_deleted_partly_downloaded">Els missatges baixats parcialment no es poden suprimir fins que s\'acabin de baixar.</string> <string name="dialog_message_not_deleted_partly_downloaded">Els missatges baixats parcialment no es poden suprimir fins que s\'acabin de baixar.</string>
<string name="dialog_message_not_deleted_not_all_selected_both">Per a suprimir una invitació o una presentació, cal que seleccioneu la sol·licitud i la resposta.</string> <string name="dialog_message_not_deleted_not_all_selected_both">Per a suprimir una invitació o una introducció, cal que seleccioneu la sol·licitdu i la resposta.</string>
<string name="dialog_message_not_deleted_not_all_selected_introductions">Per a suprimir una presentació, cal que seleccioneu la sol·licitud i la resposta.</string> <string name="dialog_message_not_deleted_not_all_selected_introductions">Per a suprimir una introducció, cal que seleccioneu la sol·licitud i la resposta.</string>
<string name="dialog_message_not_deleted_not_all_selected_invitations">Per a suprimir una invitació, cal que seleccioneu la sol·licitud i la resposta.</string> <string name="dialog_message_not_deleted_not_all_selected_invitations">Per a suprimir una invitació, cal que seleccioneu la sol·licitud i la resposta.</string>
<string name="delete_contact">Suprimeix aquest contacte</string> <string name="delete_contact">Suprimeix aquest contacte</string>
<string name="dialog_title_delete_contact">Confirmeu la supressió del contacte</string> <string name="dialog_title_delete_contact">Confirmeu la supressió del contacte</string>
@@ -196,6 +172,7 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="connecting_to_device">Connectant-se al dispositiu\u2026</string> <string name="connecting_to_device">Connectant-se al dispositiu\u2026</string>
<string name="authenticating_with_device">Autenticant-se amb el dispositiu\u2026</string> <string name="authenticating_with_device">Autenticant-se amb el dispositiu\u2026</string>
<string name="connection_error_title">No ha pogut connectar-se al vostre contacte</string> <string name="connection_error_title">No ha pogut connectar-se al vostre contacte</string>
<string name="connection_error_explanation">Comproveu que esteu connectats a la mateixa xarxa Wi-Fi.</string>
<string name="connection_error_feedback">Si aquest problema persisteix, <a href="feedback">envieu-nos un comentari</a> per ajudar-nos a millorar l\'aplicació.</string> <string name="connection_error_feedback">Si aquest problema persisteix, <a href="feedback">envieu-nos un comentari</a> per ajudar-nos a millorar l\'aplicació.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Afegeix un contacte llunyà</string> <string name="add_contact_remotely_title_case">Afegeix un contacte llunyà</string>
@@ -436,20 +413,10 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="pref_theme_auto">Automàtic (segons l\'hora)</string> <string name="pref_theme_auto">Automàtic (segons l\'hora)</string>
<string name="pref_theme_system">Valor per defecte del sistema</string> <string name="pref_theme_system">Valor per defecte del sistema</string>
<!--Settings Connections--> <!--Settings Connections-->
<string name="network_settings_title">Connexions</string>
<string name="bluetooth_setting">Connectat als contactes via Bluetooth</string>
<string name="wifi_setting">Connectat als contactes en la mateixa xarxa WiFi</string>
<string name="tor_enable_title">Connectat als contactes via Internet</string>
<string name="tor_enable_summary">Totes les connexions van via la xarxa Tor per augmentar privacitat</string>
<string name="tor_network_setting">Mètode de connexió per a la xarxa Tor</string>
<string name="tor_network_setting_automatic">Automàtic, basat en la posició</string> <string name="tor_network_setting_automatic">Automàtic, basat en la posició</string>
<string name="tor_network_setting_without_bridges">Usa la xarxa Tor sense ponts</string>
<string name="tor_network_setting_with_bridges">Usa la xarxa Tor amb ponts</string>
<string name="tor_network_setting_never">No connectis a Internet</string>
<!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"--> <!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"-->
<string name="tor_network_setting_summary">Automàtic: %1$s (a %2$s)</string> <string name="tor_network_setting_summary">Automàtic: %1$s (a %2$s)</string>
<string name="tor_mobile_data_title">Usa dades mòbils</string> <string name="tor_mobile_data_title">Usa dades mòbils</string>
<string name="tor_only_when_charging_title">Només connecta\'t a Internet mentre s\'està carregant </string>
<string name="tor_only_when_charging_summary">Desactiva la connexió a Internet quan el dispositiu estigui funcionant amb la bateria</string> <string name="tor_only_when_charging_summary">Desactiva la connexió a Internet quan el dispositiu estigui funcionant amb la bateria</string>
<!--Settings Security and Panic--> <!--Settings Security and Panic-->
<string name="security_settings_title">Seguretat</string> <string name="security_settings_title">Seguretat</string>
@@ -560,7 +527,6 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="lock_is_locked">Briar està bloquejat</string> <string name="lock_is_locked">Briar està bloquejat</string>
<string name="lock_tap_to_unlock">Toqueu per desbloquejar-lo</string> <string name="lock_tap_to_unlock">Toqueu per desbloquejar-lo</string>
<!--Connections Screen--> <!--Connections Screen-->
<string name="transports_help_text">Briar pot contactar els vostres contactes via Internet, WiFi o Bluetooth.\n\nTotes les connexions d\'Internet van via la xarxa Tor per privacitat.\n\nSi es pot arribar a un contacte per diversos mètodes, Briar els usa tots simultàniament.</string>
<!--Screenshots--> <!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.--> <!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alba</string> <string name="screenshot_alice">Alba</string>

View File

@@ -195,6 +195,7 @@
<string name="connecting_to_device">Verbinde mit Gerät\u2026</string> <string name="connecting_to_device">Verbinde mit Gerät\u2026</string>
<string name="authenticating_with_device">Authentifiziere Gerät\u2026</string> <string name="authenticating_with_device">Authentifiziere Gerät\u2026</string>
<string name="connection_error_title">Keine Verbindung zum Kontakt</string> <string name="connection_error_title">Keine Verbindung zum Kontakt</string>
<string name="connection_error_explanation">Überprüfe, ob ihr beide mit demselben WLAN-Netzwerk verbunden seid.</string>
<string name="connection_error_feedback">Wenn das Problem weiterbesteht, hilf uns die App zu verbessern und <a href="feedback">schicke Feedback</a>.</string> <string name="connection_error_feedback">Wenn das Problem weiterbesteht, hilf uns die App zu verbessern und <a href="feedback">schicke Feedback</a>.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Kontakt aus der Ferne hinzufügen</string> <string name="add_contact_remotely_title_case">Kontakt aus der Ferne hinzufügen</string>
@@ -210,7 +211,7 @@
<string name="add_contact_choose_nickname">Wähle einen Spitznamen</string> <string name="add_contact_choose_nickname">Wähle einen Spitznamen</string>
<string name="add_contact_choose_a_nickname">Gib einen Spitznamen ein</string> <string name="add_contact_choose_a_nickname">Gib einen Spitznamen ein</string>
<string name="nickname_intro">Gib deinem Kontakt einen Spitznamen. Nur du kannst ihn sehen.</string> <string name="nickname_intro">Gib deinem Kontakt einen Spitznamen. Nur du kannst ihn sehen.</string>
<string name="your_link">Gib diesen Link dem Kontakt, den du hinzufügen möchtest</string> <string name="your_link">Gebe diesen Link dem Kontakt, den du hinzufügen möchtest:</string>
<string name="link_clip_label">Briar Link</string> <string name="link_clip_label">Briar Link</string>
<string name="link_copied_toast">Link kopiert</string> <string name="link_copied_toast">Link kopiert</string>
<string name="adding_contact_error">Es gab einen Fehler beim Hinzufügen des Kontaktes.</string> <string name="adding_contact_error">Es gab einen Fehler beim Hinzufügen des Kontaktes.</string>
@@ -549,7 +550,6 @@
<string name="permission_camera_location_title">Kamera und Standort</string> <string name="permission_camera_location_title">Kamera und Standort</string>
<string name="permission_camera_location_request_body">Um den QR-Code zu scannen, braucht Briar Zugriff auf die Kamera.\n\nUm Bluetooth-Geräte zu finden, braucht Briar Zugriff auf deinen Standort.\n\nBriar speichert weder deinen Standort noch gibt es ihn an andere weiter.</string> <string name="permission_camera_location_request_body">Um den QR-Code zu scannen, braucht Briar Zugriff auf die Kamera.\n\nUm Bluetooth-Geräte zu finden, braucht Briar Zugriff auf deinen Standort.\n\nBriar speichert weder deinen Standort noch gibt es ihn an andere weiter.</string>
<string name="permission_camera_denied_body">Du hast den Zugriff auf die Kamera verweigert, aber das Hinzufügen von Kontakten erfordert die Verwendung der Kamera.\n\nBitte gewähre den Zugriff.</string> <string name="permission_camera_denied_body">Du hast den Zugriff auf die Kamera verweigert, aber das Hinzufügen von Kontakten erfordert die Verwendung der Kamera.\n\nBitte gewähre den Zugriff.</string>
<string name="permission_location_denied_body">Du hast den Zugriff auf deinen Standort verweigert, aber Briar benötigt diese Berechtigung, um Bluetooth-Geräte zu erkennen.\n\nBitte gewähre den Zugriff.</string>
<string name="qr_code">QR-Code</string> <string name="qr_code">QR-Code</string>
<string name="show_qr_code_fullscreen">QR-Code im Vollbildmodus anzeigen</string> <string name="show_qr_code_fullscreen">QR-Code im Vollbildmodus anzeigen</string>
<!--App Locking--> <!--App Locking-->

View File

@@ -157,12 +157,12 @@
<string name="dialog_message_delete_all_messages">¿Estás seguro de que deseas eliminar todos los mensajes?</string> <string name="dialog_message_delete_all_messages">¿Estás seguro de que deseas eliminar todos los mensajes?</string>
<string name="dialog_title_not_all_messages_deleted">No se pudieron eliminar todos los mensajes.</string> <string name="dialog_title_not_all_messages_deleted">No se pudieron eliminar todos los mensajes.</string>
<string name="dialog_message_not_deleted_ongoing_both">Los mensajes relacionados con presentaciones o invitaciones en curso no se pueden eliminar hasta que finalicen.</string> <string name="dialog_message_not_deleted_ongoing_both">Los mensajes relacionados con presentaciones o invitaciones en curso no se pueden eliminar hasta que finalicen.</string>
<string name="dialog_message_not_deleted_ongoing_introductions">Los mensajes relacionados con presentaciones no se pueden eliminar hasta que finalicen.</string> <string name="dialog_message_not_deleted_ongoing_introductions">Los mensajes relacionados con presentaciones o invitaciones en curso no se pueden eliminar hasta que finalicen.</string>
<string name="dialog_message_not_deleted_ongoing_invitations">Los mensajes relacionados a invitaciones en curso no pueden ser borrados hasta su conclusión.</string> <string name="dialog_message_not_deleted_ongoing_invitations">Los mensajes relacionados a invitaciones en curso no pueden ser borrados hasta su conclusión.</string>
<string name="dialog_message_not_deleted_partly_downloaded">Los mensajes parcialmente descargados no se pueden eliminar hasta que haya finalizado la descarga.</string> <string name="dialog_message_not_deleted_partly_downloaded">Los mensajes parcialmente descargados no se pueden eliminar hasta que haya finalizado la descarga.</string>
<string name="dialog_message_not_deleted_not_all_selected_both">Para borrar una invitación o presentación, debes seleccionar la petición y la respuesta.</string> <string name="dialog_message_not_deleted_not_all_selected_both">Para borrar una invitación o presentación, debes seleccionar la petición y la respuesta.</string>
<string name="dialog_message_not_deleted_not_all_selected_introductions">Para eliminar una introducción, debes seleccionar la solicitud y la respuesta.</string> <string name="dialog_message_not_deleted_not_all_selected_introductions">Para eliminar una introducción, debe seleccionar la solicitud y la respuesta.</string>
<string name="dialog_message_not_deleted_not_all_selected_invitations">Para eliminar una invitación, debes seleccionar la solicitud y la respuesta.</string> <string name="dialog_message_not_deleted_not_all_selected_invitations">Para eliminar una invitación, debe seleccionar la solicitud y la respuesta.</string>
<string name="delete_contact">Eliminar contacto</string> <string name="delete_contact">Eliminar contacto</string>
<string name="dialog_title_delete_contact">Confirmar eliminación de contacto</string> <string name="dialog_title_delete_contact">Confirmar eliminación de contacto</string>
<string name="dialog_message_delete_contact">¿Seguro que quieres eliminar este contacto y todos los mensajes intercambiados entre vosotros?</string> <string name="dialog_message_delete_contact">¿Seguro que quieres eliminar este contacto y todos los mensajes intercambiados entre vosotros?</string>
@@ -195,6 +195,7 @@
<string name="connecting_to_device">Conectando al dispositivo\u2026</string> <string name="connecting_to_device">Conectando al dispositivo\u2026</string>
<string name="authenticating_with_device">Autentificándose con el dispositivo\u2026</string> <string name="authenticating_with_device">Autentificándose con el dispositivo\u2026</string>
<string name="connection_error_title">No se pudo conectar a tu contacto</string> <string name="connection_error_title">No se pudo conectar a tu contacto</string>
<string name="connection_error_explanation">Por favor comprobar que ambos estén conectados a la misma red Wi-Fi.</string>
<string name="connection_error_feedback">Si este problema persiste, por favor <a href="feedback">envía tus comentarios</a> para ayudarnos a mejorar la aplicación.</string> <string name="connection_error_feedback">Si este problema persiste, por favor <a href="feedback">envía tus comentarios</a> para ayudarnos a mejorar la aplicación.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Añadir un Contacto a Distancia</string> <string name="add_contact_remotely_title_case">Añadir un Contacto a Distancia</string>
@@ -549,7 +550,6 @@
<string name="permission_camera_location_title">Cámara y ubicación</string> <string name="permission_camera_location_title">Cámara y ubicación</string>
<string name="permission_camera_location_request_body">Para escanear el código QR, Briar necesita acceso a la cámara.\n\nPara descubrir dispositivos Bluetooth, Briar necesita permiso para acceder tu ubicación.\n\nBriar no la almacena o la comparte con nadie.</string> <string name="permission_camera_location_request_body">Para escanear el código QR, Briar necesita acceso a la cámara.\n\nPara descubrir dispositivos Bluetooth, Briar necesita permiso para acceder tu ubicación.\n\nBriar no la almacena o la comparte con nadie.</string>
<string name="permission_camera_denied_body">Has denegado el acceso a la cámara, pero para añadir contactos se requiere el uso de la cámara.\n\nPor favor considera la posibilidad de conceder el acceso.</string> <string name="permission_camera_denied_body">Has denegado el acceso a la cámara, pero para añadir contactos se requiere el uso de la cámara.\n\nPor favor considera la posibilidad de conceder el acceso.</string>
<string name="permission_location_denied_body">Has denegado el acceso a tu ubicación, pero Briar necesita este permiso para descubrir dispositivos Bluetooth.\n\nPor favor considera la posibilidad de conceder el acceso.</string>
<string name="qr_code">Código QR</string> <string name="qr_code">Código QR</string>
<string name="show_qr_code_fullscreen">Mostrar código QR a pantalla completa</string> <string name="show_qr_code_fullscreen">Mostrar código QR a pantalla completa</string>
<!--App Locking--> <!--App Locking-->

View File

@@ -171,6 +171,7 @@
<string name="connecting_to_device">Gailura konektatzen\u2026</string> <string name="connecting_to_device">Gailura konektatzen\u2026</string>
<string name="authenticating_with_device">Gailuarekin autentifikatzen\u2026</string> <string name="authenticating_with_device">Gailuarekin autentifikatzen\u2026</string>
<string name="connection_error_title">Ezin izan da zure kontaktuarekin konektatu</string> <string name="connection_error_title">Ezin izan da zure kontaktuarekin konektatu</string>
<string name="connection_error_explanation">Egiaztatu biak Wi-Fi sare berera konektatuta zaudetela.</string>
<string name="connection_error_feedback">Arazoa mantentzen bada, mesedez <a href="feedback">bidali iruzkin bat</a> aplikazioa hobetzen laguntzeko.</string> <string name="connection_error_feedback">Arazoa mantentzen bada, mesedez <a href="feedback">bidali iruzkin bat</a> aplikazioa hobetzen laguntzeko.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Gehitu urruneko kontaktua</string> <string name="add_contact_remotely_title_case">Gehitu urruneko kontaktua</string>

View File

@@ -69,21 +69,10 @@
<string name="sign_out_button">خروج</string> <string name="sign_out_button">خروج</string>
<!--Transports: Tor--> <!--Transports: Tor-->
<string name="transport_tor">اینترنت</string> <string name="transport_tor">اینترنت</string>
<string name="tor_plugin_status_enabling">Briar در حال اتصال به اینترنت می باشد</string>
<string name="tor_plugin_status_active">Briar به اینترنت متصل شد</string>
<string name="tor_plugin_status_inactive">Briar نمی تواند به اینترنت متصل شود</string>
<!--Transports: Wi-Fi--> <!--Transports: Wi-Fi-->
<string name="transport_lan">وای فای</string> <string name="transport_lan">وای فای</string>
<string name="transport_lan_long">همان شبکه وای-فای</string>
<string name="lan_device_status_on">موبایل شما به وای-فای وصل می باشد</string>
<string name="lan_device_status_off">موبایل شما به وای-فای وصل نیست</string>
<string name="lan_plugin_status_enabling">Briar در حال اتصال به شبکه وای-فای می باشد</string>
<!--Transports: Bluetooth--> <!--Transports: Bluetooth-->
<string name="transport_bt">بلوتوث</string> <string name="transport_bt">بلوتوث</string>
<string name="bt_device_status_on">بلوتوث موبایل شما روشن می باشد</string>
<string name="bt_device_status_off">بلوتوث موبایل شما خاموش می باشد</string>
<string name="bt_plugin_status_inactive">Briar نمی تواند به بلوتوث وصل شود</string>
<string name="bt_plugin_status_disabled">Briar طوری پیکربندی شده که از بلوتوث استفاده نکند</string>
<!--Notifications--> <!--Notifications-->
<string name="reminder_notification_title">از Briar (برایر) خارج شد</string> <string name="reminder_notification_title">از Briar (برایر) خارج شد</string>
<string name="reminder_notification_text">برای وارد شدن دوباره ضربه بزنید.</string> <string name="reminder_notification_text">برای وارد شدن دوباره ضربه بزنید.</string>
@@ -196,6 +185,7 @@
<string name="connecting_to_device">اتصال به دستگاهu2026\</string> <string name="connecting_to_device">اتصال به دستگاهu2026\</string>
<string name="authenticating_with_device">تصدیق سازی با دستگاه u2026\</string> <string name="authenticating_with_device">تصدیق سازی با دستگاه u2026\</string>
<string name="connection_error_title">اتصال به مخاطب شما برقرار نشد</string> <string name="connection_error_title">اتصال به مخاطب شما برقرار نشد</string>
<string name="connection_error_explanation">لطفا مطمئن شوید که هر دو شما به شبکه وای فای یکسان متصل هستید.</string>
<string name="connection_error_feedback">اگر این مشکل ادامه پیدا کرد، لطفا <a href="feedback">بازخورد ارسال کنید</a> تا به ما در بهبود برنامه کمک کنید.</string> <string name="connection_error_feedback">اگر این مشکل ادامه پیدا کرد، لطفا <a href="feedback">بازخورد ارسال کنید</a> تا به ما در بهبود برنامه کمک کنید.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">افزودن مخاطب از دور</string> <string name="add_contact_remotely_title_case">افزودن مخاطب از دور</string>

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