Compare commits

...

49 Commits

Author SHA1 Message Date
akwizgran
f9e4ffa841 Update test class. 2020-11-02 14:42:46 +00:00
akwizgran
027329ddd6 Don't try to make multiple Bluetooth connections on Motorola devices. 2020-11-02 14:18:05 +00:00
akwizgran
01a146ba71 Merge branch '1647-illegal-state' into 'master'
Fix IllegalStateException when creating image attachments

Closes #1647

See merge request briar/briar!1187
2020-10-30 16:17:20 +00:00
akwizgran
a30e5b672e Merge branch '1592-image-placeholders' into 'master'
Show Attachment Placeholders

Closes #1592

See merge request briar/briar!1186
2020-10-30 15:54:25 +00:00
Torsten Grote
edb584dc3b Merge branch 'add-contacts-via-bluetooth' into 'master'
Add contacts via Bluetooth if possible

See merge request briar/briar!1292
2020-10-29 16:54:05 +00:00
akwizgran
12a8907c8b Ignore missing location permission on API < 23 where it's not needed. 2020-10-29 14:34:10 +00:00
akwizgran
e0f381a973 Try all transports in order of preference. 2020-10-29 11:48:10 +00:00
Torsten Grote
61d3d133e8 Merge branch '1147-only-alice-performs-discovery' into 'master'
Only Alice should perform Bluetooth discovery

See merge request briar/briar!1291
2020-10-28 11:11:34 +00:00
akwizgran
0caa522f07 Remove error message, return to intro fragment when retrying. 2020-10-27 17:37:22 +00:00
akwizgran
948212103c Require Bluetooth permissions if device supports Bluetooth. 2020-10-27 16:24:34 +00:00
akwizgran
ce1a57c2b4 Prefer Bluetooth for adding contacts. 2020-10-27 16:24:33 +00:00
akwizgran
922a52bf83 Only Alice should perform Bluetooth discovery. 2020-10-27 16:21:30 +00:00
akwizgran
8cbb38ee68 Bump version numbers for 1.2.11 release. 2020-10-14 13:15:29 +01:00
akwizgran
1c4cf7d771 Update translations. 2020-10-14 13:14:05 +01:00
akwizgran
090a1bd84e Merge branch '1781-change-alias' into 'master'
Add method to change contact alias to REST API

Closes #1781

See merge request briar/briar!1286
2020-10-14 11:47:10 +00:00
Nico Alt
44f6f5d416 Add method to change contact alias to REST API
Needed for https://code.briarproject.org/briar/briar-gtk/-/issues/14 and
https://code.briarproject.org/briar/python-briar-wrapper/-/issues/6.

Fixes #1781
2020-10-13 23:33:26 +02:00
Torsten Grote
b88f012880 Merge branch 'make-crash-report-text-selectable' into 'master'
Make the text of crash reports selectable

See merge request briar/briar!1290
2020-10-13 16:23:49 +00:00
akwizgran
93f434e54b Merge branch '1782-delete-all-messages' into 'master'
Add method to delete all private messages to REST API

Closes #1782

See merge request briar/briar!1287
2020-10-13 15:46:01 +00:00
akwizgran
92f4a3a404 Make the text of crash reports selectable.
This makes it possible for users to send device data by other means if
they can't connect to Tor to send a crash report.
2020-10-13 16:44:43 +01:00
Nico Alt
c017a813b0 Add output of DeletionResult to deleteAllMessages call 2020-10-08 15:03:49 +02:00
Nico Alt
6c6dbfd357 Add method to delete all private messages to REST API
Needed for https://code.briarproject.org/briar/briar-gtk/-/issues/11.

Fixes #1782
2020-10-08 14:03:16 +02:00
akwizgran
1f246637e2 Merge branch 'kotlin-no-star-imports' into 'master'
Change Kotlin coding style to not do star imports

See merge request briar/briar!1289
2020-10-05 13:54:51 +00:00
Torsten Grote
1ac17cf859 [headless] Change coding style to not do star imports 2020-10-05 09:54:35 -03:00
akwizgran
0a3ff41feb Merge branch '1780-mark-as-read' into 'master'
Add method to mark message as read to REST API

Closes #1780

See merge request briar/briar!1285
2020-10-05 11:38:33 +00:00
Nico Alt
9738dd2838 Add method to mark message as read to REST API
When exposing unread messages counters in
https://code.briarproject.org/briar/briar/-/merge_requests/1283, I
noticed that they were never set to 0.

Fixes #1780
2020-10-03 23:23:54 +02:00
akwizgran
be0e21d39b Merge branch '1507-extract-tor-binaries-to-lib-dir' into 'master'
Raise targetSdkVersion to 29, package Tor binaries as libraries

Closes #1507 and #1185

See merge request briar/briar!1279
2020-09-29 13:19:42 +00:00
Torsten Grote
6a2c2bed0f Merge branch '1785-bluetooth-adapter-npe' into 'master'
Check whether Bluetooth adapter exists before trying to get address

Closes #1785

See merge request briar/briar!1288
2020-09-29 12:51:22 +00:00
akwizgran
de9c6d4447 Extract version constants into top-level build file. 2020-09-29 13:50:17 +01:00
akwizgran
37a2d9f990 Extract binaries even if older versions already exist. 2020-09-29 13:48:45 +01:00
akwizgran
0e1fb406b5 Extract library filenames into constants. 2020-09-29 13:48:45 +01:00
akwizgran
b72e8fa490 Package Tor binaries as libraries so we're allowed to execute them. 2020-09-29 13:48:45 +01:00
akwizgran
f3157e5276 Raise target SDK version to 29. 2020-09-29 13:48:43 +01:00
akwizgran
e2124ff3c9 Merge branch '1779-headless-messages-sent-acked' into 'master'
Expose message delivery state changes to websockets API

Closes #1779

See merge request briar/briar!1284
2020-09-29 12:46:41 +00:00
akwizgran
66cc9d25e7 Merge branch '1746-headless-unread-counter' into 'master'
Expose unread messages count in API's contacts list

Closes #1746

See merge request briar/briar!1283
2020-09-29 12:45:30 +00:00
akwizgran
e9cdec95e0 Check whether Bluetooth adapter exists before trying to get address. 2020-09-29 13:39:46 +01:00
Nico Alt
63d3a78dda Expose message delivery state changes to websockets API
We already indicate whether a message was sent/acked, but we don't
inform about updates.

Needed for briar-gtk#69.

Fixes #1779
2020-09-25 22:39:40 +02:00
Nico Alt
ccbe6d4bb8 Expose unread messages count in API's contacts list
Fixes #1746
2020-09-25 17:46:55 +02:00
Torsten Grote
cf8241e79c Fix IllegalStateException in RecyclerView when backing out very quickly
after adding image attachments for preview before sending
2020-02-13 10:28:00 -03:00
Torsten Grote
61d3fe9055 [android] fix IllegalStateException when creating attachments
Injecting the non-singleton AttachmentCreator keeps an instance around
that gets re-used with a different ViewModel.
When backing out without sending or cancelling the attachments,
we don't reset the state which leads us into an illegal state.
2020-02-13 10:28:00 -03:00
Torsten Grote
bded1edb2b [android] Use ordinary HashMap for to be received attachments
Also don't do list stacking from end for now.
2020-02-13 10:26:43 -03:00
akwizgran
4d27828712 Check for concurrent cache updates. 2020-02-13 10:26:43 -03:00
Torsten Grote
0f6f52c37a [android] Listen to AttachmentReceivedEvents when ConversationActivity is stopped
This way Attachments get shown when the activity resumes.
2020-02-13 10:26:42 -03:00
Torsten Grote
c1cf6f61b9 [android] fix concurrency issues when attachments are received delayed
Do not observe attachment live data multiple times
and don't miss received attachments in ImageActivity resp. ImageViewModel.
2020-02-13 10:26:42 -03:00
Torsten Grote
7c22016b81 [android] attach some smaller image attachment issues 2020-02-13 10:26:42 -03:00
Torsten Grote
31f42d44af [android] Refactor attachment loading to use LiveData 2020-02-13 10:26:42 -03:00
Torsten Grote
a1cf485ecc [android] address first round of code review for attachment placeholders 2020-02-13 10:26:41 -03:00
Torsten Grote
b7d3cd7990 [android] support attachments arriving *before* the message containing them 2020-02-13 10:26:41 -03:00
Torsten Grote
4122e0852a Show placeholders for missing attachments in ImageActivity
and display attachments as they arrive while ImageActivity is open.
2020-02-13 10:26:41 -03:00
Torsten Grote
41411b0e2e Refactor attachment loading to support incremental display once loaded 2020-02-13 10:26:40 -03:00
63 changed files with 1645 additions and 442 deletions

View File

@@ -28,6 +28,20 @@
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</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" />
</JetCodeStyleSettings>
<XML>

View File

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

View File

@@ -5,14 +5,14 @@ apply plugin: 'witness'
apply from: 'witness.gradle'
android {
compileSdkVersion 29
buildToolsVersion '29.0.2'
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 10210
versionName "1.2.10"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -53,10 +53,12 @@ dependencies {
}
def torBinariesDir = 'src/main/res/raw'
def torLibsDir = 'src/main/jniLibs'
task cleanTorBinaries {
doLast {
delete fileTree(torBinariesDir) { include '*.zip' }
delete fileTree(torLibsDir) { include '**/*.so' }
}
}
@@ -67,8 +69,36 @@ task unpackTorBinaries {
copy {
from configurations.tor.collect { zipTree(it) }
into torBinariesDir
// TODO: Remove after next Tor upgrade, which won't include non-PIE binaries
include 'geoip.zip', '*_pie.zip'
include 'geoip.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
@@ -76,5 +106,6 @@ task unpackTorBinaries {
tasks.withType(MergeResources) {
inputs.dir torBinariesDir
inputs.dir torLibsDir
dependsOn unpackTorBinaries
}

View File

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

View File

@@ -24,6 +24,7 @@ import java.util.concurrent.Executor;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static android.os.Build.BRAND;
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
@Immutable
@@ -81,8 +82,12 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
@Override
public DuplexPlugin createPlugin(PluginCallback callback) {
// On Motorola devices, don't try to open multiple connections - the
// Bluetooth stack can't handle it
// TODO: Narrow this down to specific models/versions if possible
boolean singleConnection = "motorola".equalsIgnoreCase(BRAND);
BluetoothConnectionLimiter connectionLimiter =
new BluetoothConnectionLimiterImpl(eventBus);
new BluetoothConnectionLimiterImpl(eventBus, singleConnection);
BluetoothConnectionFactory<BluetoothSocket> connectionFactory =
new AndroidBluetoothConnectionFactory(connectionLimiter,
wakeLockManager, timeoutMonitor);

View File

@@ -16,19 +16,42 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.api.system.ResourceProvider;
import org.briarproject.bramble.util.AndroidUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
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 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
@ParametersNotNullByDefault
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 AndroidWakeLock wakeLock;
private final File torLib, obfs4Lib;
AndroidTorPlugin(Executor ioExecutor,
Executor wakefulIoExecutor,
@@ -55,6 +78,9 @@ class AndroidTorPlugin extends TorPlugin {
maxIdleTime, torDirectory);
this.app = app;
wakeLock = wakeLockManager.createWakeLock("TorPlugin");
String nativeLibDir = app.getApplicationInfo().nativeLibraryDir;
torLib = new File(nativeLibDir, TOR_LIB_NAME);
obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME);
}
@Override
@@ -85,4 +111,112 @@ class AndroidTorPlugin extends TorPlugin {
super.stop();
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

@@ -22,6 +22,8 @@ public abstract class AbstractDuplexTransportConnection
private final Writer writer;
private final AtomicBoolean halfClosed, closed;
private volatile boolean markedForClose = false;
protected AbstractDuplexTransportConnection(Plugin plugin) {
this.plugin = plugin;
reader = new Reader();
@@ -52,6 +54,16 @@ public abstract class AbstractDuplexTransportConnection
return remote;
}
@Override
public boolean isMarkedForClose() {
return markedForClose;
}
@Override
public void markForClose() {
markedForClose = true;
}
private class Reader implements TransportConnectionReader {
@Override

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.api.plugin.duplex;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
import org.briarproject.bramble.api.properties.TransportProperties;
@@ -30,4 +31,17 @@ public interface DuplexTransportConnection {
* the remote peer.
*/
TransportProperties getRemoteProperties();
/**
* Returns true if the connection should be closed immediately without
* sending anything.
*/
boolean isMarkedForClose();
/**
* Call this method before the connection is passed to its
* {@link ConnectionHandler} if the connection should be closed immediately
* without sending anything.
*/
void markForClose();
}

View File

@@ -33,6 +33,7 @@ abstract class DuplexSyncConnection extends SyncConnection
final Executor ioExecutor;
final TransportId transportId;
final DuplexTransportConnection connection;
final TransportConnectionReader reader;
final TransportConnectionWriter writer;
final TransportProperties remote;
@@ -80,6 +81,7 @@ abstract class DuplexSyncConnection extends SyncConnection
transportPropertyManager);
this.ioExecutor = ioExecutor;
this.transportId = transportId;
this.connection = connection;
reader = connection.getReader();
writer = connection.getWriter();
remote = connection.getRemoteProperties();
@@ -96,6 +98,13 @@ abstract class DuplexSyncConnection extends SyncConnection
disposeOnError(writer);
}
void closeOutgoingStream(StreamContext ctx, TransportConnectionWriter w)
throws IOException {
StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
w.getOutputStream(), ctx);
streamWriter.sendEndOfStream();
}
SyncSession createDuplexOutgoingSession(StreamContext ctx,
TransportConnectionWriter w, @Nullable Priority priority)
throws IOException {

View File

@@ -93,10 +93,16 @@ class IncomingDuplexSyncConnection extends DuplexSyncConnection
return;
}
try {
// Create and run the outgoing session
SyncSession out = createDuplexOutgoingSession(ctx, writer, null);
setOutgoingSession(out);
out.run();
if (connection.isMarkedForClose()) {
// Close the outgoing stream without sending anything
closeOutgoingStream(ctx, writer);
} else {
// Create and run the outgoing session
SyncSession out =
createDuplexOutgoingSession(ctx, writer, null);
setOutgoingSession(out);
out.run();
}
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);

View File

@@ -65,11 +65,16 @@ class OutgoingDuplexSyncConnection extends DuplexSyncConnection
Priority priority = generatePriority();
ioExecutor.execute(() -> runIncomingSession(priority));
try {
// Create and run the outgoing session
SyncSession out =
createDuplexOutgoingSession(ctx, writer, priority);
setOutgoingSession(out);
out.run();
if (connection.isMarkedForClose()) {
// Close the outgoing stream without sending anything
closeOutgoingStream(ctx, writer);
} else {
// Create and run the outgoing session
SyncSession out =
createDuplexOutgoingSession(ctx, writer, priority);
setOutgoingSession(out);
out.run();
}
writer.dispose(false);
} catch (IOException e) {
logException(LOG, WARNING, e);

View File

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

View File

@@ -23,7 +23,9 @@ interface BluetoothConnectionLimiter {
boolean canOpenContactConnection();
/**
* Informs the limiter that the given connection has been opened.
* Informs the limiter that the given connection has been opened. If the
* connection is above the limit it will be
* {@link DuplexTransportConnection#markForClose() marked for close}.
*/
void connectionOpened(DuplexTransportConnection conn);

View File

@@ -24,6 +24,7 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
getLogger(BluetoothConnectionLimiterImpl.class.getName());
private final EventBus eventBus;
private final boolean singleConnection;
private final Object lock = new Object();
@GuardedBy("lock")
@@ -32,8 +33,10 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
@GuardedBy("lock")
private boolean keyAgreementInProgress = false;
BluetoothConnectionLimiterImpl(EventBus eventBus) {
BluetoothConnectionLimiterImpl(EventBus eventBus,
boolean singleConnection) {
this.eventBus = eventBus;
this.singleConnection = singleConnection;
}
@Override
@@ -59,6 +62,9 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
if (keyAgreementInProgress) {
LOG.info("Can't open contact connection during key agreement");
return false;
} else if (singleConnection && !connections.isEmpty()) {
LOG.info("Can't open contact connection due to limit");
return false;
} else {
LOG.info("Can open contact connection");
return true;
@@ -68,12 +74,18 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter {
@Override
public void connectionOpened(DuplexTransportConnection conn) {
boolean shouldClose = false;
synchronized (lock) {
connections.add(conn);
if (LOG.isLoggable(INFO)) {
LOG.info("Connection opened, " + connections.size() + " open");
}
if (singleConnection && connections.size() > 1) {
LOG.info("Marking connection for close");
shouldClose = true;
}
}
if (shouldClose) conn.markForClose();
}
@Override

View File

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

View File

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

View File

@@ -43,6 +43,15 @@ public class TestDuplexTransportConnection
return new TransportProperties();
}
@Override
public boolean isMarkedForClose() {
return false;
}
@Override
public void markForClose() {
}
/**
* Creates and returns a pair of TestDuplexTransportConnections that are
* connected to each other.

View File

@@ -65,7 +65,7 @@ public class JavaBluetoothPluginFactory implements DuplexPluginFactory {
@Override
public DuplexPlugin createPlugin(PluginCallback callback) {
BluetoothConnectionLimiter connectionLimiter =
new BluetoothConnectionLimiterImpl(eventBus);
new BluetoothConnectionLimiterImpl(eventBus, false);
BluetoothConnectionFactory<StreamConnection> connectionFactory =
new JavaBluetoothConnectionFactory(connectionLimiter,
timeoutMonitor);

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.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.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@@ -75,8 +76,12 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@UiThread
public LiveData<AttachmentResult> storeAttachments(
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();
}
MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
this.result = result;
uris.addAll(newUris);
@@ -95,8 +100,12 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
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();
}
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
@@ -109,8 +118,8 @@ class AttachmentCreatorImpl implements AttachmentCreator {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItem item = retriever.createAttachmentItem(a, needsSize);
if (item.getState() == ERROR) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
@@ -167,21 +176,13 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@Override
@UiThread
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();
}
@Override
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
if (task != null) task.cancel();
deleteUnsentAttachments();
resetState();
}

View File

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

View File

@@ -1,29 +1,63 @@
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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
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.util.List;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
@NotNullByDefault
public interface AttachmentRetriever {
void cachePut(MessageId messageId, List<AttachmentItem> attachments);
@Nullable
List<AttachmentItem> cacheGet(MessageId messageId);
@DatabaseExecutor
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
* {@link InputStream} which will be closed when this method returns.
*/
AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
AttachmentItem createAttachmentItem(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,25 +1,39 @@
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.NoSuchMessageException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
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.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
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
class AttachmentRetrieverImpl implements AttachmentRetriever {
@@ -27,6 +41,8 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
private static final Logger LOG =
getLogger(AttachmentRetrieverImpl.class.getName());
@DatabaseExecutor
private final Executor dbExecutor;
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final ImageSizeCalculator imageSizeCalculator;
@@ -34,13 +50,17 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
private final ConcurrentMap<MessageId, MutableLiveData<AttachmentItem>>
itemsWithSize = new ConcurrentHashMap<>();
private final ConcurrentMap<MessageId, MutableLiveData<AttachmentItem>>
itemsWithoutSize = new ConcurrentHashMap<>();
@Inject
AttachmentRetrieverImpl(MessagingManager messagingManager,
AttachmentRetrieverImpl(@DatabaseExecutor Executor dbExecutor,
MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper,
ImageSizeCalculator imageSizeCalculator) {
this.dbExecutor = dbExecutor;
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
this.imageSizeCalculator = imageSizeCalculator;
@@ -52,40 +72,143 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
}
@Override
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
@DatabaseExecutor
public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException {
return messagingManager.getAttachment(h);
}
@Override
public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
AttachmentHeader h = a.getHeader();
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
public List<LiveData<AttachmentItem>> getAttachmentItems(
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());
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
// 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);
}
InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
// 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();
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 =
imageHelper.getExtensionFromMimeType(h.getContentType());
State state = AVAILABLE;
if (extension == null) {
extension = "";
state = ERROR;
}
item = new AttachmentItem(h, extension, state);
}
return item;
}
private AttachmentItem createAttachmentItem(AttachmentHeader h, Size size) {
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
@@ -104,8 +227,9 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
State state = hasError ? ERROR : AVAILABLE;
return new AttachmentItem(h, size.width, size.height,
extension, thumbnailSize.width, thumbnailSize.height, state);
}
private Size getThumbnailSize(int width, int height, String mimeType) {

View File

@@ -28,7 +28,6 @@ import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
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.EventBus;
import org.briarproject.bramble.api.event.EventListener;
@@ -65,17 +64,16 @@ import org.briarproject.briar.api.client.ProtocolStateException;
import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.api.conversation.ConversationManager;
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.ConversationResponse;
import org.briarproject.briar.api.conversation.DeletionResult;
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
import org.briarproject.briar.api.forum.ForumSharingManager;
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.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList;
@@ -97,6 +95,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
@@ -118,8 +117,6 @@ import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimati
import static androidx.core.view.ViewCompat.setTransitionName;
import static androidx.lifecycle.Lifecycle.State.STARTED;
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.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
@@ -136,6 +133,7 @@ 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.ATTACHMENT_POSITION;
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.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
@@ -185,8 +183,6 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> {
requireNonNull(name);
loadMessages();
@@ -540,6 +536,7 @@ public class ConversationActivity extends BriarActivity
});
}
@DatabaseExecutor
private void eagerlyLoadMessageSize(PrivateMessageHeader h) {
try {
MessageId id = h.getId();
@@ -556,21 +553,11 @@ public class ConversationActivity extends BriarActivity
// images we use a grid so the size is fixed
List<AttachmentHeader> headers = h.getAttachmentHeaders();
if (headers.size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
AttachmentHeader header = headers.get(0);
try {
Attachment a = attachmentRetriever
.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);
}
}
LOG.info("Eagerly loading image size for latest message");
AttachmentHeader header = headers.get(0);
// get the item to retrieve its size
attachmentRetriever
.cacheAttachmentItemWithSize(h.getId(), header);
}
} catch (DbException e) {
logException(LOG, WARNING, e);
@@ -651,59 +638,18 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager);
}
private void loadMessageAttachments(PrivateMessageHeader h) {
// TODO: Use placeholders for missing/invalid attachments
runOnDbThread(() -> {
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();
pair.getSecond().setAttachments(items);
adapter.notifyItemChanged(pair.getFirst());
if (scroll) scrollToBottom();
}
});
@UiThread
private void updateMessageAttachment(MessageId m, AttachmentItem item) {
Pair<Integer, ConversationMessageItem> pair = adapter.getMessageItem(m);
if (pair != null && pair.getSecond().updateAttachments(item)) {
boolean scroll = shouldScrollWhenUpdatingMessage();
adapter.notifyItemChanged(pair.getFirst());
if (scroll) scrollToBottom();
}
}
@Override
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) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) {
@@ -763,15 +709,6 @@ public class ConversationActivity extends BriarActivity
scrollToBottom();
}
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread
private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest ||
@@ -780,7 +717,7 @@ public class ConversationActivity extends BriarActivity
observeOnce(viewModel.getContactDisplayName(), this,
name -> addConversationItem(h.accept(visitor)));
} else {
// visitor also loads message text (if existing)
// visitor also loads message text and attachments (if existing)
addConversationItem(h.accept(visitor));
}
}
@@ -1107,8 +1044,9 @@ public class ConversationActivity extends BriarActivity
i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item));
i.putExtra(NAME, name);
i.putExtra(DATE, messageItem.getTime());
i.putExtra(ITEM_ID, messageItem.getId().getBytes());
// restoring list position should not trigger android bug #224270
String transitionName = item.getTransitionName();
String transitionName = item.getTransitionName(messageItem.getId());
ActivityOptionsCompat options =
makeSceneTransitionAnimation(this, view, transitionName);
ActivityCompat.startActivity(this, i, options.toBundle());
@@ -1147,15 +1085,41 @@ public class ConversationActivity extends BriarActivity
return text;
}
/**
* Called by {@link PrivateMessageHeader#accept(ConversationMessageVisitor)}
*/
@Override
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
List<AttachmentItem> attachments =
attachmentRetriever.cacheGet(h.getId());
if (attachments == null) {
loadMessageAttachments(h);
return emptyList();
List<LiveData<AttachmentItem>> liveDataList =
attachmentRetriever.getAttachmentItems(h);
List<AttachmentItem> items = new ArrayList<>(liveDataList.size());
for (LiveData<AttachmentItem> liveData : liveDataList) {
// first remove all our observers to avoid having more than one
// 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,12 +9,13 @@ import java.util.List;
import javax.annotation.concurrent.NotThreadSafe;
import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread;
@NotThreadSafe
@NotNullByDefault
class ConversationMessageItem extends ConversationItem {
private List<AttachmentItem> attachments;
private final List<AttachmentItem> attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
List<AttachmentItem> attachments) {
@@ -26,8 +27,14 @@ class ConversationMessageItem extends ConversationItem {
return attachments;
}
void setAttachments(List<AttachmentItem> attachments) {
this.attachments = attachments;
@UiThread
boolean updateAttachments(AttachmentItem item) {
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,6 +11,9 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
@@ -30,6 +33,7 @@ import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import java.util.Collection;
import java.util.List;
@@ -56,7 +60,7 @@ import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault
public class ConversationViewModel extends AndroidViewModel
implements AttachmentManager {
implements EventListener, AttachmentManager {
private static Logger LOG =
getLogger(ConversationViewModel.class.getName());
@@ -69,6 +73,7 @@ public class ConversationViewModel extends AndroidViewModel
@DatabaseExecutor
private final Executor dbExecutor;
private final TransactionManager db;
private final EventBus eventBus;
private final MessagingManager messagingManager;
private final ContactManager contactManager;
private final SettingsManager settingsManager;
@@ -101,6 +106,7 @@ public class ConversationViewModel extends AndroidViewModel
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
TransactionManager db,
EventBus eventBus,
MessagingManager messagingManager,
ContactManager contactManager,
SettingsManager settingsManager,
@@ -110,6 +116,7 @@ public class ConversationViewModel extends AndroidViewModel
super(application);
this.dbExecutor = dbExecutor;
this.db = db;
this.eventBus = eventBus;
this.messagingManager = messagingManager;
this.contactManager = contactManager;
this.settingsManager = settingsManager;
@@ -119,12 +126,27 @@ public class ConversationViewModel extends AndroidViewModel
messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false);
eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
attachmentCreator.deleteUnsentAttachments();
attachmentCreator.cancel(); // also deletes unsent attachments
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()));
}
}
}
/**
@@ -252,6 +274,7 @@ public class ConversationViewModel extends AndroidViewModel
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
}
@UiThread
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> headers, long timestamp,
boolean hasImageSupport) {
@@ -270,6 +293,7 @@ public class ConversationViewModel extends AndroidViewModel
}
}
@UiThread
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
dbExecutor.execute(() -> {

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ import com.bumptech.glide.request.target.Target;
import com.github.chrisbanes.photoview.PhotoView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.attachment.AttachmentItem;
@@ -23,6 +24,7 @@ import org.briarproject.briar.android.conversation.glide.GlideApp;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.inject.Inject;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -32,27 +34,36 @@ import static android.os.Build.VERSION.SDK_INT;
import static android.widget.ImageView.ScaleType.FIT_START;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
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.ITEM_ID;
@MethodsNotNullByDefault
@ParametersAreNonnullByDefault
public class ImageFragment extends Fragment {
public class ImageFragment extends Fragment
implements RequestListener<Drawable> {
private final static String IS_FIRST = "isFirst";
@DrawableRes
private static final int ERROR_RES = R.drawable.ic_image_broken;
@Inject
ViewModelProvider.Factory viewModelFactory;
private AttachmentItem attachment;
private boolean isFirst;
private MessageId conversationItemId;
private ImageViewModel viewModel;
private PhotoView photoView;
static ImageFragment newInstance(AttachmentItem a, boolean isFirst) {
static ImageFragment newInstance(AttachmentItem a,
MessageId conversationMessageId, boolean isFirst) {
ImageFragment f = new ImageFragment();
Bundle args = new Bundle();
args.putParcelable(ATTACHMENT_POSITION, a);
args.putBoolean(IS_FIRST, isFirst);
args.putByteArray(ITEM_ID, conversationMessageId.getBytes());
f.setArguments(args);
return f;
}
@@ -70,6 +81,8 @@ public class ImageFragment extends Fragment {
Bundle args = requireNonNull(getArguments());
attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION));
isFirst = args.getBoolean(IS_FIRST);
conversationItemId =
new MessageId(requireNonNull(args.getByteArray(ITEM_ID)));
}
@Nullable
@@ -87,50 +100,68 @@ public class ImageFragment extends Fragment {
photoView.setScaleLevels(1, 2, 4);
photoView.setOnClickListener(view -> viewModel.clickImage());
// Request Listener
RequestListener<Drawable> listener = new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
if (getActivity() != null && isFirst)
getActivity().supportStartPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
// set transition name only when not animatable,
// because the animation won't start otherwise
photoView.setTransitionName(
attachment.getTransitionName());
}
// Move image to the top if overlapping toolbar
if (viewModel.isOverlappingToolbar(photoView, resource)) {
photoView.setScaleType(FIT_START);
}
if (getActivity() != null && isFirst) {
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);
if (attachment.getState() == AVAILABLE) {
loadImage();
// 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
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
startPostponedTransition();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
if (SDK_INT >= 21 && !(resource instanceof Animatable)) {
// set transition name only when not animatable,
// because the animation won't start otherwise
photoView.setTransitionName(
attachment.getTransitionName(conversationItemId));
}
// Move image to the top if overlapping toolbar
if (viewModel.isOverlappingToolbar(photoView, resource)) {
photoView.setScaleType(FIT_START);
}
startPostponedTransition();
return false;
}
private void startPostponedTransition() {
if (getActivity() != null && isFirst) {
getActivity().supportStartPostponedEnterTransition();
}
}
}

View File

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

View File

@@ -7,13 +7,18 @@ import android.view.View;
import org.briarproject.bramble.api.db.DatabaseExecutor;
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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import java.io.File;
import java.io.FileOutputStream;
@@ -22,6 +27,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -35,22 +42,28 @@ import androidx.lifecycle.AndroidViewModel;
import static android.media.MediaScannerConnection.scanFile;
import static android.os.Environment.DIRECTORY_PICTURES;
import static android.os.Environment.getExternalStoragePublicDirectory;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class ImageViewModel extends AndroidViewModel {
public class ImageViewModel extends AndroidViewModel implements EventListener {
private static Logger LOG = getLogger(ImageViewModel.class.getName());
private final MessagingManager messagingManager;
private final EventBus eventBus;
@DatabaseExecutor
private final Executor dbExecutor;
@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.
*/
@@ -62,13 +75,60 @@ public class ImageViewModel extends AndroidViewModel {
@Inject
ImageViewModel(Application application,
MessagingManager messagingManager,
MessagingManager messagingManager, EventBus eventBus,
@DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor) {
super(application);
this.messagingManager = messagingManager;
this.eventBus = eventBus;
this.dbExecutor = dbExecutor;
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() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,22 @@ import androidx.lifecycle.Observer;
@NotNullByDefault
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,
LiveEventHandler<T> handler) {
LiveEventObserver<T> observer = new LiveEventObserver<>(handler);

View File

@@ -5,6 +5,22 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
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) {
super.postValue(new ConsumableEvent<>(value));
}

View File

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

View File

@@ -0,0 +1,10 @@
<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

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

View File

@@ -27,6 +27,7 @@
<string name="enter_password">‮كلمة السّر</string>
<string name="try_again">كلمة السرّ خاطئة, الرجاء المحاولة مجدّدا</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="forgotten_password">نسيتُ كلمة السر</string>
<string name="dialog_title_lost_password">فقدت كلمة السر</string>
@@ -64,12 +65,36 @@
<string name="lock_button">قفل التطبيق</string>
<string name="settings_button">الإعدادات</string>
<string name="sign_out_button">تسجيل الخروج</string>
<string name="transports_onboarding_text">إلمس هنا من أجل التحكم بطريقةالربط مع جهات الاتصال.</string>
<!--Transports: Tor-->
<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-->
<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-->
<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-->
<string name="reminder_notification_title">تم تسجيل الخروج من Briar (براير)</string>
<string name="reminder_notification_text">الرجاء اللمس لإعادة الدخول</string>
@@ -459,6 +484,8 @@
<string name="tor_enable_summary">كل جهات الاتصال تمر عبر شبكة تور من أجل الخصوصية</string>
<string name="tor_network_setting">وسيلة الاتصال لشبكة تور</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>
<!--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>
@@ -574,6 +601,7 @@
<string name="lock_is_locked">Briar (براير) مقفل</string>
<string name="lock_tap_to_unlock">الرجاء اللمس لفك القفل</string>
<!--Connections Screen-->
<string name="transports_help_text">يمكن ل Briar التواصل مع جهات الاتصال عن طريق الانترنت, شكبة ال Wi-Fi أو البلوتوث.n\n\كل وسائل الاتصال عن طريق الانترنت تمر عبر شبكة تور من أجل الخصوصية.n\n\إذا كان من الممكن الوصول إلى شبكة إتصال بعدة طرق فإن Briar سوف يستعملهم جميعاً بالتوازي.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">آليس</string>

View File

@@ -31,7 +31,7 @@
<string name="sign_in_button">Connexion</string>
<string name="forgotten_password">Jai oublié mon mot de passe</string>
<string name="dialog_title_lost_password">Mot de passe oublié</string>
<string name="dialog_message_lost_password">Votre compte Briar est enregistré chiffré sur votre appareil, pas dans le nuage, et nous ne pouvons donc pas réinitialiser votre mot de passe. Souhaitez-vous supprimer votre compte et recommencer?\n\nAttention : vos identités, contacts et messages seront perdus irrémédiablement.</string>
<string name="dialog_message_lost_password">Votre compte Briar est enregistré chiffré sur votre appareil, pas dans le nuage, et nous ne pouvons donc pas réinitialiser votre mot de passe. Voulez-vous supprimer votre compte et recommencer?\n\nAttention : vos identités, contacts et messages seront perdus irrémédiablement.</string>
<string name="startup_failed_notification_title">Impossible de démarrer Briar</string>
<string name="startup_failed_notification_text">Toucher pour plus dinformations.</string>
<string name="startup_failed_activity_title">Échec de démarrage de Briar</string>
@@ -143,7 +143,7 @@
<string name="no_contacts_action">Touchez licône + pour ajouter un contact</string>
<string name="date_no_private_messages">Aucun message.</string>
<string name="no_private_messages">Aucun message à afficher</string>
<string name="message_hint">Rédiger le message</string>
<string name="message_hint">Rédigez un message</string>
<string name="image_caption_hint">Ajouter une légende (facultatif)</string>
<string name="image_attach">Joindre une image</string>
<string name="image_attach_error">Impossible de joindre une ou des images</string>
@@ -154,7 +154,7 @@
<string name="set_alias_button">Modifier</string>
<string name="delete_all_messages">Supprimer tous les messages</string>
<string name="dialog_title_delete_all_messages">Confirmer la suppression des messages</string>
<string name="dialog_message_delete_all_messages">Souhaitez-vous vraiment supprimer tous les messages?</string>
<string name="dialog_message_delete_all_messages">Voulez-vous vraiment supprimer tous les messages?</string>
<string name="dialog_title_not_all_messages_deleted">Impossible de supprimer tous les messages</string>
<string name="dialog_message_not_deleted_ongoing_both">Les messages relatifs aux invitations et présentations en cours ne peuvent pas être supprimés jusquà leur conclusion.</string>
<string name="dialog_message_not_deleted_ongoing_introductions">Les messages relatifs aux présentations en cours ne peuvent pas être supprimés jusquà leur conclusion.</string>
@@ -165,13 +165,13 @@
<string name="dialog_message_not_deleted_not_all_selected_invitations">Pour supprimer une invitation, vous devez sélectionner la demande et la réponse.</string>
<string name="delete_contact">Supprimer le contact</string>
<string name="dialog_title_delete_contact">Confirmer la suppression du contact</string>
<string name="dialog_message_delete_contact">Souhaitez-vous vraiment supprimer ce contact et tous les messages associés?</string>
<string name="dialog_message_delete_contact">Voulez-vous vraiment supprimer ce contact et tous les messages associés?</string>
<string name="contact_deleted_toast">Le contact a été supprimé</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">Vous</string>
<string name="save_image">Enregistrer limage</string>
<string name="dialog_title_save_image">Enregistrer limage?</string>
<string name="dialog_message_save_image">Lenregistrement de cette image permettra aux autres applis dy accéder.\n\n Souhaitez-vous vraiment lenregistrer?</string>
<string name="dialog_message_save_image">Lenregistrement de cette image permettra aux autres applis dy accéder.\n\n Voulez-vous vraiment lenregistrer?</string>
<string name="save_image_success">Limage a été enregistrée</string>
<string name="save_image_error">Impossible denregistrer limage</string>
<string name="dialog_title_no_image_support">Les images ne sont pas disponibles</string>
@@ -198,9 +198,9 @@
<string name="connection_error_explanation">Veuillez vérifier que vous êtes connecté au même réseau Wi-Fi.</string>
<string name="connection_error_feedback">Si le problème persiste, veuillez nous <a href="feedback">envoyer une rétroaction</a> pour nous aider à améliorer l\'appli.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Ajouter un contact à distance</string>
<string name="add_contact_remotely_title_case">Ajouter un contact éloigné</string>
<string name="add_contact_nearby_title">Ajouter un contact à proximité</string>
<string name="add_contact_remotely_title">Ajouter un contact à distance</string>
<string name="add_contact_remotely_title">Ajouter un contact éloigné</string>
<string name="contact_link_intro">Saisissez ici le lien de votre contact</string>
<string name="contact_link_hint">Lien de votre contact</string>
<string name="paste_button">Coller</string>
@@ -266,7 +266,7 @@
<string name="introduction_sent">Votre présentation a été envoyée.</string>
<string name="introduction_error">Une erreur est survenue lors de la présentation.</string>
<string name="introduction_request_sent">Vous avez demandé de présenter %1$s à %2$s.</string>
<string name="introduction_request_received">%1$s a demandé de vous présenter à %2$s. Souhaitez-vous ajouter %2$s à votre liste de contacts?</string>
<string name="introduction_request_received">%1$s a demandé de vous présenter à %2$s. Voulez-vous ajouter %2$s à votre liste de contacts?</string>
<string name="introduction_request_exists_received">%1$s a demandé de vous présenter à %2$s, mais %2$s est déjà dans votre liste de contacts. Puisque %1$s pourrait ne pas le savoir, vous pouvez tout de même répondre :</string>
<string name="introduction_request_answered_received">%1$s a demandé de vous présenter à %2$s.</string>
<string name="introduction_response_accepted_sent">Vous avez accepté dêtre présenté à %1$s.</string>
@@ -299,10 +299,10 @@
<string name="groups_member_joined">%s sest joint au groupe</string>
<string name="groups_leave">Quitter le groupe</string>
<string name="groups_leave_dialog_title">Confirmer la sortie du groupe</string>
<string name="groups_leave_dialog_message">Souhaitez-vous vraiment quitter ce groupe?</string>
<string name="groups_leave_dialog_message">Voulez-vous vraiment quitter ce groupe?</string>
<string name="groups_dissolve">Dissoudre le groupe</string>
<string name="groups_dissolve_dialog_title">Confirmer la dissolution du groupe</string>
<string name="groups_dissolve_dialog_message">Souhaitez-vous vraiment dissoudre ce groupe?\n\nLes autres participants ne pourront pas poursuivre leur conversation et ne recevront peut-être pas les derniers messages.</string>
<string name="groups_dissolve_dialog_message">Voulez-vous vraiment dissoudre ce groupe?\n\nLes autres participants ne pourront pas poursuivre leur conversation et ne recevront peut-être pas les derniers messages.</string>
<string name="groups_dissolve_button">Dissoudre</string>
<string name="groups_dissolved_dialog_title">Le groupe a été dissous</string>
<string name="groups_dissolved_dialog_message">Le créateur de ce groupe la dissous.\n\nVous ne pouvez plus écrire de messages au groupe et ne recevrez peut-être pas tous ceux qui y ont été publiés.</string>
@@ -346,7 +346,7 @@
<string name="btn_reply">Répondre</string>
<string name="forum_leave">Quitter le forum</string>
<string name="dialog_title_leave_forum">Confirmer la sortie du forum</string>
<string name="dialog_message_leave_forum">Souhaitez-vous vraiment quitter ce forum?\n\nLes contacts avec qui vous lavez partagé pourraient ne plus en recevoir les mises à jour.</string>
<string name="dialog_message_leave_forum">Voulez-vous vraiment quitter ce forum?\n\nLes contacts avec qui vous lavez partagé pourraient ne plus en recevoir les mises à jour.</string>
<string name="dialog_button_leave">Quitter</string>
<string name="forum_left_toast">À quitté le forum</string>
<!--Forum Sharing-->
@@ -390,7 +390,7 @@
<string name="blogs_feed_empty_state">Aucun billet à afficher</string>
<string name="blogs_feed_empty_state_action">Les billets de vos contacts et les blogues auxquels vous vous abonnez apparaîtront ici.\n\nTouchez licône de crayon pour rédiger un billet</string>
<string name="blogs_remove_blog">Supprimer le blogue</string>
<string name="blogs_remove_blog_dialog_message">Souhaitez-vous vraiment supprimer ce blogue?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce blogue pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_remove_blog_dialog_message">Voulez-vous vraiment supprimer ce blogue?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce blogue pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_remove_blog_ok">Supprimer</string>
<string name="blogs_blog_removed">Le blogue a été supprimé</string>
<string name="blogs_reblog_comment_hint">Ajouter un commentaire (facultatif)</string>
@@ -420,7 +420,7 @@
<string name="blogs_rss_feeds_manage_author">Auteur :</string>
<string name="blogs_rss_feeds_manage_updated">Dernière mise à jour :</string>
<string name="blogs_rss_remove_feed">Supprimer le fil</string>
<string name="blogs_rss_remove_feed_dialog_message">Souhaitez-vous vraiment supprimer ce fil?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce fil pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_rss_remove_feed_dialog_message">Voulez-vous vraiment supprimer ce fil?\nLes billets seront supprimés de votre appareil mais pas des appareils dautrui.\n\nLes contacts avec qui vous avez partagé ce fil pourraient ne plus en recevoir les mises à jour.</string>
<string name="blogs_rss_remove_feed_ok">Supprimer</string>
<string name="blogs_rss_feeds_manage_delete_error">Impossible de supprimer le fil!</string>
<string name="blogs_rss_feeds_manage_empty_state">Aucun fil RSS à afficher\n\nTouchez licône + pour importer un fil</string>
@@ -484,7 +484,7 @@
<string name="panic_app_setting_summary">Aucune appli na été définie</string>
<string name="panic_app_setting_none">Aucune</string>
<string name="dialog_title_connect_panic_app">Confirmer lapplication durgence</string>
<string name="dialog_message_connect_panic_app">Souhaitez-vous vraiment autoriser %1$s à déclencher les actions destructrices du bouton durgence?</string>
<string name="dialog_message_connect_panic_app">Voulez-vous vraiment autoriser %1$s à déclencher les actions destructrices du bouton durgence?</string>
<string name="panic_setting_destructive_action">Actions destructrices</string>
<string name="panic_setting_signout_title">Déconnexion</string>
<string name="panic_setting_signout_summary">Se déconnecter de Briar si lon appuie sur un bouton durgence</string>

View File

@@ -61,12 +61,36 @@
<string name="lock_button">Bloquear App</string>
<string name="settings_button">Axustes</string>
<string name="sign_out_button">Finalizar sesión</string>
<string name="transports_onboarding_text">Toca aquí para controlar o xeito en que Briar conecta cos teus contactos.</string>
<!--Transports: Tor-->
<string name="transport_tor">Internet</string>
<string name="tor_device_status_online_wifi">O teu móbil ten acceso a internet por Wi-Fi</string>
<string name="tor_device_status_online_mobile">O teu móbil ten acceso a internet por datos móbiles</string>
<string name="tor_device_status_offline">O teu móbil non ten acceso a internet</string>
<string name="tor_plugin_status_enabling">Briar estase conectando a internet</string>
<string name="tor_plugin_status_active">Briar está conectada a internet</string>
<string name="tor_plugin_status_inactive">Briar non pode conectarse a internet</string>
<string name="tor_plugin_status_disabled">Briar está configurada para non usar internet</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar está configurada para non usar datos móbilies</string>
<string name="tor_plugin_status_disabled_battery">Briar está configurada para non usar internet se está usando batería</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar está configurada para non usar internet neste país</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">Mesma rede Wi-Fi</string>
<string name="lan_device_status_on">O móbil está conectado á Wi-Fi</string>
<string name="lan_device_status_off">O móbil non está conectado á Wi-Fi</string>
<string name="lan_plugin_status_enabling">Briar está conectando á rede Wi-Fi</string>
<string name="lan_plugin_status_active">Briar está conectada á rede Wi-Fi</string>
<string name="lan_plugin_status_inactive">Briar non pode conectar coa rede Wi-Fi</string>
<string name="lan_plugin_status_disabled">Briar está configurada para non usar a rede Wi-Fi</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">O móbil ten o Bluetooth activado</string>
<string name="bt_device_status_off">O móbil ten o Bluetooth desactivado</string>
<string name="bt_plugin_status_enabling">Briar está conectando por Bluetooth</string>
<string name="bt_plugin_status_active">Briar está conectada ó Bluetooth</string>
<string name="bt_plugin_status_inactive">Briar non pode conectar por Bluetooth</string>
<string name="bt_plugin_status_disabled">Briar está configurada para non usar Bluetooth</string>
<!--Notifications-->
<string name="reminder_notification_title">Desconectou de Briar</string>
<string name="reminder_notification_text">Toque para voltar a conectar</string>
@@ -536,6 +560,7 @@
<string name="lock_is_locked">Briar está bloqueada</string>
<string name="lock_tap_to_unlock">Toque para desbloquear</string>
<!--Connections Screen-->
<string name="transports_help_text">Briar pode conectar cos teus contactos a través de Internet, Wi-Fi ou Bluetooth.\n\nTodas as conexións a internet pasan a través da rede Tor para máis privacidade.\n\nSe un contacto é accesible de múltiples xeitos, Briar usaráos en paralelo.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alice</string>

View File

@@ -61,12 +61,36 @@
<string name="lock_button">App zárolása</string>
<string name="settings_button">Beállítások</string>
<string name="sign_out_button">Kijelentkezés</string>
<string name="transports_onboarding_text">Érintse meg itt, hogy beállíthassa, hogyan csatlakozzon a Tor kapcsolataihoz.</string>
<!--Transports: Tor-->
<string name="transport_tor">Internet</string>
<string name="tor_device_status_online_wifi">A telefonja Internet hozzáféréssel rendelkezik Wi-Fi-n keresztül</string>
<string name="tor_device_status_online_mobile">A telefonja Internet hozzáféréssel rendelkezik mobil hálózaton keresztül</string>
<string name="tor_device_status_offline">A telefonja nem rendelkezik Internet hozzáféréssel</string>
<string name="tor_plugin_status_enabling">A Briar csatlakozik az Internethez</string>
<string name="tor_plugin_status_active">A Briar csatlakoztatva az Internethez</string>
<string name="tor_plugin_status_inactive">A Briar nem tud csatlakozni az Internethez</string>
<string name="tor_plugin_status_disabled">A Briar Internet nélküli használatra van beállítva</string>
<string name="tor_plugin_status_disabled_mobile_data">A Briar mobil hálózat nélküli használatra van beállítva</string>
<string name="tor_plugin_status_disabled_battery">A Briar úgy van beállítva, hogy ne használjon Internetet, ha akkumulátorról megy a telefon</string>
<string name="tor_plugin_status_disabled_country_blocked">A Briar úgy van beállítva, hogy ne használjon Internetet ebben az országban</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">Azonos Wi-Fi hálózat</string>
<string name="lan_device_status_on">A telefonja Wi-Fi-hez csatlakoztatott.</string>
<string name="lan_device_status_off">A telefonja Wi-Fi-hez nem csatlakoztatott.</string>
<string name="lan_plugin_status_enabling">A Briar csatlakozik a Wi-Fi hálózathoz</string>
<string name="lan_plugin_status_active">A Briar csatlakoztatva a Wi-Fi hálózathoz</string>
<string name="lan_plugin_status_inactive">A Briar nem tud csatlakozni a Wi-Fi hálózathoz</string>
<string name="lan_plugin_status_disabled">A Briar Wi-Fi nélküli használatra van beállítva</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">A telefonja Bluetooth-ja bekapcsolva</string>
<string name="bt_device_status_off">A telefonja Bluetooth-ja kikapcsolva</string>
<string name="bt_plugin_status_enabling">A Briar csatlakozik a Bluetooth-hoz</string>
<string name="bt_plugin_status_active">A Briar csatlakoztatva a Bluetooth-hoz</string>
<string name="bt_plugin_status_inactive">A Briar nem tud csatlakozni a Bluetooth-hoz</string>
<string name="bt_plugin_status_disabled">A Briar Bluetooth nélküli használatra van beállítva</string>
<!--Notifications-->
<string name="reminder_notification_title">Kilépve a Briar-ból</string>
<string name="reminder_notification_text">Érintse meg az újra belépéshez.</string>
@@ -421,10 +445,19 @@ Kapcsolatai, akivel megosztotta ezt a blogot, lehet nem kapnak többé frissít
<string name="pref_theme_system">Rendszer alapértelmezett</string>
<!--Settings Connections-->
<string name="network_settings_title">Kapcsolatok</string>
<string name="bluetooth_setting">Csatlakozás a kapcsolatokhoz Bluetooth-on</string>
<string name="wifi_setting">Csatlakozás az egy Wi-Fi hálózaton lévő kapcsolatokhoz</string>
<string name="tor_enable_title">Csatlakozás a kapcsolatokhoz Interneten</string>
<string name="tor_enable_summary">Minden kapcsolat átmegy a Tor hálózaton az adatvédelem érdekében</string>
<string name="tor_network_setting">Kapcsolódási mód a Tor hálózathoz</string>
<string name="tor_network_setting_automatic">Automatikusan hely alapján</string>
<string name="tor_network_setting_without_bridges">Tor hálózat használata hidak nélkül</string>
<string name="tor_network_setting_with_bridges">Tor hálózat használata hidakkal</string>
<string name="tor_network_setting_never">Ne csatlakozzon az internethez</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)"-->
<string name="tor_network_setting_summary">Automatikus: %1$s ( %2$s)</string>
<string name="tor_mobile_data_title">Mobil adat használata</string>
<string name="tor_only_when_charging_title">Csatlakozás az internethez csak töltés alatt</string>
<string name="tor_only_when_charging_summary">Letiltja az internet kapcsolatot, amikor elemről fut az eszköz</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Biztonság</string>
@@ -536,6 +569,7 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
<string name="lock_is_locked">A Briar zárolt</string>
<string name="lock_tap_to_unlock">Érintse meg a zárolás feloldásához</string>
<!--Connections Screen-->
<string name="transports_help_text">A Briar Interneten, Wi-Fi-n vagy Bluetooth-on keresztül csatlakozhat kapcsolataihoz.\n\nAz összes internetkapcsolat a Tor hálózaton megy keresztül megy az adatvédelem érdekében.\n\nHa egy kapcsolatot több módszerrel is el lehet érni, Briar párhuzamosan használja azokat.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alice</string>

View File

@@ -456,9 +456,9 @@
<string name="pref_lock_title">Forritslæsing</string>
<string name="pref_lock_summary">Notaðu skjálæsingu tækisins til að vernda Briar á meðan þú ert skráð/ur inn</string>
<string name="pref_lock_disabled_summary">Til að nota þetta þarftu að setja upp skjálæsingu fyrir tækið þitt</string>
<string name="pref_lock_timeout_title">Tímamörk forritslæsingar við aðgerðaleysi</string>
<string name="pref_lock_timeout_title">Tímamörk læsingar forrits við aðgerðaleysi</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Þehar ekki er verið að nota Briar, læsa því sjálfvirkt eftir %s</string>
<string name="pref_lock_timeout_summary">Þegar ekki er verið að nota Briar, læsa því sjálfvirkt eftir %s</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_1">1 mínúta</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->

View File

@@ -61,12 +61,36 @@
<string name="lock_button">Lås appen</string>
<string name="settings_button">Inställningar</string>
<string name="sign_out_button">Logga ut</string>
<string name="transports_onboarding_text">Klicka här för att styra hur Briar ansluter till dina kontakter.</string>
<!--Transports: Tor-->
<string name="transport_tor">Internet</string>
<string name="tor_device_status_online_wifi">Din telefon har internetåtkomst via Wi-Fi</string>
<string name="tor_device_status_online_mobile">Din telefon har internetåtkomst via mobildata</string>
<string name="tor_device_status_offline">Din telefon har inte internetåtkomst</string>
<string name="tor_plugin_status_enabling">Briar ansluter till internet</string>
<string name="tor_plugin_status_active">Briar är anslutet till internet</string>
<string name="tor_plugin_status_inactive">Briar kan inte ansluta till internet</string>
<string name="tor_plugin_status_disabled">Briar är inte konfigurerat för att använda internet</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar är konfigurerat så att de inte använder mobildata</string>
<string name="tor_plugin_status_disabled_battery">Briar är konfigurerat att inte använda internet vid batterianvändning</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar är konfigurerat att inte använda internet i detta landet</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">Samma Wi-Fi-nätverk</string>
<string name="lan_device_status_on">Din telefon är ansluten till Wi-Fi</string>
<string name="lan_device_status_off">Din telefon är inte ansluten till Wi-Fi</string>
<string name="lan_plugin_status_enabling">Briar ansluter till Wi-Fi-nätverket</string>
<string name="lan_plugin_status_active">Briar är anslutet till Wi-Fi nätverket</string>
<string name="lan_plugin_status_inactive">Briar kan inte ansluta till Wi-Fi-nätverket</string>
<string name="lan_plugin_status_disabled">Briar är konfigurerat för att inte använda Wi-Fi-nätverket</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">Din telefons Bluetooth är aktiverad</string>
<string name="bt_device_status_off">Din telefons Bluetooth är inaktiverad</string>
<string name="bt_plugin_status_enabling">Briar ansluter till Bluetooth</string>
<string name="bt_plugin_status_active">Briar är ansluten till Bluetooth</string>
<string name="bt_plugin_status_inactive">Briar kan inte ansluta till Bluetooth</string>
<string name="bt_plugin_status_disabled">Briar är konfigurerat för att inte använda Bluetooth</string>
<!--Notifications-->
<string name="reminder_notification_title">Utloggad från Briar</string>
<string name="reminder_notification_text">Tryck för att logga in igen.</string>
@@ -412,10 +436,20 @@
<string name="pref_theme_auto">Automatisk (dagtid)</string>
<string name="pref_theme_system">Systemets förval</string>
<!--Settings Connections-->
<string name="network_settings_title">Anslutningar</string>
<string name="bluetooth_setting">Anslut till kontakter via Bluetooth</string>
<string name="wifi_setting">Anslut till kontakter på samma Wi-Fi-nätverk</string>
<string name="tor_enable_title">Anslut till kontakter via internet</string>
<string name="tor_enable_summary">Alla anslutningar gå via Tor-nätverket av integritetsskäl</string>
<string name="tor_network_setting">Anslutningsmetod för Tor-nätverket</string>
<string name="tor_network_setting_automatic">Automatisk, baserad på position</string>
<string name="tor_network_setting_without_bridges">Använd Tor-nätverket utan bryggor</string>
<string name="tor_network_setting_with_bridges">Anvädn Tor-nätverket med bryggor</string>
<string name="tor_network_setting_never">Anslut inte till 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)"-->
<string name="tor_network_setting_summary">Automatisk: %1$s (i %2$s)</string>
<string name="tor_mobile_data_title">Använd mobildata</string>
<string name="tor_only_when_charging_title">Anslut till internet endast vid laddning</string>
<string name="tor_only_when_charging_summary">Avaktiverar anslutning över Internet när enheten går på batteri</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">Säkerhet</string>
@@ -526,6 +560,7 @@
<string name="lock_is_locked">Briar är låst</string>
<string name="lock_tap_to_unlock">Tryck för att låsa upp</string>
<!--Connections Screen-->
<string name="transports_help_text">Briar kan ansluta till dina kontakter via internet, Wi-Fi eller Bluetooth.\n\nAlla internetanslutningar går via Tor-nätverket av integritetsskäl.\n\nOm en kontakt kan nås via flera metoder kommer Briar att använda dem parallellt.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alice</string>

View File

@@ -61,12 +61,36 @@
<string name="lock_button">Uygulamayı Kilitle</string>
<string name="settings_button">Ayarlar</string>
<string name="sign_out_button">Oturumu Kapat</string>
<string name="transports_onboarding_text">Briar\'ın kişilerinizle nasıl bağlanacağını kontrol etmek için buraya tıklayın</string>
<!--Transports: Tor-->
<string name="transport_tor">İnternet</string>
<string name="tor_device_status_online_wifi">Telefonunuzun Wi-Fi aracılığıyla İnternet erişimi var</string>
<string name="tor_device_status_online_mobile">Telefonunuzun mobil veri aracılığıyla İnternet erişimi var</string>
<string name="tor_device_status_offline">Telefonunuzun İnternet erişimi yok</string>
<string name="tor_plugin_status_enabling">Briar İnternet\'e bağlanıyor</string>
<string name="tor_plugin_status_active">Briar İnternet\'e bağlandı</string>
<string name="tor_plugin_status_inactive">Briar İnternet\'e bağlanamıyor</string>
<string name="tor_plugin_status_disabled">Briar İnternet kullanmamak üzere yapılandırılmış</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar mobil veri kullanmamak üzere yapılandırılmış</string>
<string name="tor_plugin_status_disabled_battery">Briar pille çalışırken İnternet kullanmamak üzere yapılandırılmış</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar bu ülkede İnternet kullanmamak üzere yapılandırılmış</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">Aynı Wi-Fi Ağı</string>
<string name="lan_device_status_on">Telefonunuz Wi-Fi\'ye bağlı</string>
<string name="lan_device_status_off">Telefonunuz Wi-Fi\'ye bağlı değil</string>
<string name="lan_plugin_status_enabling">Briar Wi-Fi ağına bağlanıyor</string>
<string name="lan_plugin_status_active">Briar Wi-Fi ağına bağlandı</string>
<string name="lan_plugin_status_inactive">Briar Wi-Fi ağına bağlanamıyor</string>
<string name="lan_plugin_status_disabled">Briar Wi-Fi ağını kullanmamak üzere yapılandırılmış</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">Telefonunuzun Bluetooth\'u açık</string>
<string name="bt_device_status_off">Telefonunuzun Bluetooth\'u kapalı</string>
<string name="bt_plugin_status_enabling">Briar Bluetooth\'a bağlanıyor</string>
<string name="bt_plugin_status_active">Briar Bluetooth\'a bağlandı</string>
<string name="bt_plugin_status_inactive">Briar Bluetooth\'a bağlanamıyor</string>
<string name="bt_plugin_status_disabled">Briar Bluetooth kullanmamak üzere yapılandırılmış</string>
<!--Notifications-->
<string name="reminder_notification_title">Briar oturumu kapatıldı</string>
<string name="reminder_notification_text">Tekrar oturum açmak için dokunun</string>
@@ -536,6 +560,7 @@
<string name="lock_is_locked">Briar kilitli</string>
<string name="lock_tap_to_unlock">Kilidi açmak için dokunun</string>
<!--Connections Screen-->
<string name="transports_help_text">Briar kişilerinizle İnternet, Wi-Fi veya Bluetooth ile bağlanabilir.\n\nBütün İnternet bağlantıları gizliliğiniz için Tor Ağı üzerinden yapılıyor.\n\nEğer bir kişiniz birçok yöntemle erişilebiliyorsa, Briar bu yöntemleri paralel olarak kullanır.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alice</string>

View File

@@ -210,7 +210,6 @@
<string name="connecting_to_device">Connecting to device\u2026</string>
<string name="authenticating_with_device">Authenticating with device\u2026</string>
<string name="connection_error_title">Could not connect to your contact</string>
<string name="connection_error_explanation">Please check that you\'re both connected to the same Wi-Fi network.</string>
<string name="connection_error_feedback">If this problem persists, please <a href="feedback">send feedback</a> to help us improve the app.</string>
<!-- Adding Contacts Remotely -->
@@ -589,6 +588,7 @@
<string name="permission_camera_location_title">Camera and location</string>
<string name="permission_camera_location_request_body">To scan the QR code, Briar needs access to the camera.\n\nTo discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string>
<string name="permission_camera_denied_body">You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access.</string>
<string name="permission_location_denied_body">You have denied access to your location, but Briar needs this permission to discover Bluetooth devices.\n\nPlease consider granting access.</string>
<string name="qr_code">QR code</string>
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.attachment;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.ImmediateExecutor;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
@@ -11,12 +12,13 @@ import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.concurrent.Executor;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
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.assertFalse;
import static org.junit.Assert.assertTrue;
public class AttachmentRetrieverTest extends BrambleMockTestCase {
@@ -33,8 +35,9 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
MessagingManager messagingManager =
context.mock(MessagingManager.class);
imageSizeCalculator = context.mock(ImageSizeCalculator.class);
retriever = new AttachmentRetrieverImpl(messagingManager, dimensions,
imageHelper, imageSizeCalculator);
Executor dbExecutor = new ImmediateExecutor();
retriever = new AttachmentRetrieverImpl(dbExecutor, messagingManager,
dimensions, imageHelper, imageSizeCalculator);
}
@Test
@@ -47,10 +50,10 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
will(returnValue("jpg"));
}});
AttachmentItem item = retriever.getAttachmentItem(attachment, false);
AttachmentItem item = retriever.createAttachmentItem(attachment, false);
assertEquals(mimeType, item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
assertEquals(AVAILABLE, item.getState());
}
@Test
@@ -63,8 +66,8 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
will(returnValue(null));
}});
AttachmentItem item = retriever.getAttachmentItem(attachment, false);
assertTrue(item.hasError());
AttachmentItem item = retriever.createAttachmentItem(attachment, false);
assertEquals(ERROR, item.getState());
}
@Test
@@ -80,7 +83,7 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
will(returnValue("jpg"));
}});
AttachmentItem item = retriever.getAttachmentItem(attachment, true);
AttachmentItem item = retriever.createAttachmentItem(attachment, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
@@ -88,7 +91,7 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
assertEquals(240, item.getThumbnailHeight());
assertEquals(mimeType, item.getMimeType());
assertEquals("jpg", item.getExtension());
assertFalse(item.hasError());
assertEquals(AVAILABLE, item.getState());
}
@Test
@@ -104,12 +107,12 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
will(returnValue("jpg"));
}});
AttachmentItem item = retriever.getAttachmentItem(attachment, true);
AttachmentItem item = retriever.createAttachmentItem(attachment, true);
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
assertEquals(dimensions.maxHeight, item.getThumbnailHeight());
assertFalse(item.hasError());
assertEquals(AVAILABLE, item.getState());
}
@Test
@@ -125,8 +128,8 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
will(returnValue(null));
}});
AttachmentItem item = retriever.getAttachmentItem(attachment, true);
assertTrue(item.hasError());
AttachmentItem item = retriever.createAttachmentItem(attachment, true);
assertEquals(ERROR, item.getState());
}
private Attachment getAttachment(String contentType) {

View File

@@ -68,7 +68,7 @@ public interface MessagingManager extends ConversationClient {
String getMessageText(MessageId m) throws DbException;
/**
* Returns the attachment with the given message ID and content type.
* Returns the attachment with the given attachment header.
*
* @throws InvalidAttachmentException If the header refers to a message
* that is not an attachment, or to an attachment that does not have the

View File

@@ -69,7 +69,8 @@ Returns a JSON array of contacts:
"handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=",
"verified": true,
"lastChatActivity": 1557838312175,
"connected": false
"connected": false,
"unreadCount": 7
}
```
@@ -182,6 +183,18 @@ Note that it's also possible to add contacts nearby via Bluetooth/Wifi or
introductions. In these cases contacts omit the `pendingContact` state and
directly become `contact`s.
### Changing alias of a contact
`PUT /v1/contacts/{contactId}/alias`
The alias should be posted as a JSON object:
```json
{
"alias": "A nickname for the new contact"
}
```
### Removing a contact
`DELETE /v1/contacts/{contactId}`
@@ -233,6 +246,25 @@ The text of the message should be posted as JSON:
}
```
### Marking private messages as read
`POST /v1/messages/{contactId}/read`
The `messageId` of the message to be marked as read
needs to be provided in the request body as follows:
```json
{
"messageId": "+AIMMgOCPFF8HDEhiEHYjbfKrg7v0G94inKxjvjYzA8="
}
```
### Deleting all private messages
`DELETE /v1/messages/{contactId}/all`
It returns with a status code `200`, if removal was successful.
### Listing blog posts
`GET /v1/blogs/posts`
@@ -409,3 +441,39 @@ When the last connection is lost (the contact goes offline), it sends a `Contact
"type": "event"
}
```
### A message was sent
When Briar sent a message to a contact, it sends a `MessagesSentEvent`. This is indicated in Briar
by showing one tick next to the message.
```json
{
"data": {
"contactId": 1,
"messageIds": [
"+AIMMgOCPFF8HDEhiEHYjbfKrg7v0G94inKxjvjYzA8="
]
},
"name": "MessagesSentEvent",
"type": "event"
}
```
### A message was acknowledged
When a contact acknowledges that they received a message, Briar sends a `MessagesAckedEvent`.
This is indicated in Briar by showing two ticks next to the message.
```json
{
"data": {
"contactId": 1,
"messageIds": [
"+AIMMgOCPFF8HDEhiEHYjbfKrg7v0G94inKxjvjYzA8="
]
},
"name": "MessagesAckedEvent",
"type": "event"
}
```

View File

@@ -81,11 +81,20 @@ constructor(
path("/:contactId") {
delete { ctx -> contactController.delete(ctx) }
}
path("/:contactId/alias") {
put { ctx -> contactController.setContactAlias(ctx) }
}
}
path("/messages/:contactId") {
get { ctx -> messagingController.list(ctx) }
post { ctx -> messagingController.write(ctx) }
}
path("/messages/:contactId/read") {
post { ctx -> messagingController.markMessageRead(ctx) }
}
path("/messages/:contactId/all") {
delete { ctx -> messagingController.deleteAllMessages(ctx) }
}
path("/forums") {
get { ctx -> forumController.list(ctx) }
post { ctx -> forumController.create(ctx) }

View File

@@ -9,6 +9,7 @@ interface ContactController {
fun addPendingContact(ctx: Context): Context
fun listPendingContacts(ctx: Context): Context
fun removePendingContact(ctx: Context): Context
fun setContactAlias(ctx: Context): Context
fun delete(ctx: Context): Context
}

View File

@@ -77,7 +77,8 @@ constructor(
val contacts = contactManager.contacts.map { contact ->
val latestMsgTime = conversationManager.getGroupCount(contact.id).latestMsgTime
val connected = connectionRegistry.isConnected(contact.id)
contact.output(latestMsgTime, connected)
val unreadCount = conversationManager.getGroupCount(contact.id).unreadCount
contact.output(latestMsgTime, connected, unreadCount)
}
return ctx.json(contacts)
}
@@ -91,9 +92,7 @@ constructor(
val link = ctx.getFromJson(objectMapper, "link")
val alias = ctx.getFromJson(objectMapper, "alias")
if (!LINK_REGEX.matcher(link).find()) throw BadRequestResponse("Invalid Link")
val aliasUtf8 = toUtf8(alias)
if (aliasUtf8.isEmpty() || aliasUtf8.size > MAX_AUTHOR_NAME_LENGTH)
throw BadRequestResponse("Invalid Alias")
checkAliasLength(alias)
val pendingContact = contactManager.addPendingContact(link, alias)
return ctx.json(pendingContact.output())
}
@@ -124,6 +123,18 @@ constructor(
return ctx
}
override fun setContactAlias(ctx: Context): Context {
val contactId = ctx.getContactIdFromPathParam()
val alias = ctx.getFromJson(objectMapper, "alias")
checkAliasLength(alias)
try {
contactManager.setContactAlias(contactId, alias)
} catch (e: NoSuchContactException) {
throw NotFoundResponse()
}
return ctx
}
override fun delete(ctx: Context): Context {
val contactId = ctx.getContactIdFromPathParam()
try {
@@ -134,4 +145,10 @@ constructor(
return ctx
}
private fun checkAliasLength(alias: String) {
val aliasUtf8 = toUtf8(alias)
if (aliasUtf8.isEmpty() || aliasUtf8.size > MAX_AUTHOR_NAME_LENGTH)
throw BadRequestResponse("Invalid Alias")
}
}

View File

@@ -7,12 +7,13 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
import org.briarproject.bramble.identity.output
import org.briarproject.briar.headless.json.JsonDict
internal fun Contact.output(latestMsgTime: Long, connected: Boolean) = JsonDict(
internal fun Contact.output(latestMsgTime: Long, connected: Boolean, unreadCount: Int) = JsonDict(
"contactId" to id.int,
"author" to author.output(),
"verified" to isVerified,
"lastChatActivity" to latestMsgTime,
"connected" to connected
"connected" to connected,
"unreadCount" to unreadCount
).apply {
alias?.let { put("alias", it) }
handshakePublicKey?.let { put("handshakePublicKey", it.encoded) }

View File

@@ -8,4 +8,8 @@ interface MessagingController {
fun write(ctx: Context): Context
fun markMessageRead(ctx: Context): Context
fun deleteAllMessages(ctx: Context): Context
}

View File

@@ -11,6 +11,9 @@ import org.briarproject.bramble.api.db.DatabaseExecutor
import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.api.event.Event
import org.briarproject.bramble.api.event.EventListener
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent
import org.briarproject.bramble.api.sync.event.MessagesSentEvent
import org.briarproject.bramble.api.sync.MessageId
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.util.StringUtils.utf8IsTooLong
import org.briarproject.briar.api.blog.BlogInvitationRequest
@@ -33,12 +36,16 @@ import org.briarproject.briar.headless.event.output
import org.briarproject.briar.headless.getContactIdFromPathParam
import org.briarproject.briar.headless.getFromJson
import org.briarproject.briar.headless.json.JsonDict
import org.spongycastle.util.encoders.Base64
import org.spongycastle.util.encoders.DecoderException
import java.util.concurrent.Executor
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
internal const val EVENT_CONVERSATION_MESSAGE = "ConversationMessageReceivedEvent"
internal const val EVENT_MESSAGES_ACKED = "MessagesAckedEvent"
internal const val EVENT_MESSAGES_SENT = "MessagesSentEvent"
@Immutable
@Singleton
@@ -79,6 +86,36 @@ constructor(
return ctx.json(m.output(contact.id, text))
}
override fun markMessageRead(ctx: Context): Context {
val contact = getContact(ctx)
val groupId = messagingManager.getContactGroup(contact).id
val messageIdString = ctx.getFromJson(objectMapper, "messageId")
val messageId = deserializeMessageId(messageIdString)
messagingManager.setReadFlag(groupId, messageId, true)
return ctx.json(messageIdString)
}
private fun deserializeMessageId(idString: String): MessageId {
val idBytes = try {
Base64.decode(idString)
} catch (e: DecoderException) {
throw NotFoundResponse()
}
if (idBytes.size != MessageId.LENGTH) throw NotFoundResponse()
return MessageId(idBytes)
}
override fun deleteAllMessages(ctx: Context): Context {
val contactId = ctx.getContactIdFromPathParam()
try {
val result = conversationManager.deleteAllMessages(contactId)
return ctx.json(result.output())
} catch (e: NoSuchContactException) {
throw NotFoundResponse()
}
}
override fun eventOccurred(e: Event) {
when (e) {
is ConversationMessageReceivedEvent<*> -> {
@@ -90,6 +127,12 @@ constructor(
webSocketController.sendEvent(EVENT_CONVERSATION_MESSAGE, e.output())
}
}
is MessagesSentEvent -> {
webSocketController.sendEvent(EVENT_MESSAGES_SENT, e.output())
}
is MessagesAckedEvent -> {
webSocketController.sendEvent(EVENT_MESSAGES_ACKED, e.output())
}
}
}

View File

@@ -1,7 +1,11 @@
package org.briarproject.briar.headless.messaging
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.api.sync.MessageId
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent
import org.briarproject.bramble.api.sync.event.MessagesSentEvent
import org.briarproject.briar.api.conversation.ConversationMessageHeader
import org.briarproject.briar.api.conversation.DeletionResult
import org.briarproject.briar.api.messaging.PrivateMessage
import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.headless.json.JsonDict
@@ -43,3 +47,24 @@ internal fun PrivateMessage.output(contactId: ContactId, text: String) = JsonDic
"groupId" to message.groupId.bytes,
"text" to text
)
internal fun DeletionResult.output() = JsonDict(
"allDeleted" to allDeleted(),
"hasIntroductionSessionInProgress" to hasIntroductionSessionInProgress(),
"hasInvitationSessionInProgress" to hasInvitationSessionInProgress(),
"hasNotAllIntroductionSelected" to hasNotAllIntroductionSelected(),
"hasNotAllInvitationSelected" to hasNotAllInvitationSelected(),
"hasNotFullyDownloaded" to hasNotFullyDownloaded()
)
internal fun MessagesAckedEvent.output() = JsonDict(
"contactId" to contactId.int,
"messageIds" to messageIds.toJson()
)
internal fun MessagesSentEvent.output() = JsonDict(
"contactId" to contactId.int,
"messageIds" to messageIds.toJson()
)
internal fun Collection<MessageId>.toJson() = map { it.bytes }

View File

@@ -46,6 +46,7 @@ abstract class ControllerTest {
protected val message: Message = getMessage(group.id)
protected val text: String = getRandomString(5)
protected val timestamp = 42L
protected val unreadCount = 42
protected fun assertJsonEquals(json: String, obj: Any) {
assertEquals(json, outputCtx.json(obj).resultString(), STRICT)

View File

@@ -6,6 +6,7 @@ import io.javalin.plugin.json.JavalinJson.toJson
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockkStatic
import io.mockk.runs
import org.briarproject.bramble.api.Pair
import org.briarproject.bramble.api.contact.Contact
@@ -27,6 +28,7 @@ import org.briarproject.bramble.test.TestUtils.getPendingContact
import org.briarproject.bramble.test.TestUtils.getRandomBytes
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.getFromJson
import org.briarproject.briar.headless.json.JsonDict
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows
@@ -58,7 +60,8 @@ internal class ContactControllerTest : ControllerTest() {
every { contactManager.contacts } returns listOf(contact)
every { conversationManager.getGroupCount(contact.id).latestMsgTime } returns timestamp
every { connectionRegistry.isConnected(contact.id) } returns connected
every { ctx.json(listOf(contact.output(timestamp, connected))) } returns ctx
every { conversationManager.getGroupCount(contact.id).unreadCount } returns unreadCount
every { ctx.json(listOf(contact.output(timestamp, connected, unreadCount))) } returns ctx
controller.list(ctx)
}
@@ -193,6 +196,66 @@ internal class ContactControllerTest : ControllerTest() {
}
}
@Test
fun testSetContactAlias() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
every { ctx.pathParam("contactId") } returns "1"
every { ctx.getFromJson(objectMapper, "alias") } returns "foo"
every { contactManager.setContactAlias(ContactId(1), "foo") } just Runs
controller.setContactAlias(ctx)
}
@Test
fun testSetContactAliasInvalidId() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
every { ctx.pathParam("contactId") } returns "foo"
every { ctx.getFromJson(objectMapper, "alias") } returns "bar"
assertThrows(NotFoundResponse::class.java) {
controller.setContactAlias(ctx)
}
}
@Test
fun testSetContactAliasNonexistentId() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
every { ctx.pathParam("contactId") } returns "1"
every { ctx.getFromJson(objectMapper, "alias") } returns "foo"
every { contactManager.setContactAlias(ContactId(1), "foo") } throws NotFoundResponse()
assertThrows(NotFoundResponse::class.java) {
controller.setContactAlias(ctx)
}
}
@Test
fun testSetContactAliasInvalid() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
every { ctx.pathParam("contactId") } returns "1"
every { ctx.getFromJson(objectMapper, "alias") } returns getRandomString(MAX_AUTHOR_NAME_LENGTH + 1)
assertThrows(BadRequestResponse::class.java) {
controller.setContactAlias(ctx)
}
}
@Test
fun testSetContactAliasEmpty() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
every { ctx.pathParam("contactId") } returns "1"
every { ctx.getFromJson(objectMapper, "alias") } returns ""
assertThrows(BadRequestResponse::class.java) {
controller.setContactAlias(ctx)
}
}
@Test
fun testSetContactAliasMissing() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
every { ctx.pathParam("contactId") } returns "1"
every { ctx.body() } returns ""
assertThrows(BadRequestResponse::class.java) {
controller.setContactAlias(ctx)
}
}
@Test
fun testDelete() {
every { ctx.pathParam("contactId") } returns "1"
@@ -313,10 +376,11 @@ internal class ContactControllerTest : ControllerTest() {
"handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)},
"verified": ${contact.isVerified},
"lastChatActivity": $timestamp,
"connected": $connected
"connected": $connected,
"unreadCount": $unreadCount
}
"""
assertJsonEquals(json, contact.output(timestamp, connected))
assertJsonEquals(json, contact.output(timestamp, connected, unreadCount))
}
@Test

View File

@@ -10,11 +10,14 @@ import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.api.identity.AuthorInfo
import org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED
import org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED
import org.briarproject.bramble.api.sync.MessageId
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent
import org.briarproject.bramble.api.sync.event.MessagesSentEvent
import org.briarproject.bramble.test.ImmediateExecutor
import org.briarproject.bramble.test.TestUtils.getRandomId
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.api.client.SessionId
import org.briarproject.briar.api.conversation.ConversationManager
import org.briarproject.briar.api.conversation.DeletionResult
import org.briarproject.briar.api.introduction.IntroductionRequest
import org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH
import org.briarproject.briar.api.messaging.MessagingManager
@@ -24,10 +27,13 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent
import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.event.output
import org.briarproject.briar.headless.getFromJson
import org.briarproject.briar.headless.json.JsonDict
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.spongycastle.util.encoders.Base64
import kotlin.random.Random
internal class MessagingControllerImplTest : ControllerTest() {
@@ -100,6 +106,40 @@ internal class MessagingControllerImplTest : ControllerTest() {
testInvalidContactId { controller.list(ctx) }
}
@Test
fun testMessagesAckedEvent() {
val messageId1 = MessageId(getRandomId())
val messageId2 = MessageId(getRandomId())
val messageIds = listOf(messageId1, messageId2)
val event = MessagesAckedEvent(contact.id, messageIds)
every {
webSocketController.sendEvent(
EVENT_MESSAGES_ACKED,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test
fun testMessagesSentEvent() {
val messageId1 = MessageId(getRandomId())
val messageId2 = MessageId(getRandomId())
val messageIds = listOf(messageId1, messageId2)
val event = MessagesSentEvent(contact.id, messageIds)
every {
webSocketController.sendEvent(
EVENT_MESSAGES_SENT,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test
fun listNonexistentContactId() {
testNonexistentContactId { controller.list(ctx) }
@@ -162,6 +202,32 @@ internal class MessagingControllerImplTest : ControllerTest() {
assertThrows(BadRequestResponse::class.java) { controller.write(ctx) }
}
@Test
fun markMessageRead() {
mockkStatic("org.briarproject.briar.headless.RouterKt")
mockkStatic("org.spongycastle.util.encoders.Base64")
expectGetContact()
val messageIdString = message.id.bytes.toString()
every { messagingManager.getContactGroup(contact).id } returns group.id
every { ctx.getFromJson(objectMapper, "messageId") } returns messageIdString
every { Base64.decode(messageIdString) } returns message.id.bytes
every { messagingManager.setReadFlag(group.id, message.id, true) } just Runs
every { ctx.json(messageIdString) } returns ctx
controller.markMessageRead(ctx)
}
@Test
fun markMessageReadInvalidContactId() {
testInvalidContactId { controller.markMessageRead(ctx) }
}
@Test
fun markMessageReadNonexistentId() {
testNonexistentContactId { controller.markMessageRead(ctx) }
}
@Test
fun privateMessageEvent() {
val event = PrivateMessageReceivedEvent(header, contact.id)
@@ -177,6 +243,43 @@ internal class MessagingControllerImplTest : ControllerTest() {
controller.eventOccurred(event)
}
@Test
fun testOutputMessagesAckedEvent() {
val messageId1 = MessageId(getRandomId())
val messageId2 = MessageId(getRandomId())
val messageIds = listOf(messageId1, messageId2)
val event = MessagesAckedEvent(contact.id, messageIds)
val json = """
{
"contactId": ${contact.id.int},
"messageIds": [
${toJson(messageId1.bytes)},
${toJson(messageId2.bytes)}
]
}
"""
assertJsonEquals(json, event.output())
}
@Test
fun testOutputMessagesSentEvent() {
val messageId1 = MessageId(getRandomId())
val messageId2 = MessageId(getRandomId())
val messageIds = listOf(messageId1, messageId2)
val event = MessagesSentEvent(contact.id, messageIds)
val json = """
{
"contactId": ${contact.id.int},
"messageIds": [
${toJson(messageId1.bytes)},
${toJson(messageId2.bytes)}
]
}
"""
assertJsonEquals(json, event.output())
}
@Test
fun testOutputPrivateMessageHeader() {
val json = """
@@ -242,6 +345,53 @@ internal class MessagingControllerImplTest : ControllerTest() {
assertJsonEquals(json, request.output(contact.id))
}
@Test
fun testDeleteAllMessages() {
val result = DeletionResult()
every { ctx.pathParam("contactId") } returns "1"
every { conversationManager.deleteAllMessages(ContactId(1)) } returns result
every { ctx.json(result.output()) } returns ctx
controller.deleteAllMessages(ctx)
}
@Test
fun testDeleteAllMessagesInvalidContactId() {
every { ctx.pathParam("contactId") } returns "foo"
assertThrows(NotFoundResponse::class.java) {
controller.deleteAllMessages(ctx)
}
}
@Test
fun testDeleteAllMessagesNonexistentContactId() {
every { ctx.pathParam("contactId") } returns "1"
every { conversationManager.deleteAllMessages(ContactId(1)) } throws NoSuchContactException()
assertThrows(NotFoundResponse::class.java) {
controller.deleteAllMessages(ctx)
}
}
@Test
fun testOutputDeletionResult() {
val result = DeletionResult()
if (Random.nextBoolean()) result.addInvitationNotAllSelected()
if (Random.nextBoolean()) result.addInvitationSessionInProgress()
if (Random.nextBoolean()) result.addIntroductionNotAllSelected()
if (Random.nextBoolean()) result.addIntroductionSessionInProgress()
if (Random.nextBoolean()) result.addNotFullyDownloaded()
val json = """
{
"allDeleted": ${result.allDeleted()},
"hasIntroductionSessionInProgress": ${result.hasIntroductionSessionInProgress()},
"hasInvitationSessionInProgress": ${result.hasInvitationSessionInProgress()},
"hasNotAllIntroductionSelected": ${result.hasNotAllIntroductionSelected()},
"hasNotAllInvitationSelected": ${result.hasNotAllInvitationSelected()},
"hasNotFullyDownloaded": ${result.hasNotFullyDownloaded()}
}
"""
assertJsonEquals(json, result.output())
}
private fun expectGetContact() {
every { ctx.pathParam("contactId") } returns contact.id.int.toString()
every { contactManager.getContact(contact.id) } returns contact

View File

@@ -33,3 +33,12 @@ buildscript {
classpath files('libs/gradle-witness.jar')
}
}
project.ext {
buildToolsVersion = '30.0.2'
compileSdkVersion = 30
minSdkVersion = 16
targetSdkVersion = 29
versionCode = 10211
versionName = '1.2.11'
}